Files
logstream/web/style.css
T
cedricandClaude Opus 5.5 dcb09e619e Log files: one file per entry, format detection
- a path is one file (wildcards refused for now); the name defaults to
  the file name
- format "auto" (the default): detected from the last 50 lines of the
  file, the structured format followed by at least 80 % of them (Traefik
  access JSON, JSON lines, CLF, logfmt), else plain text; done again when
  the format changes, shown in the status ("Format detected: …"); any
  other format forces it
- POST /api/files/detect {path}: found / permission / empty, detected
  format and last line, shown under the row as soon as the path is typed
  (POST so that read-only accounts cannot read files through it)
- README (EN/FR) updated

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 10:33:26 +02:00

884 lines
43 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Narrow monospace fonts served by LogStream itself, so they also work offline (licences in
web/fonts). Latin subset only; other scripts fall back to --mono. Inconsolata Condensed is
Inconsolata's variable font pinned at width 75 (0.4em per character, others 0.5em). */
@font-face { font-family: "Iosevka"; src: url("fonts/iosevka-400.woff2") format("woff2"); font-weight: 400 500; font-display: swap; }
@font-face { font-family: "Iosevka"; src: url("fonts/iosevka-700.woff2") format("woff2"); font-weight: 600 800; font-display: swap; }
@font-face { font-family: "Ubuntu Mono"; src: url("fonts/ubuntu-mono-400.woff2") format("woff2"); font-weight: 400 500; font-display: swap; }
@font-face { font-family: "Ubuntu Mono"; src: url("fonts/ubuntu-mono-700.woff2") format("woff2"); font-weight: 600 800; font-display: swap; }
@font-face { font-family: "Inconsolata Condensed"; src: url("fonts/inconsolata-condensed-400.woff2") format("woff2"); font-weight: 400 500; font-display: swap; }
@font-face { font-family: "Inconsolata Condensed"; src: url("fonts/inconsolata-condensed-700.woff2") format("woff2"); font-weight: 600 800; font-display: swap; }
/* ---------- Theme: everything goes through these variables ---------- */
:root {
--bg: #f6f7f9;
--panel: #ffffff;
--panel-2: #f0f2f5;
--border: #e2e5ea;
--text: #1a1d23;
--muted: #6b7280;
--accent: #6366f1;
--accent-soft: rgba(99, 102, 241, .12);
--row-hover: #f5f6fa;
--hit: rgba(99, 102, 241, .18);
--shadow: 0 10px 30px rgba(15, 23, 42, .12);
--chip-l: 38%; /* lightness of project-colored text (labels, Docker apps) */
--src-audit: #7c4dbd; /* audit events (auditd) */
--src-ls: #1f9aa3; /* LogStream internal logs */
--src-file: #5f8a1f; /* log files (Settings > Sources > Log files) */
--src-tint: 6%; /* background tint of their rows */
--sev-crit: #be123c;
--sev-err: #dc2626;
--sev-warning: #d97706;
--sev-notice: #2563eb;
--sev-info: #0d9488;
--sev-debug: #9ca3af;
/* Timeline: severity layers, intensity, graduations */
--h-err: #f87171;
--h-warn: #fbbf24;
--h-other: #a5b4fc;
--h-all: #818cf8;
--h-calm: #a5b4fc;
--h-burst: #f59e0b;
--h-anom: #e11d48;
--h-grid: rgba(100, 116, 139, .28);
--h-hover: rgba(99, 102, 241, .10);
/* Filled severity badges; overwritten by the colors of the "error" / "warning" tags */
--tag-error: #fca5a5;
--tag-error-text: #111827;
--tag-warning: #fdba74;
--tag-warning-text: #111827;
--log-size: 12.5px; /* Settings > Interface > Font size */
--codes-w: 4.9rem; /* tag codes column: room for 3 badges */
--code-bg: #858c97; /* tag code badges */
/* --log-font is set by Settings > Interface > Font (defaults to --mono) */
--mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--radius: 10px;
color-scheme: light;
}
:root[data-theme="dark"] {
--bg: #0f1115;
--panel: #161a20;
--panel-2: #1d222a;
--border: #2a3039;
--text: #e6e8ec;
--muted: #8b93a1;
--accent: #818cf8;
--accent-soft: rgba(129, 140, 248, .16);
--row-hover: #1a1f26;
--hit: rgba(129, 140, 248, .28);
--shadow: 0 10px 30px rgba(0, 0, 0, .5);
--chip-l: 70%;
--src-audit: #a983e6;
--src-ls: #3cc3cc;
--src-file: #9ccc54;
--src-tint: 9%;
--sev-crit: #fb7185;
--sev-err: #f87171;
--sev-warning: #fbbf24;
--sev-notice: #60a5fa;
--sev-info: #2dd4bf;
--sev-debug: #6b7280;
--h-err: #ef4444;
--h-warn: #f59e0b;
--h-other: #4f5bd5;
--h-all: #6d74e8;
--h-calm: #4f5bd5;
--h-burst: #fbbf24;
--h-anom: #fb7185;
--h-grid: rgba(148, 163, 184, .2);
--h-hover: rgba(129, 140, 248, .14);
color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0f1115;
--panel: #161a20;
--panel-2: #1d222a;
--border: #2a3039;
--text: #e6e8ec;
--muted: #8b93a1;
--accent: #818cf8;
--accent-soft: rgba(129, 140, 248, .16);
--row-hover: #1a1f26;
--hit: rgba(129, 140, 248, .28);
--shadow: 0 10px 30px rgba(0, 0, 0, .5);
--chip-l: 70%;
--src-audit: #a983e6;
--src-ls: #3cc3cc;
--src-file: #9ccc54;
--src-file: #9ccc54;
--src-tint: 9%;
--sev-crit: #fb7185;
--sev-err: #f87171;
--sev-warning: #fbbf24;
--sev-notice: #60a5fa;
--sev-info: #2dd4bf;
--sev-debug: #6b7280;
--h-err: #ef4444;
--h-warn: #f59e0b;
--h-other: #4f5bd5;
--h-all: #6d74e8;
--h-calm: #4f5bd5;
--h-burst: #fbbf24;
--h-anom: #fb7185;
--h-grid: rgba(148, 163, 184, .2);
--h-hover: rgba(129, 140, 248, .14);
color-scheme: dark;
}
}
/* ---------- Base ---------- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font: 14px/1.45 var(--sans);
padding-bottom: 40px;
}
button, input, select { font: inherit; color: inherit; }
button { cursor: pointer; }
svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.muted { color: var(--muted); }
.small { font-size: 12.5px; }
[hidden] { display: none !important; }
/* ---------- Top bar ---------- */
.topbar {
position: sticky; top: 0; z-index: 20;
display: flex; align-items: center; gap: 16px;
padding: 10px 20px;
background: color-mix(in srgb, var(--panel) 88%, transparent);
backdrop-filter: saturate(160%) blur(10px);
-webkit-backdrop-filter: saturate(160%) blur(10px);
border-bottom: 1px solid var(--border);
}
.brand { display: flex; align-items: center; gap: 10px; font-weight: 650; font-size: 16px; letter-spacing: -.01em; }
.brand svg { width: 26px; height: 26px; }
.brand svg rect { fill: var(--accent); stroke: none; }
.brand svg path { stroke: #fff; stroke-width: 3; }
.search {
flex: 1; max-width: 720px;
display: flex; align-items: center; gap: 8px;
padding: 0 6px 0 12px; height: 38px;
background: var(--panel-2);
border: 1px solid transparent;
border-radius: var(--radius);
transition: border-color .15s, box-shadow .15s;
}
.search:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.search .ico { width: 17px; height: 17px; color: var(--muted); flex: none; }
.search input {
flex: 1; min-width: 0; height: 100%;
border: 0; outline: 0; background: transparent;
font-family: var(--mono); font-size: 13px;
}
.search input::-webkit-search-cancel-button { cursor: pointer; }
.chip {
flex: none;
border: 1px solid var(--border); background: var(--panel);
border-radius: 7px; padding: 3px 9px; font-size: 12px; font-weight: 600;
color: var(--muted);
}
.chip.on { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.actions { display: flex; align-items: center; gap: 6px; margin-left: auto; }
/* Logged-in user, with how they logged in (oidc / local) */
.whoami { display: flex; align-items: center; gap: 6px; min-width: 0; margin-left: 6px; font-size: 13px; color: var(--muted); }
.whoami .who-name { font-weight: 600; color: var(--text); max-width: 18ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.whoami .who-badge {
flex: none; border: 1px solid var(--border); border-radius: 6px;
padding: 1px 6px; font-size: 10.5px; font-weight: 650; letter-spacing: .02em;
}
.whoami .who-badge.oidc { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.whoami .who-badge:empty { display: none; }
.whoami.none .who-name { font-weight: 400; color: var(--muted); }
.icon-btn {
width: 36px; height: 36px; display: grid; place-items: center;
border: 1px solid transparent; background: transparent; border-radius: 9px; color: var(--muted);
}
.icon-btn:hover { background: var(--panel-2); color: var(--text); }
.icon-btn svg { width: 19px; height: 19px; }
.icon-btn.danger:hover { color: var(--sev-err); }
/* Sun or moon depending on the effective theme */
#themeBtn .moon { display: none; }
:root[data-theme="dark"] #themeBtn .sun { display: none; }
:root[data-theme="dark"] #themeBtn .moon { display: block; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) #themeBtn .sun { display: none; }
:root:not([data-theme="light"]) #themeBtn .moon { display: block; }
}
.btn {
display: inline-flex; align-items: center; gap: 7px;
height: 34px; padding: 0 12px;
border: 1px solid var(--border); border-radius: 9px;
background: var(--panel); font-weight: 550; font-size: 13px;
}
.btn:hover { background: var(--panel-2); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { filter: brightness(1.08); }
.btn.ghost { border-color: transparent; background: transparent; color: var(--muted); }
.btn.ghost:hover { color: var(--text); background: var(--panel-2); }
.live .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
.live.on { color: var(--sev-info); border-color: color-mix(in srgb, var(--sev-info) 45%, var(--border)); }
.live.on .dot { background: var(--sev-info); animation: pulse 1.6s infinite; }
.live:disabled { opacity: .5; cursor: not-allowed; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sev-info) 60%, transparent); }
70% { box-shadow: 0 0 0 7px transparent; }
100% { box-shadow: 0 0 0 0 transparent; }
}
.progress { position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; overflow: hidden; }
body.busy .progress::after {
content: ""; position: absolute; inset: 0; width: 30%;
background: var(--accent);
animation: slide 1s ease-in-out infinite;
}
@keyframes slide { from { transform: translateX(-100%); } to { transform: translateX(400%); } }
/* ---------- Filters ---------- */
.filters {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
padding: 12px 20px 8px;
}
.filters select {
height: 32px; padding: 0 28px 0 10px;
border: 1px solid var(--border); border-radius: 8px;
background: var(--panel) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b93a1' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 8px center / 14px;
appearance: none; -webkit-appearance: none;
font-size: 13px; max-width: 220px;
}
.filters select.set { border-color: var(--accent); background-color: var(--accent-soft); }
/* Display mode: Stream or Time range, then the start → end fields of the range */
.view-mode { padding: 2px; gap: 2px; border-radius: 9px; }
.view-mode button { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; }
.view-mode svg { width: 15px; height: 15px; }
.period { display: inline-flex; align-items: center; gap: 6px; }
.period .btn.tool { width: 32px; padding: 0; justify-content: center; }
.period input {
height: 32px; padding: 0 8px;
border: 1px solid var(--accent); border-radius: 8px; background: var(--accent-soft);
font-size: 13px; font-variant-numeric: tabular-nums; color-scheme: light dark;
}
.period input:focus { outline: 0; box-shadow: 0 0 0 3px var(--accent-soft); }
.period input.bad { border-color: var(--sev-err); background: color-mix(in srgb, var(--sev-err) 10%, transparent); }
.spacer { flex: 1; }
#count { font-size: 12.5px; font-variant-numeric: tabular-nums; }
/* Export button and menu */
.export { position: relative; }
.btn.tool { height: 32px; padding: 0 10px; font-size: 13px; }
.btn.tool svg { width: 16px; height: 16px; }
.menu {
position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
width: 290px; padding: 6px;
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
box-shadow: var(--shadow);
}
.menu button {
display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%;
padding: 8px 10px; border: 0; border-radius: 8px; background: transparent; text-align: left;
}
.menu button:hover, .menu button:focus-visible { background: var(--panel-2); outline: 0; }
.menu strong { font-size: 13px; font-weight: 600; }
.menu small { font-size: 12px; color: var(--muted); }
.menu-note { margin: 4px 0 0; padding: 8px 10px 4px; border-top: 1px solid var(--border); font-size: 11.5px; color: var(--muted); }
/* Docker logs */
.src-ico { width: 1em; height: 1em; vertical-align: -.15em; margin-right: 4px; color: #2496ed; }
/* Docker app names take the color of their compose project (see hueOf in app.js) */
.row .app.proj { color: hsl(var(--h) 55% var(--chip-l)); }
.row .app.proj .src-ico { color: inherit; }
/* ---------- Timeline (histogram) ---------- */
.histo { position: relative; padding: 0 20px 6px; user-select: none; -webkit-user-select: none; }
.h-plot {
position: relative; height: var(--histo-h, 80px); cursor: crosshair; touch-action: none;
border-bottom: 1px solid var(--border);
}
.h-svg { position: absolute; inset: 0; width: 100%; height: 100%; stroke: none; } /* no icon outline (global svg rule): it would be stretched with the bars */
.h-svg rect { shape-rendering: crispEdges; }
.h-svg .l-err { fill: var(--h-err); }
.h-svg .l-warn { fill: var(--h-warn); }
.h-svg .l-other { fill: var(--h-other); }
.h-svg .l-all { fill: var(--h-all); }
.h-svg .l-calm { fill: var(--h-calm); }
.h-svg .l-burst { fill: var(--h-burst); }
.h-svg .l-anom { fill: var(--h-anom); }
.h-grid i { position: absolute; top: 0; bottom: 0; border-left: 1px dashed var(--h-grid); pointer-events: none; }
.h-grid i.day { border-left-style: solid; }
.h-hover { position: absolute; top: 0; bottom: 0; background: var(--h-hover); pointer-events: none; }
.h-sel { position: absolute; top: 0; bottom: 0; background: var(--accent-soft); box-shadow: inset 1px 0 var(--accent), inset -1px 0 var(--accent); pointer-events: none; }
.h-axis { position: relative; height: 16px; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.h-axis span { position: absolute; top: 2px; transform: translateX(-50%); white-space: nowrap; }
.h-axis span.day { color: var(--text); font-weight: 600; }
.h-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 11px; color: var(--muted); }
.h-meta .sp { flex: 1; }
.h-meta .warn { color: var(--sev-warning); cursor: help; }
.h-meta .link-btn { font-size: 11px; }
.h-leg { display: inline-flex; gap: 10px; }
.histo .sw { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 5px; }
.h-tip {
position: absolute; z-index: 30; pointer-events: none; min-width: 170px;
background: var(--panel); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow);
padding: 7px 10px; font-size: 12px; font-variant-numeric: tabular-nums;
}
.h-tip .hd { font-weight: 600; margin-bottom: 4px; }
.h-tip .ln { display: flex; justify-content: space-between; gap: 14px; line-height: 1.55; }
/* ---------- Log list ---------- */
.list { margin: 6px 20px 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.list:empty { display: none; }
/* Columns: received, message time, severity, host, app, tag codes, message. Widths come from
--col-* (set on #table when a column has been resized, see app.js), shared by
the header and every row through subgrid so that the columns line up. */
.table {
display: grid;
grid-template-columns: var(--col-rcv, max-content) var(--col-mt, max-content) var(--col-sev, 4.6rem)
var(--col-host, minmax(5rem, 9rem)) var(--col-app, minmax(4rem, 8rem)) var(--codes-w) minmax(0, 1fr);
column-gap: 12px;
margin: 6px 20px 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
overflow: clip; /* rounded corners without breaking the sticky header (hidden would) */
}
.table[hidden] { display: none; }
.table .list {
display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
margin: 0; border: 0; border-radius: 0; background: none; overflow: visible;
}
.table .row { grid-template-columns: subgrid; grid-column: 1 / -1; }
.list-head {
display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
position: sticky; top: var(--topbar-h, 0px); z-index: 5;
padding: 0 14px 0 11px; border-left: 3px solid transparent;
background: var(--panel-2); border-bottom: 1px solid var(--border);
font-size: 10.5px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--muted);
}
.list-head > span { position: relative; min-width: 0; padding: 4px 0; }
/* Labels never widen a column (contain: inline-size), they are cut instead */
.list-head .lbl { display: block; contain: inline-size; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: inherit; }
.list-head .grip { position: absolute; top: 0; bottom: 0; right: -9px; width: 6px; z-index: 1; cursor: col-resize; touch-action: none; }
.list-head .grip::after { content: ""; position: absolute; left: 2px; top: 25%; bottom: 25%; border-left: 1px solid var(--border); }
.list-head .grip:hover::after, .list-head .grip.active::after { top: 0; bottom: 0; border-left: 2px solid var(--accent); left: 2px; }
body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-select: none; -webkit-user-select: none; }
.cols-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin-top: 12px; }
.row {
display: grid;
grid-template-columns: max-content max-content 4.6rem minmax(5rem, 9rem) minmax(4rem, 8rem) var(--codes-w) 1fr;
gap: 0 12px; align-items: baseline;
padding: 5px 14px 5px 11px;
border-left: 3px solid transparent;
border-bottom: 1px solid var(--border);
font-family: var(--log-font, var(--mono)); font-size: var(--log-size);
font-variant-ligatures: none; /* keep -> != etc. as typed */
cursor: pointer;
}
.row:last-child { border-bottom: 0; }
/* Settings > Interface > Density: compact fits about 50% more lines on screen */
:root[data-density="compact"] .row { padding-top: 1px; padding-bottom: 1px; line-height: 1.25; }
:root[data-density="compact"] .row .sev { padding-top: 0; padding-bottom: 0; line-height: 1.3; }
:root[data-density="compact"] .row .codes b { padding-top: 0; padding-bottom: 0; line-height: 1.2; }
.row:hover { background: var(--row-hover); }
.row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.row.new { animation: flash 1.2s ease-out; }
@keyframes flash { from { background: var(--accent-soft); } to { background: transparent; } }
.row > * { min-width: 0; }
.row time { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; }
.row time.rcv { color: var(--text); }
.row time .ms { opacity: .6; }
.row .host, .row .app { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .host { font-weight: 600; }
.row .app { color: var(--muted); }
.row .msg { white-space: pre-wrap; word-break: break-word; min-width: 0; }
.sev {
justify-self: start; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
padding: 1px 6px; border-radius: 5px;
color: var(--sev); background: color-mix(in srgb, var(--sev) 14%, transparent);
}
.sev-crit { --sev: var(--tag-error); --sev-text: var(--tag-error-text); }
.sev-err { --sev: var(--tag-error); --sev-text: var(--tag-error-text); }
.sev-warning { --sev: var(--tag-warning); --sev-text: var(--tag-warning-text); }
.sev-notice { --sev: var(--sev-notice); }
.sev-info { --sev: var(--sev-info); }
.sev-debug { --sev: var(--sev-debug); }
.row.sev-crit .sev, .row.sev-err .sev, .row.sev-warning .sev { background: var(--sev); color: var(--sev-text); }
.row.sev-crit .sev { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--sev-text) 45%, transparent); }
.row.sev-crit, .row.sev-err, .row.sev-warning { border-left-color: var(--sev); }
.row.sev-crit, .row.sev-err { background: color-mix(in srgb, var(--sev) 7%, transparent); }
.row .host[data-act], .row .app[data-act] { cursor: pointer; }
/* Audit events and LogStream internal logs: colored edge (over the severity
one), light tint, badge before the app name. */
.row.src-audit { --src: var(--src-audit); }
.row.src-logstream { --src: var(--src-ls); }
.row.src-file { --src: var(--src-file); }
.row.src-audit, .row.src-logstream, .row.src-file { border-left-color: var(--src); background: color-mix(in srgb, var(--src) var(--src-tint), transparent); }
.row.src-audit:hover, .row.src-logstream:hover, .row.src-file:hover { background: color-mix(in srgb, var(--src) 13%, var(--row-hover)); }
.src-badge {
display: inline-block; margin-right: 5px; padding: 0 4px; border-radius: 4px; vertical-align: 1px;
font: 700 9.5px/1.45 var(--sans); letter-spacing: .04em;
color: var(--src); background: color-mix(in srgb, var(--src) 16%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--src) 40%, transparent);
}
.row .raw {
margin-top: 2px; font-size: .85em; line-height: 1.35; color: var(--muted);
white-space: pre-wrap; word-break: break-all;
}
:root[data-density="compact"] .row:not(.open) .raw { display: none; }
.src-help { margin-top: 10px !important; }
.self-level { width: auto; justify-self: start; }
/* Codes of the tags found in the message: grey badges, fixed size */
.row .codes { white-space: nowrap; overflow: hidden; }
.row .codes > span { display: inline-flex; gap: 3px; vertical-align: 1px; }
.row .codes b, .tag-code {
min-width: 2.2ch; padding: 1px 4px; border-radius: 5px; text-align: center;
/* same font as the severity badges (the log font) */
font-family: var(--log-font, var(--mono)); font-size: 10.5px; font-weight: 700; letter-spacing: .03em;
line-height: 1.35; font-variant-numeric: tabular-nums;
background: var(--code-bg); color: #0b0f17;
}
.row .codes b.more { background: none; box-shadow: inset 0 0 0 1px var(--code-bg); color: var(--muted); }
.row .host[data-act]:hover, .row .app[data-act]:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
mark.tag {
padding: 0 4px; border-radius: 4px; font-weight: 650;
box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
mark.hit { background: var(--hit); color: inherit; border-radius: 3px; padding: 0 1px; }
/* Message details */
.details {
grid-column: 1 / -1;
margin: 8px 0 4px; padding: 10px 12px;
background: var(--panel-2); border-radius: 8px;
cursor: auto; font-size: calc(var(--log-size) - .5px);
}
.details dl { display: grid; grid-template-columns: max-content 1fr; gap: 3px 16px; margin: 0; }
.details dt { color: var(--muted); }
.details dd { margin: 0; word-break: break-all; }
.details .acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; font-family: var(--sans); }
.details .acts .btn { height: 28px; font-size: 12px; }
/* ---------- States ---------- */
.empty { text-align: center; padding: 60px 20px; }
.empty svg { width: 40px; height: 40px; color: var(--muted); opacity: .6; }
.empty p { margin: 6px 0; }
.error-banner {
margin: 6px 20px 0; padding: 10px 14px;
border: 1px solid color-mix(in srgb, var(--sev-err) 40%, transparent);
background: color-mix(in srgb, var(--sev-err) 10%, transparent);
color: var(--sev-err); border-radius: var(--radius);
font-family: var(--mono); font-size: 12.5px; white-space: pre-wrap;
}
.auth-warn {
display: flex; align-items: center; gap: 10px;
margin: 6px 20px 0; padding: 6px 8px 6px 14px;
border: 1px solid color-mix(in srgb, var(--sev-warning) 45%, transparent);
background: color-mix(in srgb, var(--sev-warning) 12%, transparent);
color: var(--text); border-radius: var(--radius); font-size: 12.5px;
}
.auth-warn[hidden] { display: none; }
.auth-warn span { flex: 1; }
.auth-warn .icon-btn { width: 26px; height: 26px; flex: none; }
.ro-note {
margin: 0 0 12px; padding: 8px 12px; border-radius: var(--radius); font-size: 12.5px;
background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--text);
}
.read-only .set-panel .set-section[inert] { opacity: .55; }
.pill {
position: fixed; top: 70px; left: 50%; transform: translateX(-50%); z-index: 30;
border: 0; border-radius: 999px; padding: 7px 16px;
background: var(--accent); color: #fff; font-weight: 600; font-size: 13px;
box-shadow: var(--shadow);
}
.status {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 10;
display: flex; gap: 16px; align-items: center;
padding: 6px 20px; font-size: 12px;
background: var(--panel); border-top: 1px solid var(--border);
font-variant-numeric: tabular-nums;
}
.conn { white-space: nowrap; }
.conn::before { content: "●"; margin-right: 6px; color: var(--muted); }
.conn.ok::before { color: var(--sev-info); }
.conn.ko::before { color: var(--sev-err); }
#stats .bad { color: var(--sev-err); }
#stats .warn { color: var(--sev-warning); }
/* "Back to top", at the right end of the status bar: never over a log row */
.to-top { width: 24px; height: 24px; margin: -5px -6px -5px auto; flex: none; } /* no taller bar */
.to-top svg { width: 16px; height: 16px; }
.to-top[hidden] { display: none; }
.toast {
position: fixed; bottom: 48px; left: 50%; transform: translateX(-50%); z-index: 50;
background: var(--text); color: var(--bg);
padding: 8px 14px; border-radius: 8px; font-size: 13px; box-shadow: var(--shadow);
}
/* ---------- Settings dialog ---------- */
dialog.settings {
width: min(1280px, calc(100vw - 32px));
height: calc(100vh - 48px); max-height: 960px;
}
dialog.settings .dlg { display: flex; flex-direction: column; height: 100%; padding: 0; }
dialog.settings .dlg > header { padding: 14px 16px 14px 22px; border-bottom: 1px solid var(--border); }
.set-body { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(176px, max-content) minmax(0, 1fr); }
.set-nav {
display: flex; flex-direction: column; gap: 2px;
padding: 12px 10px; border-right: 1px solid var(--border);
background: color-mix(in srgb, var(--panel-2) 50%, transparent);
}
.set-nav button {
display: flex; align-items: center; gap: 10px;
height: 38px; padding: 0 12px; border: 0; border-radius: 9px;
background: transparent; color: var(--muted); font-weight: 560; font-size: 13.5px; text-align: left;
white-space: nowrap;
}
.set-nav button:hover { background: var(--panel-2); color: var(--text); }
.set-nav button[aria-selected="true"] { background: var(--accent-soft); color: var(--accent); }
.set-nav svg { width: 18px; height: 18px; flex: none; }
.set-panels { overflow-y: auto; padding: 16px 20px 20px; }
/* Sections are cards laid out in columns when there is room; .span ones take the full width. */
.set-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 14px; align-items: start; grid-auto-flow: row dense; }
.set-panel[hidden] { display: none; }
.set-panel.wide { grid-template-columns: minmax(0, 1fr); }
.set-panel > .set-section, .set-panel > .set-section + .set-section {
margin: 0; padding: 14px 16px; min-width: 0;
border: 1px solid var(--border); border-radius: 12px;
background: color-mix(in srgb, var(--panel-2) 35%, var(--panel));
}
.set-panel > .set-section.span { grid-column: 1 / -1; }
.set-panel > .ro-note { grid-column: 1 / -1; margin: 0; }
.sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.hint { margin-top: 8px !important; }
.lbl { font-size: 13px; font-weight: 550; }
.set-panel .preview-list { margin: 14px 0 10px; pointer-events: none; }
.preview-list .row {
cursor: default;
grid-template-columns: max-content max-content minmax(0, max-content) minmax(0, 1fr);
grid-template-areas: "rcv sev host app" "msg msg msg msg";
gap: 3px 10px;
}
.preview-list .row time.rcv { grid-area: rcv; }
.preview-list .row time.mt { display: none; }
.preview-list .row .sev { grid-area: sev; }
.preview-list .row .host { grid-area: host; }
.preview-list .row .app { grid-area: app; display: block; }
.preview-list .row .msg { grid-area: msg; }
.preview-list .row .codes { display: none; }
dialog {
width: min(720px, calc(100vw - 32px)); max-height: calc(100vh - 64px);
padding: 0; border: 1px solid var(--border); border-radius: 14px;
background: var(--panel); color: var(--text); box-shadow: var(--shadow);
}
dialog::backdrop { background: rgba(10, 12, 16, .45); backdrop-filter: blur(2px); }
.dlg { padding: 18px 20px 16px; }
.dlg header { display: flex; align-items: center; justify-content: space-between; }
.dlg h2 { margin: 0; font-size: 17px; }
.set-section { margin-top: 18px; }
.set-section + .set-section { padding-top: 16px; border-top: 1px solid var(--border); }
.set-section h3 { margin: 0 0 8px; font-size: 13px; font-weight: 650; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.set-section p { margin: 0; }
.field-grid { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px 12px; align-items: center; }
.field-grid label { font-size: 13px; font-weight: 550; }
select.field {
height: 34px; padding: 0 30px 0 10px; min-width: 0; width: 100%;
border: 1px solid var(--border); border-radius: 8px;
background: var(--panel) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b93a1' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 9px center / 14px;
appearance: none; -webkit-appearance: none; font-size: 13px;
}
select.field:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.time-preview {
margin: 10px 0 6px !important; padding: 8px 12px; border-radius: 8px;
background: var(--panel-2); font-family: var(--mono); font-size: 12.5px;
}
.set-section.danger h3 { color: var(--sev-err); }
.db-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin: 4px 0 6px; }
.db-stats {
display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 5px 14px;
margin: 0 0 14px; padding: 10px 12px; border-radius: 8px; background: var(--panel-2); font-size: 12.5px;
}
.db-stats dt { color: var(--muted); }
.db-stats dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.db-stats dd b { font-weight: 600; font-variant-numeric: tabular-nums; }
.db-msg { grid-column: 1 / -1; color: var(--muted); }
.db-msg.bad { color: var(--sev-err); }
.link-btn:disabled { opacity: .5; cursor: default; text-decoration: none; }
.purge-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 10px; }
.btn.danger { color: var(--sev-err); border-color: color-mix(in srgb, var(--sev-err) 45%, var(--border)); }
.btn.danger:hover:not(:disabled) { background: var(--sev-err); border-color: var(--sev-err); color: #fff; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.purge-status { font-size: 12.5px; }
.purge-status.bad { color: var(--sev-err); }
.purge-status.good { color: var(--sev-info); }
.docker-status { font-size: 13px; margin: 0 0 12px !important; }
.docker-status.good::before, .docker-status.bad::before { content: "●"; margin-right: 6px; }
.docker-status.good::before { color: var(--sev-info); }
.docker-status.bad { color: var(--sev-err); }
.switch-row { display: flex; align-items: center; gap: 10px; font-size: 13px; font-weight: 550; cursor: pointer; }
.dk-opts { display: flex; flex-direction: column; gap: 8px; }
.sl-grid { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 12px 14px; align-items: baseline; margin-top: 14px; }
.sl-protos { display: inline-flex; min-height: 0; padding: 5px; justify-self: start; }
.sl-protos.off { opacity: .5; }
.sl-port { display: flex; flex-direction: column; align-items: flex-start; gap: 5px; }
.sl-port code {
font: 12px var(--mono); padding: 2px 8px; border-radius: 5px;
background: var(--panel-2); border: 1px solid var(--border);
}
.dk-toolbar { display: flex; align-items: center; gap: 12px; margin: 14px 0 6px; }
.dk-filter {
flex: 1; min-width: 0; height: 28px; padding: 0 10px;
border: 1px solid var(--border); border-radius: 7px; background: var(--panel);
font: 12px var(--mono); outline: 0;
}
.dk-filter:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.link-btn { border: 0; background: none; padding: 0; font-size: 12px; font-weight: 550; color: var(--accent); white-space: nowrap; }
.link-btn:hover { text-decoration: underline; }
.dk-legend { font-size: 11.5px; color: var(--muted); margin: 8px 0 12px !important; }
/* Container labels (Settings > Sources) */
.chip-field {
display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
padding: 8px; min-height: 40px;
border: 1px solid var(--border); border-radius: 10px; background: var(--panel);
}
.chip {
display: inline-flex; align-items: center; gap: 4px;
height: 22px; padding: 0 7px;
border: 1px solid var(--border); border-radius: 5px; background: transparent;
color: var(--muted); font: 11px/1 var(--mono); white-space: nowrap; cursor: pointer;
}
.chip:hover { color: var(--text); border-color: var(--muted); }
.chip.on {
color: hsl(var(--h) 55% var(--chip-l));
border-color: hsl(var(--h) 50% var(--chip-l) / .7);
background: hsl(var(--h) 60% 50% / .13);
}
.chip.on:hover { background: hsl(var(--h) 60% 50% / .22); }
.chip.stopped { border-style: dashed; }
.chip.stopped:not(.on) { opacity: .7; }
.chip.locked { opacity: .45; cursor: not-allowed; }
.chip svg { width: 10px; height: 10px; }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.chip-sep { width: 1px; height: 18px; margin: 0 4px; background: var(--border); }
.chip-empty { font-size: 12px; color: var(--muted); padding: 0 4px; }
/* Toggle switch */
input.switch {
appearance: none; -webkit-appearance: none; flex: none;
width: 34px; height: 20px; margin: 0; border-radius: 999px;
background: var(--border); position: relative; cursor: pointer; transition: background .15s;
}
input.switch::after {
content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .25); transition: transform .15s;
}
input.switch:checked { background: var(--accent); }
input.switch:checked::after { transform: translateX(14px); }
input.switch:disabled { cursor: not-allowed; }
input.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Settings > Pattern: toolbar kept in view while the list scrolls, dense rows. */
.tags-section { padding-top: 12px !important; }
.tag-toolbar {
position: sticky; top: -16px; z-index: 2;
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
margin: 10px -16px 0; padding: 8px 16px;
background: color-mix(in srgb, var(--panel-2) 35%, var(--panel));
border-bottom: 1px solid var(--border);
}
.tag-toolbar .btn { height: 30px; }
.tag-toolbar select.field { width: auto; height: 30px; }
.tag-toolbar .dk-filter { flex: 1 1 220px; height: 30px; }
.tag-toolbar #resetTags { margin-left: auto; }
.seg { display: inline-flex; padding: 3px; gap: 3px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 10px; }
.seg button {
border: 0; background: transparent; border-radius: 7px;
height: 30px; padding: 0 14px; font-size: 13px; font-weight: 600; color: var(--muted);
}
.seg button:hover { color: var(--text); }
.seg button[aria-checked="true"] { background: var(--panel); color: var(--accent); box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
/* One wide column: long regular expressions stay readable. */
.tag-list { display: flex; flex-direction: column; gap: 3px; margin-top: 10px; }
.tag-row {
display: grid; align-items: center; gap: 4px 6px;
grid-template-columns: 2.4em 30px minmax(7rem, 1fr) 10rem auto 26px;
padding: 3px 4px 3px 6px; border: 1px solid var(--border); border-radius: 8px;
background: var(--panel);
}
.tag-row[hidden] { display: none; }
.tag-row.off { opacity: .55; }
.tag-row .tag-code { font-size: 11px; padding: 2px 4px; cursor: default; text-align: center; }
.tag-row input[type="color"] {
width: 30px; height: 26px; padding: 0; border: 1px solid var(--border); border-radius: 6px;
background: none; cursor: pointer;
}
.tag-row input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
.tag-row input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 5px; }
.tag-row input[type="text"] {
height: 26px; padding: 0 8px; min-width: 0;
border: 1px solid var(--border); border-radius: 6px; background: var(--panel);
font-family: var(--mono); font-size: 12.5px; outline: 0;
}
.tag-row input[type="text"]:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.tag-row .preview { font-family: var(--mono); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }
.tag-row .opts { display: flex; gap: 3px; }
.opt {
display: inline-flex; align-items: center; height: 24px; padding: 0 6px;
border: 1px solid var(--border); border-radius: 6px; background: var(--panel);
font-size: 11px; font-weight: 600; color: var(--muted); cursor: pointer; user-select: none;
}
.tag-row .icon-btn { width: 26px; height: 26px; border-radius: 6px; }
.tag-row .icon-btn svg { width: 15px; height: 15px; }
.opt input { display: none; }
.opt:has(input:checked) { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
/* Log files (Settings > Sources) */
.file-list { display: flex; flex-direction: column; gap: 4px; }
.file-row {
display: grid; align-items: center; gap: 6px;
grid-template-columns: auto minmax(7rem, 1fr) minmax(10rem, 2fr) minmax(9rem, 1.4fr) minmax(6rem, .8fr) 26px;
padding: 6px 6px 6px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel);
}
.file-row.off > input[type="text"], .file-row.off > select { opacity: .6; }
.file-row input[type="text"] {
height: 28px; padding: 0 8px; min-width: 0;
border: 1px solid var(--border); border-radius: 6px; background: var(--panel); font-size: 12.5px; outline: 0;
}
.file-row input.mono { font-family: var(--mono); }
.file-row input[type="text"]:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.file-row select.field { height: 28px; }
.file-row .icon-btn { width: 26px; height: 26px; border-radius: 6px; }
.file-row .icon-btn svg { width: 15px; height: 15px; }
.file-row .fr-regex, .file-row .fr-status, .file-row .fr-sample { grid-column: 2 / -1; }
.file-row .fr-sample {
margin: 0; font: 11.5px var(--mono); color: var(--muted);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.file-row .fr-sample[hidden] { display: none; }
.file-row .fr-regex[hidden] { display: none; }
.file-row .fr-status { margin: 0 !important; font-size: 12px; }
.file-toolbar { margin-top: 10px; }
.tag-row .err { grid-column: 1 / -1; color: var(--sev-err); font-size: 12px; }
.tag-row .err:empty { display: none; }
/* ---------- Mobile ---------- */
@media (max-width: 760px) {
.file-row { grid-template-columns: auto minmax(0, 1fr) 26px; }
.file-row [data-del] { grid-column: 3; grid-row: 1; }
.file-row > [data-f="path"], .file-row > [data-f="format"], .file-row > [data-f="host"] { grid-column: 2 / -1; }
body { font-size: 13.5px; }
.topbar { flex-wrap: wrap; padding: 8px 16px; gap: 8px 10px; }
.search { order: 3; flex-basis: 100%; max-width: none; }
.live .lbl { display: none; }
.whoami .who-name { max-width: 12ch; }
.filters { padding: 10px 16px 6px; }
.filters select { flex: 1 1 calc(50% - 8px); max-width: none; min-width: 0; }
.view-mode { flex: 1 1 100%; }
.view-mode button { flex: 1; justify-content: center; }
/* Phones: the two dates on one line, the ◀ ▶ − buttons below */
.period { flex: 1 1 100%; flex-wrap: wrap; }
.period > span { display: none; }
.period input { flex: 1 1 calc(50% - 3px); min-width: 0; }
.period .btn.tool { order: 1; flex: 1; }
.spacer { display: none; }
#count { flex: 1 1 50%; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.histo { padding: 0 16px 6px; }
.h-leg { display: none; }
.list, .error-banner, .auth-warn, .table { margin-left: 16px; margin-right: 16px; }
/* Phones: no columns, two lines per log (layout below); the widths do not apply */
.table { display: block; }
.table .list { display: block; margin: 0; }
.list-head { display: none; }
.row, .table .row {
grid-template-columns: auto auto 1fr auto;
grid-template-areas: "rcv sev host codes" "msg msg msg msg";
gap: 3px 8px; padding: 8px 12px 8px 10px;
}
.row time.rcv { grid-area: rcv; }
.row time.mt { display: none; } /* still shown in the details panel */
.row .sev { grid-area: sev; }
.row .host { grid-area: host; justify-self: end; max-width: 100%; }
.row .app { display: none; }
.row .codes { grid-area: codes; }
.row .msg { grid-area: msg; }
.details { grid-column: 1 / -1; }
.details dl { grid-template-columns: 1fr; }
.details dt { margin-top: 4px; }
.field-grid { grid-template-columns: 1fr; gap: 4px; }
dialog.settings { height: calc(100vh - 24px); width: calc(100vw - 16px); }
.set-body { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
.set-nav {
display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); padding: 6px;
border-right: 0; border-bottom: 1px solid var(--border);
}
.set-nav button {
flex-direction: column; justify-content: center; height: auto; padding: 6px 2px; gap: 3px;
font-size: 11.5px; text-align: center;
}
.set-nav button span { max-width: 100%; white-space: normal; line-height: 1.15; overflow: hidden; text-overflow: ellipsis; }
.set-panels { padding: 12px 12px 18px; }
.set-panel { grid-template-columns: minmax(0, 1fr); gap: 10px; }
.set-panel > .set-section, .set-panel > .set-section + .set-section { padding: 12px; }
.tag-toolbar { position: static; margin: 10px -12px 0; padding: 8px 12px; }
.tag-toolbar #resetTags { margin-left: 0; }
#sizeSwitch, #densitySwitch { display: flex; }
#sizeSwitch button, #densitySwitch button { flex: 1; padding: 0 4px; white-space: nowrap; font-size: 12px; }
:root[data-density="compact"] .row { padding-top: 3px; padding-bottom: 3px; }
.field-grid select { margin-bottom: 6px; }
.status { padding: 6px 16px; }
.tag-row { grid-template-columns: auto 30px 1fr 26px; }
.tag-row .preview { display: none; }
.tag-row .opts { grid-column: 1 / -1; grid-row: 2; flex-wrap: wrap; }
.tag-row [data-del] { grid-column: 4; grid-row: 1; }
}
/* ---------- Login page (AUTH_MODE=local) ---------- */
body.login-page {
min-height: 100vh; padding: 16px;
display: grid; place-items: center;
}
.login-tools { position: fixed; top: 12px; right: 16px; display: flex; align-items: center; gap: 8px; }
.login-card {
width: 100%; max-width: 360px;
display: flex; flex-direction: column; align-items: center; gap: 10px;
padding: 32px 28px 28px;
background: var(--panel); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow);
}
.login-logo { max-width: 200px; max-height: 96px; object-fit: contain; margin-bottom: 6px; }
.login-card .brand { font-size: 20px; }
.login-card .brand svg { width: 32px; height: 32px; }
.login-card > p { margin: 0 0 8px; text-align: center; }
.login-card form { width: 100%; display: flex; flex-direction: column; gap: 6px; }
.login-card label { font-size: 13px; font-weight: 550; }
.login-card input {
height: 38px; padding: 0 11px; margin-bottom: 6px;
border: 1px solid var(--border); border-radius: 9px; background: var(--bg);
}
.login-card input:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.login-card .btn.primary { height: 38px; justify-content: center; margin-top: 6px; font-size: 14px; }
.login-err { margin: 0; color: var(--sev-err); font-size: 13px; }