Off by default (stats in jarl.yaml). When enabled, Jarl adds a loopback statistics channel and a response log to named's configuration (merged into existing logging/statistics-channels statements) and switches response logging with rndc responselog, because the responselog option only counts when named starts. jarld polls named's counters every minute and tails the response log (resuming after restarts and across rotation). It stores aggregates, never individual queries, in a separate SQLite database: per-minute, hourly and daily counters (queries, result codes, record types, views, zones, cache hits, upstream latency, DNSSEC) and hourly/daily top lists (names, clients, NXDOMAIN and SERVFAIL names, refused clients), each with its own retention. Clients can be anonymized or excluded. - Web UI: Statistics page with KPI tiles, queries and failed answers over time, a weekday x hour heatmap, top lists and breakdowns; SVG charts with hover/keyboard readouts, a table view and light/dark palettes validated for colour-vision deficiency. - CLI: jarl stats status|summary|top|timeline|heatmap|breakdown. - API: /api/v1/stats/*, behind a new "stats" permission resource. - jarld brings named's configuration up to date with jarl.yaml at startup (engine.SyncConfig), never overwriting hand-edited files. - resync now refuses to overwrite drifted files without force, as documented. - tmpfiles.d creates /var/log/named (allowed by named's AppArmor profile on Debian 13 and Ubuntu). - The e2e harness runs named with -f like the Debian unit. - docs/stats.md, plus API, CLI, configuration, permissions and README updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
346 lines
18 KiB
CSS
346 lines
18 KiB
CSS
/* Jarl web UI. Plain CSS, light and dark themes. */
|
|
:root {
|
|
--bg: #f6f7f9;
|
|
--panel: #ffffff;
|
|
--text: #1d2330;
|
|
--muted: #5f6b7d;
|
|
--border: #d9dee6;
|
|
--accent: #2f5fb3;
|
|
--accent-text: #ffffff;
|
|
--accent-soft: #e5edfa;
|
|
--ok: #1e7a46;
|
|
--ok-soft: #e3f4ea;
|
|
--warn: #8a5a00;
|
|
--warn-soft: #fdf2d9;
|
|
--err: #b3261e;
|
|
--err-soft: #fbe7e5;
|
|
--info-soft: #eef1f6;
|
|
--code-bg: #f0f2f5;
|
|
--add: #e6f6ea;
|
|
--add-text: #135c2c;
|
|
--del: #fde8e7;
|
|
--del-text: #8c1d18;
|
|
--hunk: #e8eefb;
|
|
--shadow: 0 1px 2px rgba(20, 30, 50, 0.08);
|
|
--radius: 6px;
|
|
--mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
|
color-scheme: light;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--bg: #14171c;
|
|
--panel: #1c2027;
|
|
--text: #e3e7ee;
|
|
--muted: #9aa5b5;
|
|
--border: #2f3540;
|
|
--accent: #6d9be8;
|
|
--accent-text: #0f1520;
|
|
--accent-soft: #23314a;
|
|
--ok: #59c287;
|
|
--ok-soft: #173326;
|
|
--warn: #e5b450;
|
|
--warn-soft: #3a2f15;
|
|
--err: #f08077;
|
|
--err-soft: #3c1f1d;
|
|
--info-soft: #232833;
|
|
--code-bg: #242933;
|
|
--add: #183a25;
|
|
--add-text: #9fe3b5;
|
|
--del: #43201e;
|
|
--del-text: #f7b2ac;
|
|
--hunk: #23314a;
|
|
--shadow: none;
|
|
color-scheme: dark;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--bg: #14171c; --panel: #1c2027; --text: #e3e7ee; --muted: #9aa5b5; --border: #2f3540;
|
|
--accent: #6d9be8; --accent-text: #0f1520; --accent-soft: #23314a; --ok: #59c287; --ok-soft: #173326;
|
|
--warn: #e5b450; --warn-soft: #3a2f15; --err: #f08077; --err-soft: #3c1f1d; --info-soft: #232833;
|
|
--code-bg: #242933; --add: #183a25; --add-text: #9fe3b5; --del: #43201e; --del-text: #f7b2ac; --hunk: #23314a;
|
|
--shadow: none; color-scheme: dark;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; }
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font: 14px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, sans-serif;
|
|
}
|
|
a { color: var(--accent); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
code, pre, .mono, textarea.mono, input.mono { font-family: var(--mono); font-size: 13px; }
|
|
pre { background: var(--code-bg); padding: 10px 12px; border-radius: var(--radius); overflow: auto; margin: 0; white-space: pre; }
|
|
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
h2 { font-size: 16px; margin: 18px 0 8px; }
|
|
h3 { font-size: 14px; margin: 14px 0 6px; }
|
|
.muted { color: var(--muted); }
|
|
.small { font-size: 12px; }
|
|
.nowrap { white-space: nowrap; }
|
|
.right { text-align: right; }
|
|
.hidden { display: none !important; }
|
|
|
|
/* Layout */
|
|
header.top {
|
|
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
|
background: var(--panel); border-bottom: 1px solid var(--border);
|
|
padding: 8px 16px; position: sticky; top: 0; z-index: 10;
|
|
}
|
|
header.top .brand { font-weight: 700; font-size: 17px; letter-spacing: 0.5px; color: var(--text); }
|
|
header.top .brand span { color: var(--muted); font-weight: 400; font-size: 12px; margin-left: 6px; }
|
|
nav.main { display: flex; gap: 2px; flex-wrap: wrap; }
|
|
nav.main a { padding: 6px 10px; border-radius: var(--radius); color: var(--text); }
|
|
nav.main a:hover { background: var(--info-soft); text-decoration: none; }
|
|
nav.main a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
header.top .spacer { flex: 1; }
|
|
header.top form.search input { width: 220px; }
|
|
header.top .user { color: var(--muted); }
|
|
main { padding: 16px; max-width: 1400px; margin: 0 auto; }
|
|
.pending-bar {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
background: var(--warn-soft); color: var(--text); border-bottom: 1px solid var(--border);
|
|
padding: 8px 16px;
|
|
}
|
|
.pending-bar strong { color: var(--warn); }
|
|
.page-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
.page-head .grow { flex: 1; min-width: 200px; }
|
|
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
|
|
/* Panels and cards */
|
|
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow); margin-bottom: 14px; }
|
|
.panel > h2:first-child, .panel > h3:first-child { margin-top: 0; }
|
|
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; margin-bottom: 14px; }
|
|
.card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; box-shadow: var(--shadow); }
|
|
.card .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; }
|
|
.card .value { font-size: 18px; font-weight: 600; margin-top: 2px; }
|
|
.card .sub { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
|
|
|
/* Tables */
|
|
.table-wrap { overflow-x: auto; }
|
|
table { border-collapse: collapse; width: 100%; background: var(--panel); }
|
|
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
th { font-size: 12px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; white-space: nowrap; }
|
|
tr:hover td { background: var(--info-soft); }
|
|
td.data { font-family: var(--mono); font-size: 13px; word-break: break-all; }
|
|
td.name { font-family: var(--mono); font-size: 13px; white-space: nowrap; }
|
|
td.name.repeat { color: var(--muted); }
|
|
td.actions-cell { white-space: nowrap; text-align: right; width: 1%; }
|
|
tr.editing td { background: var(--accent-soft); }
|
|
tr.pending-add td { background: var(--add); }
|
|
tr.pending-change td { background: var(--warn-soft); }
|
|
tr.pending-delete td { background: var(--del); text-decoration: line-through; color: var(--muted); }
|
|
.empty { color: var(--muted); padding: 18px; text-align: center; }
|
|
|
|
/* Forms */
|
|
input, select, textarea, button {
|
|
font: inherit; color: var(--text);
|
|
}
|
|
input[type=text], input[type=password], input[type=number], input[type=search], input:not([type]), select, textarea {
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 5px 8px; min-width: 0;
|
|
}
|
|
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
|
|
textarea { width: 100%; min-height: 80px; resize: vertical; }
|
|
.form-grid { display: grid; grid-template-columns: minmax(140px, 200px) 1fr; gap: 8px 14px; align-items: start; }
|
|
.form-grid > label { padding-top: 6px; color: var(--muted); }
|
|
.form-grid .help { grid-column: 2; margin-top: -4px; color: var(--muted); font-size: 12px; }
|
|
.form-grid input, .form-grid select { width: 100%; max-width: 560px; }
|
|
.inline-form { display: flex; gap: 6px; flex-wrap: wrap; align-items: flex-end; }
|
|
.inline-form .f { display: flex; flex-direction: column; gap: 2px; }
|
|
.inline-form .f label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.3px; }
|
|
.inline-form .grow { flex: 1; min-width: 220px; }
|
|
.inline-form .grow input { width: 100%; }
|
|
.hint { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
|
.check { display: inline-flex; gap: 6px; align-items: center; cursor: pointer; }
|
|
|
|
button, .button {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
padding: 5px 12px; cursor: pointer; white-space: nowrap; color: var(--text);
|
|
}
|
|
button:hover, .button:hover { background: var(--info-soft); text-decoration: none; }
|
|
button.primary, .button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); font-weight: 600; }
|
|
button.primary:hover { filter: brightness(1.08); background: var(--accent); }
|
|
button.danger { color: var(--err); border-color: var(--err); }
|
|
button.danger:hover { background: var(--err-soft); }
|
|
button.small, .button.small { padding: 2px 8px; font-size: 12px; }
|
|
button.link { border: none; background: none; color: var(--accent); padding: 0 4px; }
|
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
/* Badges and messages */
|
|
.badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; background: var(--info-soft); color: var(--muted); white-space: nowrap; }
|
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
.badge.err { background: var(--err-soft); color: var(--err); }
|
|
.badge.accent { background: var(--accent-soft); color: var(--accent); }
|
|
.msg { border-radius: var(--radius); padding: 8px 12px; margin: 8px 0; border: 1px solid transparent; }
|
|
.msg.err { background: var(--err-soft); color: var(--err); border-color: var(--err); }
|
|
.msg.warn { background: var(--warn-soft); color: var(--warn); }
|
|
.msg.ok { background: var(--ok-soft); color: var(--ok); }
|
|
.msg.info { background: var(--info-soft); }
|
|
.msg ul { margin: 4px 0 0; padding-left: 18px; }
|
|
.problems { list-style: none; padding: 0; margin: 6px 0; }
|
|
.problems li { padding: 4px 8px; border-left: 3px solid var(--border); margin-bottom: 3px; background: var(--info-soft); border-radius: 0 var(--radius) var(--radius) 0; }
|
|
.problems li.error { border-color: var(--err); background: var(--err-soft); }
|
|
.problems li.warning { border-color: var(--warn); background: var(--warn-soft); }
|
|
.problems li .where { color: var(--muted); font-size: 12px; margin-left: 6px; }
|
|
|
|
/* Diff */
|
|
.diff { background: var(--code-bg); border-radius: var(--radius); overflow: auto; font-family: var(--mono); font-size: 12.5px; line-height: 1.4; }
|
|
.diff div { white-space: pre; padding: 0 10px; }
|
|
.diff .add { background: var(--add); color: var(--add-text); }
|
|
.diff .del { background: var(--del); color: var(--del-text); }
|
|
.diff .hunk { background: var(--hunk); color: var(--accent); }
|
|
.diff .file { font-weight: 700; padding-top: 6px; }
|
|
|
|
/* Tabs */
|
|
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 12px; flex-wrap: wrap; }
|
|
.tabs a { padding: 7px 12px; border: 1px solid transparent; border-bottom: none; border-radius: var(--radius) var(--radius) 0 0; color: var(--muted); }
|
|
.tabs a.active { background: var(--panel); border-color: var(--border); color: var(--text); font-weight: 600; margin-bottom: -1px; }
|
|
|
|
/* Modal */
|
|
.modal-back { position: fixed; inset: 0; background: rgba(10, 15, 25, 0.45); display: flex; align-items: flex-start; justify-content: center; padding: 60px 16px; z-index: 50; overflow: auto; }
|
|
.modal { background: var(--panel); border-radius: 8px; border: 1px solid var(--border); width: min(760px, 100%); padding: 18px 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.25); }
|
|
.modal.wide { width: min(1100px, 100%); }
|
|
.modal h2 { margin-top: 0; }
|
|
.modal .buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
|
|
/* Toasts */
|
|
#toasts { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 100; max-width: min(480px, calc(100% - 32px)); }
|
|
.toast { background: var(--panel); border: 1px solid var(--border); border-left: 4px solid var(--accent); padding: 10px 14px; border-radius: var(--radius); box-shadow: 0 4px 16px rgba(0,0,0,0.18); }
|
|
.toast.ok { border-left-color: var(--ok); }
|
|
.toast.err { border-left-color: var(--err); }
|
|
.toast.warn { border-left-color: var(--warn); }
|
|
|
|
/* Login */
|
|
.login { max-width: 360px; margin: 10vh auto; }
|
|
.login h1 { font-size: 26px; text-align: center; margin-bottom: 4px; }
|
|
.login .tag { text-align: center; color: var(--muted); margin-bottom: 18px; }
|
|
.login form { display: flex; flex-direction: column; gap: 10px; }
|
|
.login input { width: 100%; padding: 8px 10px; }
|
|
.login button { justify-content: center; padding: 8px; }
|
|
|
|
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; }
|
|
.kv dt { color: var(--muted); }
|
|
.kv dd { margin: 0; }
|
|
.grants { font-family: var(--mono); font-size: 12px; }
|
|
.copy-box { display: flex; gap: 6px; align-items: center; }
|
|
.copy-box input { flex: 1; font-family: var(--mono); }
|
|
.sortable-row { display: flex; align-items: center; gap: 8px; }
|
|
@media (max-width: 700px) {
|
|
.form-grid { grid-template-columns: 1fr; }
|
|
.form-grid .help { grid-column: 1; }
|
|
header.top form.search input { width: 140px; }
|
|
}
|
|
|
|
/* ---- Statistics charts. Colors from the validated reference palette
|
|
(categorical slots 1-3, sequential blue), checked against this UI's
|
|
panel surfaces in both themes. */
|
|
:root {
|
|
--series-1: #2a78d6;
|
|
--series-2: #eb6834;
|
|
--series-3: #1baf7a;
|
|
--chart-grid: #eceef2;
|
|
--chart-axis: #c9cfd8;
|
|
/* Sequential ramp, low to high (blue 100 -> 700). */
|
|
--heat-0: #cde2fb; --heat-1: #9ec5f4; --heat-2: #6da7ec; --heat-3: #3987e5;
|
|
--heat-4: #256abf; --heat-5: #184f95; --heat-6: #0d366b;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--series-1: #3987e5;
|
|
--series-2: #d95926;
|
|
--series-3: #199e70;
|
|
--chart-grid: #272c34;
|
|
--chart-axis: #3a414d;
|
|
/* In dark, low values recede into the surface: the ramp runs dark -> light. */
|
|
--heat-0: #0d366b; --heat-1: #184f95; --heat-2: #256abf; --heat-3: #3987e5;
|
|
--heat-4: #6da7ec; --heat-5: #9ec5f4; --heat-6: #cde2fb;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--series-1: #3987e5;
|
|
--series-2: #d95926;
|
|
--series-3: #199e70;
|
|
--chart-grid: #272c34;
|
|
--chart-axis: #3a414d;
|
|
--heat-0: #0d366b; --heat-1: #184f95; --heat-2: #256abf; --heat-3: #3987e5;
|
|
--heat-4: #6da7ec; --heat-5: #9ec5f4; --heat-6: #cde2fb;
|
|
}
|
|
|
|
.refreshing { opacity: 0.6; transition: opacity 0.2s; }
|
|
.filter-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--panel); }
|
|
.segmented a { padding: 4px 12px; color: var(--text); border-right: 1px solid var(--border); text-decoration: none; font-variant-numeric: tabular-nums; }
|
|
.segmented a:last-child { border-right: none; }
|
|
.segmented a:hover { background: var(--info-soft); }
|
|
.segmented a.active { background: var(--accent); color: var(--accent-text); font-weight: 600; }
|
|
.card.stat .value.big { font-size: 26px; font-weight: 600; line-height: 1.2; }
|
|
|
|
.chart-card { margin: 0 0 14px; }
|
|
.chart-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
.chart-head .grow { flex: 1; min-width: 200px; }
|
|
.chart-head h2 { margin: 0; font-size: 16px; }
|
|
.chart-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 0 14px; }
|
|
.chart-grid > .panel h2 { margin: 0; font-size: 16px; }
|
|
.list-sub { margin: 2px 0 8px; }
|
|
|
|
.chart { position: relative; outline: none; }
|
|
.chart:focus-visible { box-shadow: 0 0 0 2px var(--accent); border-radius: var(--radius); }
|
|
.chart svg { display: block; overflow: visible; }
|
|
.chart .grid { stroke: var(--chart-grid); stroke-width: 1; }
|
|
.chart .axis { stroke: var(--chart-axis); stroke-width: 1; }
|
|
.chart .tick, .chart .end-label { fill: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
.chart .end-label { fill: var(--text); }
|
|
.chart .line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
.chart .area { stroke: none; fill-opacity: 0.1; }
|
|
.chart .dot { stroke: var(--panel); stroke-width: 2; }
|
|
.chart .crosshair { stroke: var(--muted); stroke-width: 1; }
|
|
.chart .hit { fill: transparent; cursor: crosshair; }
|
|
.chart .s1, .key.s1 { stroke: var(--series-1); }
|
|
.chart .s2, .key.s2 { stroke: var(--series-2); }
|
|
.chart .s3, .key.s3 { stroke: var(--series-3); }
|
|
.chart .area.s1, .chart .dot.s1 { fill: var(--series-1); }
|
|
.chart .area.s2, .chart .dot.s2 { fill: var(--series-2); }
|
|
.chart .area.s3, .chart .dot.s3 { fill: var(--series-3); }
|
|
.key.line { display: inline-block; width: 14px; height: 0; border-top: 2px solid; vertical-align: middle; margin-right: 6px; }
|
|
.key.line.s1 { border-color: var(--series-1); }
|
|
.key.line.s2 { border-color: var(--series-2); }
|
|
.key.line.s3 { border-color: var(--series-3); }
|
|
.chart-legend { list-style: none; display: flex; gap: 14px; margin: 2px 0 0; padding: 0; font-size: 13px; color: var(--text); flex-wrap: wrap; }
|
|
|
|
.chart-tip { position: absolute; z-index: 5; pointer-events: none; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16); padding: 6px 10px; font-size: 13px; white-space: nowrap; }
|
|
.chart-tip .tip-head { color: var(--muted); font-size: 12px; margin-bottom: 2px; }
|
|
.chart-tip .tip-row strong { font-variant-numeric: tabular-nums; }
|
|
.chart-table .scroll, .chart-table .table-wrap { max-height: 360px; overflow: auto; }
|
|
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
|
|
.heatmap-wrap { position: relative; }
|
|
.heatmap { display: grid; grid-template-columns: 36px repeat(24, minmax(10px, 1fr)); gap: 2px; outline: none; }
|
|
.heatmap:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
|
|
.hm-corner, .hm-col, .hm-row { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
.hm-row { display: flex; align-items: center; }
|
|
.hm-cell { height: 22px; border-radius: 3px; }
|
|
.hm-cell:hover, .hm-cell:focus { outline: 2px solid var(--text); outline-offset: -1px; }
|
|
.hm-cell.empty, .hm-swatch.empty { background: transparent; box-shadow: inset 0 0 0 1px var(--chart-grid); }
|
|
.heat-0 { background: var(--heat-0); } .heat-1 { background: var(--heat-1); } .heat-2 { background: var(--heat-2); }
|
|
.heat-3 { background: var(--heat-3); } .heat-4 { background: var(--heat-4); } .heat-5 { background: var(--heat-5); }
|
|
.heat-6 { background: var(--heat-6); }
|
|
.hm-scale { display: flex; align-items: center; gap: 2px; margin-top: 10px; flex-wrap: wrap; }
|
|
.hm-scale .muted { margin: 0 6px; }
|
|
.hm-swatch { display: inline-block; width: 18px; height: 10px; border-radius: 2px; }
|
|
|
|
.bar-list { list-style: none; margin: 0; padding: 0; }
|
|
.bar-list li { padding: 4px 0; }
|
|
.bar-text { display: flex; gap: 8px; justify-content: space-between; font-size: 13px; }
|
|
.bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 12.5px; min-width: 0; }
|
|
.bar-value { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
.bar-track { height: 6px; margin-top: 3px; }
|
|
.bar-fill { height: 6px; background: var(--series-1); border-radius: 0 3px 3px 0; min-width: 2px; }
|
|
@media (max-width: 700px) {
|
|
.heatmap { grid-template-columns: 30px repeat(24, minmax(6px, 1fr)); gap: 1px; }
|
|
.hm-cell { height: 16px; }
|
|
.chart-grid { grid-template-columns: 1fr; }
|
|
}
|