/* LoRaHAM Pi Control — local operator console design system.
   Served same-origin so it complies with `Content-Security-Policy: default-src 'self'`.
   No inline styles, no JavaScript, no external/CDN assets. */

:root {
  --bg: #f4f6f9;
  --surface: #ffffff;
  --ink: #1b2733;
  --muted: #5b6677;
  --line: #e2e8f0;
  --brand: #0b5cab;
  --brand-hover: #0a4f93;
  --brand-ink: #ffffff;
  /* Primary-button fill — separate from --brand so links/outlines stay bright on dark while filled
     buttons use a muted deep blue in dark mode. */
  --btn-bg: #0b5cab;  --btn-ink: #ffffff;  --btn-hover: #0a4f93;
  --header-ink: #ffffff;   /* nav/banner text — white in BOTH themes (banner is dark-blue in both) */
  --ok: #12722f;   /* darker green so the shared token clears WCAG AA (>=4.5:1) as small badge/flag text
                      on the light-green --badge-ok-bg/--flash-ok-bg; dark mode keeps its bright #3fb950 */
  --warn: #8f5f00;   /* ≥4.5:1 on --badge-warn-bg AND white (Lighthouse a11y) */
  --bad: #c1121f;
  --idle: #57606a;
  --shadow: 0 1px 3px rgba(16, 24, 40, .08), 0 1px 2px rgba(16, 24, 40, .06);
  --radius: 10px;
  color-scheme: light;   /* native controls (checkboxes, radios, scrollbars, select popups) follow theme */

  /* Derived surface tokens (light). A single dark block (:root[data-theme="dark"], plus the no-JS
     prefers-color-scheme fallback) re-points ALL of these — see the end of this file. */
  --header-grad-a: #0b5cab;
  --header-grad-b: #0a4f93;
  --warnbox-bg: #fff8e6;  --warnbox-border: #f5d98a;  --warnbox-ink: #6b4e00;
  --badge-ok-bg: #e7f6ec;   --badge-ok-border: #b7e3c4;
  --badge-warn-bg: #fbf1d8;  --badge-warn-border: #f0d79a;
  --badge-bad-bg: #fde7e9;   --badge-bad-border: #f3b9bf;
  --badge-idle-bg: #eef1f4;  --badge-idle-border: #dbe1e8;  --badge-idle-ink: #57606a;
  --pill-bg: #eef2f7;  --pill-ink: #334455;
  --conflict-bg: #fdf0f0;  --conflict-border: #f3b9bf;  --conflict-ink: #7a1620;
  /* Banner tints: deliberately NOT ultra-pale (clearly colored) while each -ink keeps >=4.5:1 contrast. */
  --flash-ok-bg: #d6efdd;  --flash-ok-border: #a8dcb8;  --flash-ok-ink: #12722f;
  --flash-warn-bg: #fdeec2; --flash-warn-border: #f3d081; --flash-warn-ink: #7a5200;
  --flash-bad-bg: #fbdbd7;  --flash-bad-border: #eda69e;  --flash-bad-ink: #a01b10;
  --flash-info-bg: #eef2f7; --flash-info-border: #cdd7e3; --flash-info-ink: #33414f;
  --warnbtn-bg: #fff3cd;  --warnbtn-border: #e0b64f;  --warnbtn-ink: #7a5200;
  --code-bg: #0f1b2a;  --code-ink: #d7e3f2;
  --field-bad-bg: rgba(224, 82, 82, .12);
  /* System-box GRAPH stroke hues (bars stay status-colored). Validated as a SET in row order
     (OKLab ΔE ≥15 normal / ≥8 CVD on every adjacent pair, ≥3:1 vs surface) — change together,
     keep all three theme blocks in sync. */
  --sys-cpu: #0b5cab; --sys-mem: #0b6332; --sys-swap: #0e86ad; --sys-netrx: #c11a5b;
  --sys-nettx: #6d28d9; --sys-disk: #475569; --sys-temp: #b45309;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }   /* wins over every display rule below (phone tables included) */

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  overflow-x: hidden;                 /* a stray wide child can't drag the whole page sideways */
}

a { color: var(--brand); text-decoration: underline; }   /* inline links must not rely on color alone */
/* Standalone chrome links (header/nav buttons, card titles) are distinguishable by placement
   and styling — no underline needed there. */
.site-header a, .theme-toggle { text-decoration: none; }
a:hover { text-decoration: underline; }
code, pre, .mono { font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; }
/* Wide content stays reachable: pre scrolls inside its box; long unbroken commands wrap. */
pre { max-width: 100%; overflow-x: auto; }
code { overflow-wrap: anywhere; }
/* Content sub-section heading fallback (many were <h4>, promoted to <h3> for sequential heading order):
   compact, like the old <h4>. More specific rules (.card>header h3 / .panel h3 / .cfgsec h3) still win. */
h3 { font-size: 1rem; margin: .6rem 0 .35rem; }
/* Screen-reader-only structural heading — present in the a11y outline, invisible on screen. */
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
                   overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* Header / footer ------------------------------------------------------- */
.site-header {
  background: linear-gradient(180deg, var(--header-grad-a), var(--header-grad-b));
  color: var(--header-ink);
  padding: 1rem 1.25rem;
  box-shadow: var(--shadow);
}
.site-header a { color: var(--header-ink); }
.site-header h1 { margin: 0; font-size: 1.2rem; letter-spacing: .2px; }
.site-header .sub { opacity: .85; font-size: .85rem; margin-top: .2rem; }
/* Header line is the navigation: the title is the Dash/home button (left), "Apps" is a same-style
   button on the right. Both look clickable. Wraps to keep both tappable on small screens. */
.header-top { display: flex; align-items: center; justify-content: space-between;
              gap: .4rem 1rem; flex-wrap: wrap; }
.home, .apps-link { display: inline-block; text-decoration: none; font-weight: 700;
                    padding: .2rem .7rem; border: 1px solid rgba(255, 255, 255, .4);
                    border-radius: 8px; background: rgba(255, 255, 255, .1); }
.home:hover, .apps-link:hover { background: rgba(255, 255, 255, .22); }
.apps-link { font-size: 1.2rem; white-space: nowrap; }
/* Centered brand between Home and Apps; flex:1 centers it in the leftover space without ever
   overlapping the buttons. Narrow viewports switch to the short form. */
.brand-title { flex: 1; text-align: center; font-weight: 800; font-size: 1.25rem;
               letter-spacing: .2px; white-space: nowrap; overflow: hidden; }
.brand-short { display: none; }
@media (max-width: 480px) {
  .brand-long { display: none; }
  .brand-short { display: inline; }
}
.wrap { max-width: 1024px; margin: 0 auto; padding: 1.25rem; }
/* Single-radio mode (Dashboard only): narrow the whole page — header, nav, main, footer, boxes and
   banners — to half width, matching the single radio box (the radio grid becomes one column). */
body.dash-narrow .wrap { max-width: 512px; }
body.dash-narrow .radiogrid { grid-template-columns: 1fr; }
.nav { background: #08305c; box-shadow: var(--shadow); }
.nav .wrap { display: flex; flex-wrap: wrap; gap: .25rem .9rem; padding-top: .5rem; padding-bottom: .5rem; }
.nav a { color: #cfe0f3; font-size: .9rem; }
.nav a:hover { color: #fff; text-decoration: none; }
.deps { font-size: .9rem; color: var(--muted); }
.deps .badge { font-size: .7rem; }
.links { font-size: .9rem; margin-top: .6rem; }
.links .rfline { display: block; margin-top: .45rem; }   /* the RF-log line under the daemon-log link */
.jobrun { background: var(--warnbox-bg); border: 1px solid var(--warnbox-border); color: var(--warnbox-ink);
          padding: .4rem .7rem; border-radius: 6px; font-size: .9rem; }
.banner {
  background: var(--warnbox-bg); border: 1px solid var(--warnbox-border); color: var(--warnbox-ink);
  padding: .6rem .9rem; border-radius: var(--radius); margin: 0 0 1rem;
}
.site-footer { color: var(--muted); font-size: .8rem; text-align: center; padding: 1.5rem 3.2rem;
               position: relative; }   /* relative: anchors the theme toggle to the footer's right edge */
.site-footer .sub { margin-top: .4rem; }   /* no opacity fade: muted@.85 on --bg was <4.5:1 */
/* Footer version/head freshness indicator. */
.ver { font-weight: 600; }
.ver-green { color: var(--ok); }
.ver-yellow { color: var(--warn); }
.ver-red { color: var(--bad); }
.ver-grey { color: var(--muted); }
/* "Update" row-action: a GREEN pill with a comfortable touch target (>=24px tall/wide, per WCAG 2.5.8),
   reusing the theme-aware green tokens. */
.update-link { font-weight: 700; display: inline-block; padding: .35rem .7rem; border-radius: 6px;
  line-height: 1.2; text-decoration: none; background: var(--flash-ok-bg);
  border: 1px solid var(--flash-ok-border); color: var(--flash-ok-ink); }
.update-link:hover { text-decoration: none; border-color: var(--flash-ok-ink); }

/* Overview tiles -------------------------------------------------------- */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: .75rem; margin-bottom: 1.25rem; }
.tile { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: .8rem .9rem; box-shadow: var(--shadow); }
.tile .n { font-size: 1.6rem; font-weight: 700; }
.tile .l { color: var(--muted); font-size: .8rem; text-transform: uppercase; letter-spacing: .4px; }

/* Cards ----------------------------------------------------------------- */
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: 1.1rem; overflow: hidden; }
.card > header { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; padding: .8rem 1rem; border-bottom: 1px solid var(--line); }
.card > header h2, .card > header h3 { margin: 0; font-size: 1.05rem; }
.card .body { padding: .8rem 1rem; }
.summary { color: var(--muted); font-size: .9rem; margin: .2rem 1rem .8rem; }

/* Tables ---------------------------------------------------------------- */
table { border-collapse: collapse; width: 100%; }
th, td { text-align: left; padding: .45rem .6rem; border-bottom: 1px solid var(--line); font-size: .92rem; vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: .78rem; text-transform: uppercase; letter-spacing: .4px; }
tr:last-child td { border-bottom: none; }

/* Status badges (colour + text label, never colour alone) --------------- */
.badge { display: inline-block; padding: .1rem .5rem; border-radius: 999px; font-size: .78rem; font-weight: 600; border: 1px solid transparent; white-space: nowrap; }
.badge::before { content: "● "; }
.badge-running { background: var(--badge-ok-bg); color: var(--ok); border-color: var(--badge-ok-border); }
.badge-degraded { background: var(--badge-warn-bg); color: var(--warn); border-color: var(--badge-warn-border); }
.badge-booting { background: var(--badge-warn-bg); color: var(--warn); border-color: var(--badge-warn-border); }   /* transient: firmware booting / callsign applying */
.badge-starting { background: var(--badge-warn-bg); color: var(--warn); border-color: var(--badge-warn-border); margin-left: .35rem; }   /* a detached web start/restart is running (taskbanner.js) */
.badge-failed { background: var(--badge-bad-bg); color: var(--bad); border-color: var(--badge-bad-border); }
.badge-stopped, .badge-not-installed, .badge-unknown { background: var(--badge-idle-bg); color: var(--badge-idle-ink); border-color: var(--badge-idle-border); }
.badge-not-applicable { background: var(--badge-idle-bg); color: var(--badge-idle-ink); border-color: var(--badge-idle-border); }
.badge-not-applicable::before { content: "– "; }

.pill { display: inline-block; padding: .05rem .45rem; border-radius: 6px; background: var(--pill-bg); color: var(--pill-ink); font-size: .78rem; border: 1px solid var(--line); }
/* Semantic pill colours (webserver security + running posture), theme-aware via the badge tokens. */
.pill-ok { background: var(--badge-ok-bg); border-color: var(--badge-ok-border); color: var(--ok); }
.pill-bad { background: var(--badge-bad-bg); border-color: var(--badge-bad-border); color: var(--bad); }
.pill-warn { background: var(--badge-warn-bg); border-color: var(--badge-warn-border); color: var(--warn); }
.pill-off { background: var(--badge-idle-bg); border-color: var(--badge-idle-border); color: var(--badge-idle-ink); }
/* Dashboard Webserver box: one flex line per entry — name + pills, logs link pushed to the right edge. */
.wsline { display: flex; align-items: center; flex-wrap: wrap; gap: .4rem; padding: .35rem 0; border-bottom: 1px solid var(--line); }
.wsline:last-child { border-bottom: none; }
.wsname { font-weight: 600; min-width: 5.5rem; }
.wsline .logslink { margin-left: auto; }   /* pin the per-service logs link flush right */
.wsurl { text-decoration: underline; }   /* the URL pill IS the link; keep its severity colour */
.tx-enabled { color: var(--bad); font-weight: 600; }
.tx-disabled { color: var(--idle); }

/* Panels inside stack detail ------------------------------------------- */
.panel { border: 1px solid var(--line); border-radius: 8px; padding: .6rem .8rem; margin: .6rem 0; }
.panel h4, .panel h3 { margin: 0 0 .35rem; font-size: .82rem; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); }
.kv { margin: 0; }
.kv li { list-style: none; }
ul.clean { margin: .2rem 0; padding-left: 1.1rem; }
.conflict { background: var(--conflict-bg); border: 1px solid var(--conflict-border); color: var(--conflict-ink); padding: .5rem .8rem; border-radius: 8px; }
.muted { color: var(--muted); }
details summary { cursor: pointer; color: var(--brand); font-size: .88rem; }
details[open] summary { margin-bottom: .4rem; }
/* The daemon monitor: a heading-style toggle, RSSI row first, then all values. */
details.monitor > summary { font-size: 1rem; font-weight: 600; color: var(--ink);
                            margin: .6rem 0 .3rem; }
.monrssi th { white-space: nowrap; }
.monrssi td { display: flex; align-items: center; gap: .5rem; }
pre.next { background: var(--code-bg); color: var(--code-ink); border: 1px solid var(--line); padding: .6rem .8rem; border-radius: 8px; overflow-x: auto; font-size: .85rem; }
.endpoint-ok { color: var(--ok); }
.endpoint-bad { color: var(--idle); }

/* Flash messages + daemon monitor ------------------------------------- */
.flash { padding: .5rem .8rem; border-radius: 6px; margin: 0 0 .8rem; transition: opacity .6s ease; }
.flash-ok { background: var(--flash-ok-bg); border: 1px solid var(--flash-ok-border); color: var(--flash-ok-ink); }
.flash-warn { background: var(--flash-warn-bg); border: 1px solid var(--flash-warn-border); color: var(--flash-warn-ink); }
.flash-bad { background: var(--flash-bad-bg); border: 1px solid var(--flash-bad-border); color: var(--flash-bad-ink); }
.flash-info { background: var(--flash-info-bg); border: 1px solid var(--flash-info-border); color: var(--flash-info-ink); }   /* neutral (details) */
/* Persistent (GET-rendered) callout notes — same look as a flash, but a distinct channel from the
   action-flash `.flash*` rules above (which are reserved for one-off action outcomes). Reuses the
   same --flash-* tokens, so it stays theme-correct. Persistent notes never fade (no `-hide`). */
.depnote { padding: .5rem .8rem; border-radius: 6px; margin: 0 0 .8rem; }
.depnote-pre { white-space: pre-wrap; overflow-wrap: anywhere; }
.depnote-ok { background: var(--flash-ok-bg); border: 1px solid var(--flash-ok-border); color: var(--flash-ok-ink); }
.depnote-warn { background: var(--flash-warn-bg); border: 1px solid var(--flash-warn-border); color: var(--flash-warn-ink); }
.depnote-bad { background: var(--flash-bad-bg); border: 1px solid var(--flash-bad-border); color: var(--flash-bad-ink); }
.depnote-info { background: var(--flash-info-bg); border: 1px solid var(--flash-info-border); color: var(--flash-info-ink); }
meter { width: 12rem; height: 1rem; vertical-align: middle; margin-right: .5rem; }
form label { display: inline-block; margin: .2rem .8rem .2rem 0; }
/* Text-like inputs only (NOT checkboxes/radios — those theme natively via color-scheme + accent-color). */
form select, form input[type=text], form input[type=number], form input[type=password],
form input[type=search] { font: inherit; padding: .2rem .3rem;
  background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-radius: 4px; }
option { background: var(--surface); color: var(--ink); }   /* native dropdown list in dark mode */
input[type=checkbox], input[type=radio] { accent-color: var(--brand); }
/* Base button = THE one blue-button look: border-only (outline), fills on hover. Every button (incl. bare
   button-in-link like "Dependency Overview", form submits, the webserver Apply) uses this; the yellow
   (.warnbtn / pending) and neutral utility buttons (.copybtn/.dismiss/.liveclose/.theme-toggle) set their
   own colours and out-specify it. `--btn-*` are the hover fill. */
button { font: inherit; padding: .3rem .8rem; border-radius: 6px; border: 1px solid var(--brand);
         background: var(--surface); color: var(--brand); cursor: pointer; }
button:hover { background: var(--btn-bg); color: var(--btn-ink); border-color: var(--btn-bg); }
.actbar { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin: .3rem 0; }
.actrow { display: flex; flex-wrap: wrap; gap: 1.3rem; align-items: center; margin: .4rem 0; }
.actgroup { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center;
            border: 1px solid var(--line); border-radius: 8px; padding: .25rem .5rem; }
.grouplabel { font-size: .68rem; text-transform: uppercase; letter-spacing: .5px;
              color: var(--muted); margin-right: .2rem; }
.radiogrid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; }
@media (max-width: 760px) { .radiogrid { grid-template-columns: 1fr; } }
.radio-col .feed { max-height: 9rem; overflow: auto; }
.runstack { border: 1px solid var(--line); border-radius: 8px; padding: .5rem .6rem; margin: .4rem 0; }
.complist { list-style: none; padding-left: 0; margin: .3rem 0; }
.complist li { margin: .15rem 0; }
/* A rule BETWEEN the component rows of one stack — `li + li` cannot draw one above the
   first row or below the last, and a single-component stack keeps no rule at all. */
.complist li + li { border-top: 1px solid var(--line); padding-top: .3rem; margin-top: .3rem; }
/* Enlarged, comfortably-spaced tap target for the row "logs" link (≈ WCAG 2.5.8 24×24). The padding
   grows the link inside its own grid cell, not the row layout. */
.logslink { font-size: .8rem; margin-left: .3rem; display: inline-block; padding: .25rem .45rem; }
/* A normally-spaced, blue button-style link (e.g. the HMAC pre-start "Cancel") — sits beside a real
   button in an action row without competing with a yellow warn button. */
.btnlink { display: inline-block; margin-left: .6rem; padding: .35rem .7rem; border-radius: 8px;
           border: 1px solid var(--brand); color: var(--brand); text-decoration: none; font-size: .9rem; }
.btnlink:hover { background: var(--bg); }
.iface { font-size: .8rem; margin-left: .4rem; padding: .05rem .4rem; border-radius: 6px;
         background: var(--bg); border: 1px solid var(--line); }
a.iface { text-decoration: none; color: var(--brand); }
/* On the dashboard running list, each endpoint sits on its own indented line. */
.complist .iface { display: block; width: fit-content; margin: .15rem 0 0 1.4rem; }
/* Web-UI links read as buttons — give them breathing room above and below. */
.complist .iface-web { margin-top: .5rem; margin-bottom: .5rem; padding: .2rem .55rem; }
.cmdbox { position: relative; }
.cmdbox .copybtn { position: absolute; top: .3rem; right: .3rem; font-size: .75rem;
                   padding: .1rem .5rem; cursor: pointer;
                   background: var(--surface); color: var(--ink); border: 1px solid var(--line);
                   border-radius: 6px; }
.cmdbox .copybtn:hover { background: var(--line); color: var(--ink); }   /* stay neutral, not blue */
.cmdbox pre { padding-right: 5rem; }
/* Stored passwords render as discs until `Show` removes the class. Unmasked, the value is
   ordinary selectable text. A browser without text-security support shows it as it did before. */
.secretbox pre.masked { -webkit-text-security: disc; text-security: disc; }
/* Both buttons ride in ONE flex row, so the gap holds when the copy button relabels itself
   to "copied" and back. */
.secretbox .secretbtns { position: absolute; top: .3rem; right: .3rem; display: flex; gap: .75rem; }
.secretbox .copybtn, .secretbox .revealbtn { position: static; font-size: .75rem;
                        padding: .1rem .5rem; cursor: pointer;
                        background: var(--surface); color: var(--ink); border: 1px solid var(--line);
                        border-radius: 6px; }
.secretbox .copybtn:hover, .secretbox .revealbtn:hover { background: var(--line); color: var(--ink); }
.secretbox pre { padding-right: 12rem; }
/* Phone: the buttons drop BELOW the value (they follow it in the DOM) instead of overlaying a
   narrow box, and grow to a comfortable tap target. */
@media (max-width: 480px) {
  .secretbox .secretbtns { position: static; margin-top: .5rem; gap: 1rem; flex-wrap: wrap; }
  .secretbox pre { padding-right: .6rem; }
  .secretbox .copybtn, .secretbox .revealbtn { font-size: .85rem; padding: .55rem .9rem;
                                               min-height: 2.75rem; }
}
.dismiss { float: right; }
.dismiss button { padding: 0 .4rem; line-height: 1.2;
                  background: var(--surface); color: var(--ink); border: 1px solid var(--line);
                  border-radius: 6px; }
.dismiss button:hover { background: var(--line); color: var(--ink); }   /* stay neutral, not blue */
.startform { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }
.actline { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; margin: .3rem 0; }
.actline .grouplabel { min-width: 3.5rem; }
.stacklist { padding: 0; }
.stackrow { border-bottom: 1px solid var(--line); }
.stackrow > summary { cursor: pointer; padding: .6rem .8rem; display: flex; gap: .5rem;
                      align-items: center; flex-wrap: wrap; list-style: none; }
.stackrow > summary::-webkit-details-marker { display: none; }
.stackrow > summary::before { content: "▸"; color: var(--muted); }
.stackrow[open] > summary::before { content: "▾"; }
.stackrow[open] > summary { background: var(--bg); }
.stackrow-body { padding: .3rem .8rem 1rem; }
.ss-name { font-weight: 600; }
/* The action links (logs + "Update") sit in a .row-actions OVERLAY OUTSIDE the <summary> (so no
   interactive element is nested in the summary toggle — a11y) but are positioned over the row's right
   edge, staying visible + clickable on the collapsed row. pointer-events:none on the container lets clicks
   in its empty area fall through to the summary (row still toggles); the links keep pointer-events. */
.stackrow-wrap { position: relative; }
.row-actions { position: absolute; top: 0; right: .8rem; display: flex; align-items: center; gap: .8rem;
               padding: .6rem 0; pointer-events: none; }
.row-actions > * { pointer-events: auto; }
.row-actions .update-link { font-size: .82rem; white-space: nowrap; }

/* TOP-LEVEL row headers (controller + each stack) lay out as fixed columns so the SAME field lines up
   vertically across rows: marker | name | status | source | version | head | extra. The summary reserves
   right padding for the .row-actions overlay so the flexible `col-extra` never slides under it. */
.stacklist .stackrow > summary {
  display: grid;
  grid-template-columns: 1.2em 11em 6.5em 8em 5.5em 7em 1fr;
  align-items: center;
  column-gap: .5rem;
  row-gap: .2rem;
  padding-right: 11.5em;   /* room for the logs + "Update" overlay */
}
.stacklist .stackrow > summary .col-name    { grid-column: 2; min-width: 0; overflow: hidden;
                                               text-overflow: ellipsis; white-space: nowrap; }
.stacklist .stackrow > summary .col-status  { grid-column: 3; }
.stacklist .stackrow > summary .col-source  { grid-column: 4; }
.stacklist .stackrow > summary .col-version { grid-column: 5; }
/* 7em, not 6em: the pill is `@` + 9 hex in mono at .78rem (~75-77px) + .9rem padding + 2px border
   ~= 91-94px, which overran a 96px track into the .5rem column-gap and touched `identity ok`. */
.stacklist .stackrow > summary .col-head    { grid-column: 6; min-width: 0; }
.stacklist .stackrow > summary .col-extra   { grid-column: 7; min-width: 0; display: flex;
                                               gap: .4rem; flex-wrap: wrap; align-items: center; }
/* Small screens (phones/tablets): the fixed column grid can't shrink and would be clipped by the
   card. Reflow the row header to a wrapping flex layout so it FITS the viewport — no horizontal
   cut-off, no whole-page sideways scroll. Wide config tables scroll inside their own box. */
@media (max-width: 860px) {
  .stacklist .stackrow > summary { display: flex; flex-wrap: wrap; column-gap: .5rem; row-gap: .25rem;
                                   padding-right: 10.5em; }
  .stacklist .stackrow > summary .col-name,
  .stacklist .stackrow > summary .col-status,
  .stacklist .stackrow > summary .col-source,
  .stacklist .stackrow > summary .col-version,
  .stacklist .stackrow > summary .col-head,
  .stacklist .stackrow > summary .col-extra { grid-column: auto; }
  .stacklist .stackrow > summary .col-name { overflow: visible; white-space: normal; text-overflow: clip; }
  /* Every wide block scrolls inside its own box so no section overflows the phone viewport. */
  table { display: block; overflow-x: auto; max-width: 100%; }
  .wrap { padding: .8rem; }
  .card .body, .card > header { padding-left: .7rem; padding-right: .7rem; }
  /* Config/param FORM tables reflow to stacked rows (name → full-width input → help) instead of a
     3-column grid that overran the viewport. */
  .paramgrid, .paramgrid tbody, .paramgrid tr, .paramgrid th, .paramgrid td,
  .dptab, .dptab tbody, .dptab tr, .dptab th, .dptab td { display: block; width: auto; }
  .paramgrid tr, .dptab tr { border-bottom: 1px solid var(--line); padding: .3rem 0; }
  .paramgrid th, .dptab th { padding: 0 0 .1rem; text-align: left; }
  .paramgrid td, .dptab td { padding: .1rem 0; }
  .paramgrid input:not([type=checkbox]), .paramgrid select,
  .dptab input:not([type=checkbox]), .dptab select { width: 100%; max-width: 24rem; }
  .setcell { flex-wrap: wrap; }          /* Live radio settings row wraps instead of overflowing */
  input.wide { min-width: 0; }           /* source-remote input (min-width:16rem) must shrink on phones */
  /* Daemon monitor (dashboard): the live stats table is label/value/label/value — four columns can't fit
     a phone, and a horizontally-scrolling table IS the overflow. Reflow each row into a 2-column grid so
     its two pairs stack vertically; every value stays visible with no sideways scroll, and the feed wraps. */
  .radio-col .monitor table, .radio-col .monitor table tbody { display: block; width: 100%; }
  .radio-col .monitor table tr { display: grid; grid-template-columns: max-content 1fr;
                                 column-gap: .6rem; align-items: center; }
  .radio-col .monitor table th { text-align: left; white-space: nowrap; }
  .radio-col .monitor table td { min-width: 0; overflow-wrap: anywhere; }
  .radio-col .feed { white-space: pre-wrap; overflow-wrap: anywhere; }
}
table.deps { width: 100%; }
input.wide { width: 100%; min-width: 16rem; }
/* Segmented band chooser: clear active state (brand fill + white ink). */
.bandswitch { display: inline-flex; gap: 0; border: 1px solid var(--brand);
              border-radius: 8px; overflow: hidden; vertical-align: middle; }
.bandswitch a { padding: .25rem .85rem; text-decoration: none; color: var(--brand);
                background: var(--surface); font-weight: 600;
                border-left: 1px solid var(--brand); }
/* A band this stack can't use: greyed, non-clickable segment (same size as the links). */
.bandswitch .disabled { padding: .25rem .85rem; font-weight: 600; color: var(--muted);
                        background: var(--bg); cursor: not-allowed; border-left: 1px solid var(--brand); }
.bandswitch > :first-child { border-left: 0; }   /* first segment (a OR disabled span) has no divider */
.bandswitch a:hover { background: var(--bg); }
.bandswitch a.sel { background: var(--brand); color: var(--brand-ink); }
.cfgsec { margin: .5rem 0 1rem; padding-top: .5rem; border-top: 1px solid var(--line); }
.cfgsec h3 { margin: .3rem 0 .5rem; font-size: 1rem; }
.advcfg { margin: .4rem 0; }
/* The lazy stack-body placeholder reserves a little height, so the fetched body expanding
   from nothing is a smaller shift (stacklazy.js pins the header across the injection). */
.lazy-body { min-height: 1.5rem; }
/* Row-open loading indicator: shown from the moment a stack row opens until its body lands. */
.lazy-body[data-loading] .loading { display: inline-flex; align-items: center; gap: .5rem;
                                    color: var(--muted); font-size: .85rem; padding: .3rem 0; }
.spinner { display: inline-block; width: .9rem; height: .9rem; border-radius: 50%;
           border: 2px solid var(--line); border-top-color: var(--brand);
           animation: lhpc-spin .8s linear infinite; }
@keyframes lhpc-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; border-top-color: var(--line); } }
/* Confirmation page: Cancel lower left, the action lower right. */
.confirm-btns { display: flex; justify-content: space-between; align-items: center; gap: .6rem; }
.confirm-btns .btnlink.cancel { margin-left: 0; }
/* Box every sub-panel on /stacks — including nested ones — for a clear box-in-box hierarchy. Each
   nested box is inset by its parent's padding + the .advcfg margin. */
.stacklist .advcfg { border: 1px solid var(--line); border-radius: 8px; padding: .35rem .7rem; }
/* Deep nesting on a narrow phone: each box adds its own padding, so by the third or fourth level
   the content column is almost gone. Keep the boxes (they show the level — that is the point) but
   tighten the inset. Desktop is unchanged; this only applies below the phone breakpoint. */
@media (max-width: 480px) {
  .stackrow-body { padding: .25rem .35rem .6rem; }
  .stacklist .advcfg { padding: .3rem .3rem; border-radius: 6px; margin: .3rem 0; }
  .stacklist .advcfg .advcfg { padding-left: .25rem; padding-right: .25rem; }
}
.advcfg summary { cursor: pointer; color: var(--muted); font-size: .8rem;
                  text-transform: uppercase; letter-spacing: .4px; }
.act { display: inline-block; margin: 0; }
.act button { background: var(--surface); color: var(--brand); border: 1px solid var(--brand); }
.act button:hover { background: var(--btn-bg); color: var(--btn-ink); }
.act a, a.act { text-decoration: none; }
.settings th { width: 12rem; }
/* Live daemon settings (daemon page): one param per cell, two columns when wide enough. */
.settingsgrid { display: grid; grid-template-columns: 1fr; gap: .35rem 1.6rem; margin: .3rem 0 .6rem; }
@media (min-width: 880px) { .settingsgrid { grid-template-columns: 1fr 1fr; } }
.setcell { display: flex; align-items: center; gap: .6rem; }
.setkey { min-width: 6rem; font-family: ui-monospace, monospace; font-size: .85rem; }
/* Fill the cell and push the Apply button to the right edge so buttons line up per column. */
.setcell .setform { flex: 1; }
.setcell .setform button { margin-left: auto; }
/* Live STATUS/STATS output box with an X to close */
.liveout-wrap { position: relative; margin: .3rem 0 .6rem; }
.liveout-body { margin: 0; }
/* Fixed 22-line-tall output window: never grows as lines arrive; overflow scrolls inside. */
.stream22 { line-height: 1.4; height: calc(1.4em * 22); overflow-y: scroll; white-space: pre; }
.liveclose { position: absolute; top: .3rem; right: .3rem; line-height: 1;
             padding: .1rem .4rem; cursor: pointer; border: 1px solid var(--line);
             border-radius: 6px; background: var(--surface); color: var(--muted); }
/* Keep utility buttons neutral on hover — the broad `button:hover` blue fill must not leak in. */
.liveclose:hover,
.theme-toggle:hover { background: var(--surface); color: var(--ink); border-color: var(--line); }
.liveclose:hover { color: var(--ink); }
.paramgrid { width: auto; margin: .3rem 0 .8rem; }
.paramgrid th { width: 11rem; font-weight: 600; text-transform: none; letter-spacing: 0; color: var(--ink); font-size: .92rem; }
.paramgrid td { padding: .3rem .8rem; }
.setform { display: inline-flex; gap: .4rem; align-items: center; margin: 0; }
.setform button { padding: .15rem .6rem; }
/* FREQ / SYNC etc. are short values — keep the live-setting inputs compact, not full-width. */
.setform input[type=text], .setform input[type=number] { width: 7rem; }

.cfgreset { margin-top: 1.4rem; }                    /* space Save-config off from Reset */
hr.sep { border: 0; border-top: 1px solid var(--line, #333); margin: 1.4rem 0 .6rem; }
/* Same rule, tighter: it sits INSIDE a radio card between two running STACKS, where
   `hr.sep`'s 1.4rem lead-in would read as a gap rather than a separator. It falls in the
   margin BETWEEN two bordered blocks, so it reads as the stronger of the card's two
   separators; the component rows inside a block are ruled off by `.complist li + li`. */
hr.runsep { border: 0; border-top: 1px solid var(--line, #333); margin: .7rem .15rem; }

/* Per-stack daemon radio parameters (config page) — one param per line */
.dptab { margin: .3rem 0 .6rem; }
.dptab th { width: 6.5rem; font-family: ui-monospace, monospace; font-size: .82rem;
            text-align: left; vertical-align: middle; }
.dptab td { padding: .16rem .6rem; vertical-align: middle; }
.dpin { width: 6.5rem; }
.dphelp { font-size: .78rem; }
tr.greyed th, tr.greyed .dpin { color: var(--muted); opacity: .75; }   /* visual only — still editable */
span.greyed { color: var(--muted); opacity: .8; }
.logbox { max-height: 28rem; overflow-y: auto; background: var(--code-bg); color: var(--code-ink);
          border: 1px solid var(--line); padding: .6rem .8rem; border-radius: 8px; font-size: .82rem;
          line-height: 1.35; white-space: pre-wrap; word-break: break-word; }

/* Required-to-start marker + the Settings row a refused start points at. */
.req { color: var(--bad, #e05252); font-weight: 700; white-space: nowrap; }
tr.field-bad th, tr.field-bad td { background: var(--field-bad-bg); }
tr.field-bad input, tr.field-bad select { outline: 2px solid var(--bad, #e05252); }

/* Utility classes (avoid inline styles under CSP default-src 'self') ----- */
.header-wrap { padding-top: 0; padding-bottom: 0; }
.fs-sm { font-size: .8rem; }
.loglabel { margin: .6rem 0 .2rem; }
/* Yellow operator-action buttons (Confirm known-working, Restart required) ----- */
.warnbtn { background: var(--warnbtn-bg); color: var(--warnbtn-ink); border: 1px solid var(--warnbtn-border); font-weight: 600; }
.warnbtn:hover { background: var(--warn); color: #fff; }

/* auto-install run view */
.logbox-tall { max-height: 44rem; }
.logbox-half { max-height: 22rem; }
.tasklist { width: 100%; border-collapse: collapse; margin-bottom: 0.8rem; }
.tasklist th, .tasklist td { text-align: left; padding: 0.25rem 0.6rem 0.25rem 0; }
.badge-ok { background: var(--badge-ok-bg); color: var(--ok); border-color: var(--badge-ok-border); }
.badge-failed { background: var(--badge-bad-bg); color: var(--bad); border-color: var(--badge-bad-border); }
.inline { display: inline; }
/* RF log page: the band row and the stack row of pills under the header, the shown ones marked. */
.rfbands, .rfswitch { display: flex; flex-wrap: wrap; gap: .3rem; padding: .35rem .8rem 0; }
.rfbands a[aria-current="page"], .rfswitch a[aria-current="page"] { background: var(--badge-ok-bg); border-color: var(--badge-ok-border); color: var(--ok); }
.rfforms { display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; align-items: center; margin-top: .6rem; }   /* one row per form group; the rows stack with their own spacing */
.rfdecrypt { padding: .35rem .8rem 0; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.rfcontrols { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .5rem; font-size: .85rem; }
.rfcontrols #rf-cols label { margin-right: .3rem; white-space: nowrap; }
.rfscroll { overflow-x: auto; max-height: 44rem; overflow-y: auto; }
#rfview th button { background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer; text-transform: inherit; letter-spacing: inherit; }
#rfview th[aria-sort="ascending"] button::after { content: " \25B2"; }
#rfview th[aria-sort="descending"] button::after { content: " \25BC"; }
#rfview td { font-variant-numeric: tabular-nums; white-space: nowrap; }
#rfview td.rf-payload { font-family: ui-monospace, monospace; font-size: .8rem; max-width: 18rem; overflow: hidden; text-overflow: ellipsis; }
#rfview tr.rf-open td.rf-payload { white-space: pre-wrap; word-break: break-all; max-width: none; overflow: visible; }
#rfview tr.rf-no-key td, #rfview tr.rf-undecryptable td, #rfview tr.rf-malformed td { color: var(--muted); }

/* ---- Webserver controller component (inline in the controller row) ---- */
.ws-comp form { margin: 0.7em 0; }
.ws-comp .cfgsec > .depnote { margin: 0.4em 0; }
.ws-comp button { margin: 0.25em 0.5em 0.25em 0; }
.ws-comp table.certs a { margin-left: 0.7em; }
.ws-comp table.certs td, .ws-comp table.certs th { padding: 0.3em 0.7em 0.3em 0; }
/* one setting per line: label, input, short description */
.ws-comp .setting { margin: 0.45em 0; }
.ws-comp .setting label { display: inline-block; min-width: 9em; font-weight: 600; }
.ws-comp .setting .muted { margin-left: 0.5em; }
/* a little breathing room after the System dependencies list, before "Verify now" */
.ws-comp .sysdeps { margin-bottom: 1em; }

/* ---- Running-task indicator banner (Dash + Stacks) ---- */
.taskbanner { display: flex; flex-direction: column; gap: .4rem; }
.taskbanner:empty { display: none; }
.taskitem { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap;
            padding: .55rem .8rem; border-radius: 8px; border: 1px solid var(--line);
            margin-bottom: .3rem; font-size: .92rem; }
.taskitem .ti-label { font-weight: 600; }
.taskitem .ti-state { font-variant: small-caps; }
.taskitem .ti-hint  { color: var(--muted); font-size: .85em; }
/* Hides the hint when a task has none. A CLASS, not style="display:none" — the CSP sets no
   'unsafe-inline', so style-src-attr blocks inline style attributes. taskbanner.js toggles
   this same class when it refreshes a row. */
.taskitem .ti-hint.is-empty { display: none; }
.taskitem .ti-view  { margin-left: auto; white-space: nowrap; }
.taskitem .ti-recover { white-space: nowrap; }
/* View is always rendered, so the ✕/Recover control sits just right of View (both pinned right). */
.taskitem .ti-view + .ti-recover, .taskitem .ti-view + .ti-close { margin-left: .2rem; }
.taskitem .ti-close {
  background: transparent; border: 0; color: inherit; cursor: pointer; font-size: 1rem;
  line-height: 1; padding: 0 .2rem; opacity: .7;
}
.taskitem .ti-close:hover { opacity: 1; }
/* Colours share the theme-aware --flash-* tokens (identical palette to .depnote/.flash), so the banner
   matches every other status banner AND follows the in-app data-theme toggle (not just OS preference). */
.taskitem-running, .taskitem-warn { background: var(--flash-warn-bg); border-color: var(--flash-warn-border); color: var(--flash-warn-ink); }
.taskitem-done,    .taskitem-ok   { background: var(--flash-ok-bg);   border-color: var(--flash-ok-border);   color: var(--flash-ok-ink); }
.taskitem-failed, .taskitem-unsafe, .taskitem-bad { background: var(--flash-bad-bg); border-color: var(--flash-bad-border); color: var(--flash-bad-ink); }
/* Each sub-section (Settings/Monitor/Certificates) anchors a "logs" overlay on its header, laid out
   EXACTLY like the main stack rows: the .row-actions overlay is a sibling OUTSIDE the <summary> (a11y),
   positioned over the header's right edge and visible whether the section is open or collapsed. */
.ws-comp .ws-sub-wrap { position: relative; }

/* ============================ DARK MODE ==================================
   The whole UI is driven by the :root custom properties above, so a dark theme is just a re-point of
   those tokens. `data-theme` is set on <html> by theme.js BEFORE first paint (from localStorage, else
   the OS preference) — that is authoritative. The @media block is a no-JS fallback only (it applies the
   same palette when JS never runs and the OS asks for dark). Keep the two token lists identical. */
:root[data-theme="dark"] {
  --bg: #0e1116; --surface: #171b21; --ink: #e6edf3; --muted: #9aa7b4; --line: #2a313a;
  color-scheme: dark;
  --brand: #58a6ff; --brand-hover: #79b8ff; --brand-ink: #0b1220;
  --btn-bg: #1f6feb; --btn-ink: #ffffff; --btn-hover: #388bfd;   /* muted deep-blue filled buttons */
  --ok: #3fb950; --warn: #d29922; --bad: #f85149; --idle: #8b97a5;   /* idle-as-TEXT (.tx-disabled) needs ≥4.5:1 on --surface */
  --shadow: 0 1px 3px rgba(0, 0, 0, .5), 0 1px 2px rgba(0, 0, 0, .4);
  --header-grad-a: #234e7d; --header-grad-b: #1a3c61;
  --warnbox-bg: #2b2411; --warnbox-border: #4a3f1a; --warnbox-ink: #e3c766;
  --badge-ok-bg: #10261a; --badge-ok-border: #1f5133;
  --badge-warn-bg: #2b2411; --badge-warn-border: #4a3f1a;
  --badge-bad-bg: #2d1618; --badge-bad-border: #5c2a2e;
  --badge-idle-bg: #1c222b; --badge-idle-border: #333c48; --badge-idle-ink: #adb7c2;   /* ≥4.5:1 */
  --pill-bg: #1c222b; --pill-ink: #c9d3de;
  --conflict-bg: #2d1618; --conflict-border: #5c2a2e; --conflict-ink: #f0b7b2;
  --flash-ok-bg: #0f2417; --flash-ok-border: #1f5133; --flash-ok-ink: #56d364;
  --flash-warn-bg: #2b2411; --flash-warn-border: #4a3f1a; --flash-warn-ink: #e3b341;
  --flash-bad-bg: #2d1618; --flash-bad-border: #5c2a2e; --flash-bad-ink: #ff7b72;
  --flash-info-bg: #161b22; --flash-info-border: #2a313a; --flash-info-ink: #c9d3de;
  --warnbtn-bg: #c9971f; --warnbtn-border: #e3b341; --warnbtn-ink: #1a1400;   /* clearly YELLOW in dark */
  --code-bg: #05080d; --code-ink: #c9d3de;
  --field-bad-bg: rgba(248, 81, 73, .15);
  --sys-cpu: #58a6ff; --sys-mem: #2ea86b; --sys-swap: #3ec8dd; --sys-netrx: #f4649b;
  --sys-nettx: #a78bfa; --sys-disk: #a3b0bc; --sys-temp: #e8963e;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --bg: #0e1116; --surface: #171b21; --ink: #e6edf3; --muted: #9aa7b4; --line: #2a313a;
    color-scheme: dark;
    --brand: #58a6ff; --brand-hover: #79b8ff; --brand-ink: #0b1220;
    --btn-bg: #1f6feb; --btn-ink: #ffffff; --btn-hover: #388bfd;
    --ok: #3fb950; --warn: #d29922; --bad: #f85149; --idle: #8b97a5;   /* idle-as-TEXT (.tx-disabled) needs ≥4.5:1 on --surface */
    --shadow: 0 1px 3px rgba(0, 0, 0, .5), 0 1px 2px rgba(0, 0, 0, .4);
    --header-grad-a: #234e7d; --header-grad-b: #1a3c61;
    --warnbox-bg: #2b2411; --warnbox-border: #4a3f1a; --warnbox-ink: #e3c766;
    --badge-ok-bg: #10261a; --badge-ok-border: #1f5133;
    --badge-warn-bg: #2b2411; --badge-warn-border: #4a3f1a;
    --badge-bad-bg: #2d1618; --badge-bad-border: #5c2a2e;
    --badge-idle-bg: #1c222b; --badge-idle-border: #333c48; --badge-idle-ink: #adb7c2;
    --pill-bg: #1c222b; --pill-ink: #c9d3de;
    --conflict-bg: #2d1618; --conflict-border: #5c2a2e; --conflict-ink: #f0b7b2;
    --flash-ok-bg: #0f2417; --flash-ok-border: #1f5133; --flash-ok-ink: #56d364;
    --flash-warn-bg: #2b2411; --flash-warn-border: #4a3f1a; --flash-warn-ink: #e3b341;
    --flash-bad-bg: #2d1618; --flash-bad-border: #5c2a2e; --flash-bad-ink: #ff7b72;
    --flash-info-bg: #161b22; --flash-info-border: #2a313a; --flash-info-ink: #c9d3de;
    --warnbtn-bg: #c9971f; --warnbtn-border: #e3b341; --warnbtn-ink: #1a1400;
    --code-bg: #05080d; --code-ink: #c9d3de;
    --field-bad-bg: rgba(248, 81, 73, .15);
    --sys-cpu: #58a6ff; --sys-mem: #2ea86b; --sys-swap: #3ec8dd; --sys-netrx: #f4649b;
    --sys-nettx: #a78bfa; --sys-disk: #a3b0bc; --sys-temp: #e8963e;
  }
}

/* Dark scrollbars (WebKit + Firefox). Applied for the JS-set theme and the no-JS media fallback. */
:root[data-theme="dark"] { scrollbar-color: #4a525d var(--bg); }
:root[data-theme="dark"] ::-webkit-scrollbar { width: 12px; height: 12px; }
:root[data-theme="dark"] ::-webkit-scrollbar-track { background: var(--bg); }
:root[data-theme="dark"] ::-webkit-scrollbar-thumb { background: #3a424d; border-radius: 6px; }
:root[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: #4a525d; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) { scrollbar-color: #4a525d var(--bg); }
  :root:not([data-theme]) ::-webkit-scrollbar { width: 12px; height: 12px; }
  :root:not([data-theme]) ::-webkit-scrollbar-track { background: var(--bg); }
  :root:not([data-theme]) ::-webkit-scrollbar-thumb { background: #3a424d; border-radius: 6px; }
  :root:not([data-theme]) ::-webkit-scrollbar-thumb:hover { background: #4a525d; }
}

/* Theme toggle, pinned to the footer's right edge. Light theme shows the MOON (click → dark);
   dark shows the SUN. */
.theme-toggle {
  position: absolute; right: .9rem; top: 50%; transform: translateY(-50%);
  width: 2.3rem; height: 2.3rem; padding: 0; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--muted); border: 1px solid var(--line);
  cursor: pointer; opacity: .7;
}
.theme-toggle:hover { opacity: 1; }
.theme-toggle svg { width: 1.2rem; height: 1.2rem; display: block; fill: none;
                    stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }
:root[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: none; }

/* --- System box (dashboard): collapsed-by-default live host metrics ---------------------------
   The bar fill width is set from JS via CSSOM (el.style.width) — a deliberate, commented
   exception to the class-preference convention: it is CSP-legal (no style= attribute in server
   HTML) and a 20-step class ladder would be visibly steppy. Colours are STATUS semantics only
   (ok/warn/bad tokens — green healthy, yellow warn, red crit), toggled via explicit row
   classes, never nth-child. */
.sysbox > details > summary { cursor: pointer; padding: .8rem 1rem; color: var(--ink);
                              display: flex; align-items: baseline; gap: .5rem; }
.sysbox > details > summary::-webkit-details-marker { display: none; }
.sysbox > details > summary::before { content: "▸"; color: var(--muted); }
.sysbox > details[open] > summary::before { content: "▾"; }
.sysbox > details > summary h2 { margin: 0; font-size: 1.05rem; display: inline; }
.sysbox > details[open] > summary { border-bottom: 1px solid var(--line); margin-bottom: 0; }
/* --- Webserver box: same collapsible chrome as the System box (collapsed by default). The
   summary carries ONE consolidated security pill so posture stays visible while collapsed. --- */
.wsbox > details > summary { cursor: pointer; padding: .8rem 1rem; color: var(--ink);
                             display: flex; align-items: baseline; gap: .5rem; }
.wsbox > details > summary::-webkit-details-marker { display: none; }
.wsbox > details > summary::before { content: "▸"; color: var(--muted); }
.wsbox > details[open] > summary::before { content: "▾"; }
.wsbox > details > summary h2 { margin: 0; font-size: 1.05rem; display: inline; }
.wsbox > details[open] > summary { border-bottom: 1px solid var(--line); margin-bottom: 0; }
.wsbox .fwline { display: flex; align-items: baseline; gap: .5rem; }
.wsbox .fwline .wsname { font-weight: 600; }
/* logs link pushed to the right edge of the firewall line. The HEADER logs link is
   no longer inside the summary (it is a .row-actions overlay — an interactive
   element nested in a summary toggle is not reliably keyboard/AT reachable), so
   the summary only reserves room for it. */
.wsbox .fwline .hdrlogs { margin-left: auto; }
.wsbox > details > summary { padding-right: 4rem; }
.systab { table-layout: fixed; width: 100%; border-collapse: collapse; }
.systab th { width: 4.6rem; text-align: left; font-weight: 600; font-size: .88rem;
             padding: .3rem .4rem .3rem 0; }
.systab td { padding: .3rem 0; }
/* Middle-align every cell: the gauge cell has no text baseline, so default baseline alignment
   floated the numbers off the bar line. */
.systab th, .systab td { vertical-align: middle; border-bottom: 0; }
/* ONE continuous separator per row: a tr border spans the full row width by construction —
   per-cell borders rendered as broken segments and collided with the Power pill. */
.systab tr { border-bottom: 1px solid var(--line); }
/* Blocks below the metrics table carry the SAME separator as the table rows. */
.sysblock { border-bottom: 1px solid var(--line); padding: .3rem 0; margin: 0; }
.sysblock p { margin: 0; }
.sysblock form { margin: .25rem 0; }
/* Net renders as a FIXED two-line value (JS joins with \n): constant row height — a one-line
   string wrapped and unwrapped as the rate text length changed, bouncing the box height. */
/* Net value: everything right-aligned; the rate lives in a FIXED-WIDTH block so the arrow
   directly before it neither drifts with the digit count nor strands at the far left. */
.sys-netline { display: flex; justify-content: flex-end; gap: .45rem; }
.sys-netline span:last-child { width: 5.6rem; text-align: right; }
.sys-gauge { min-width: 0; }   /* stays a real table-cell: display:flex on the td broke the
                                  row separator line (a flexed td no longer renders as a cell) */
.sys-gaugewrap { display: flex; align-items: center; gap: .5rem; min-width: 0; }
.sysbar { flex: 0 1 4rem; min-width: 1.5rem; height: .65rem; background: var(--pill-bg);
          border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.sys-cpubars { flex: 0 1 4rem; min-width: 1.5rem; display: grid;
               grid-template-columns: 1fr 1fr; gap: 2px; }   /* per-core mini bars, 2x2 on 4 cores */
.sys-cpubars .sysbar { flex: none; width: 100%; min-width: 0; height: .45rem; }
.sysbar-ghost { visibility: hidden; }   /* reserves the bar slot (Net row) so every
                                           graph column starts at the same x */
.sysbar-fill { display: block; height: 100%; width: 0; border-radius: 999px;
               transition: width .6s linear; }
.sysspark { flex: 1 1 3rem; min-width: 0; height: 1.4rem; }
.sysspark polyline { fill: none; stroke-width: 2; vector-effect: non-scaling-stroke;
                     stroke-linecap: round; stroke-linejoin: round; }
.sysrow-net .sys-gaugewrap + .sys-gaugewrap { margin-top: .25rem; }
/* Power row: extra clearance so the row's separator line runs BELOW the pill (full width),
   never through it. */
.sysrow-power th, .sysrow-power td { padding-top: .35rem; padding-bottom: .55rem; }   /* the
                                           separator line clears the pill, never touches it */
.sys-val { width: 11rem; text-align: right; font-size: .85rem;
           font-variant-numeric: tabular-nums; padding-right: 0; }
/* BARS carry status only (green = healthy, yellow = warn, red = crit); the GRAPHS may keep
   per-metric hues (operator call) — warn/crit still repaints them, so an unhealthy metric is
   unmistakable. The numeric value is always present, colour is never the only signal. */
.sysbar-fill { background: var(--ok); }
/* Time spans BOTH the gauge and value columns: the pill leaves most of the gauge cell empty,
   and the fixed 11rem value column wrapped the timestamp onto three lines. One flex line, and
   the text never breaks mid-timestamp. */
/* Two deliberate lines — local (with its zone) above UTC — each unbreakable, so a timestamp is
   never split mid-value. The row spans both columns because the fixed 11rem value column wrapped
   it onto three ragged lines while the pill left most of the gauge cell empty. */
.sysrow-time .sys-time-val { display: flex; flex-direction: column; min-width: 0; }
/* Time, Network and GNSS share this layout: one pill width, so their texts start in one column. */
.sysrow-time .sys-gaugewrap > .pill { min-width: 4.5rem; text-align: center; }
.sysrow-time .sys-time-line { font-size: .85rem; white-space: nowrap;
  font-variant-numeric: tabular-nums; }
.sysrow-time .sys-time-utc { color: var(--muted); }
/* The recovery command is REAL text in the row — selectable and copyable, and reachable on a
   touch device, which a title tooltip is not. */
/* Guidance is ordinary prose — NOT select-all, because a sentence is not a command. */
.sysrow-time .sys-time-hint { font-size: .78rem; margin-top: .15rem; color: var(--muted);
  overflow-wrap: anywhere; }
/* ...and the command is its own element, so select-all copies EXACTLY a runnable line. LHPC
   never executes it; the operator does, exactly like the dependency hints. */
.sysrow-time .sys-time-cmd { font-size: .78rem; font-family: ui-monospace, monospace;
  color: var(--muted); user-select: all; overflow-wrap: anywhere; }

/* Narrow phones: the pill plus a full timestamp cannot share one line, and `.card` clips its
   overflow — so the timestamps were cut off rather than wrapped. Let the pill sit on its own
   line and give the timestamp block the full width; the timestamps themselves stay unbreakable
   so a value is never split down the middle. */
@media (max-width: 480px) {
  .sysrow-time .sys-gaugewrap { flex-wrap: wrap; }
  .sysrow-time .sys-time-val { flex: 1 1 100%; }
}

.sysrow-cpu  polyline { stroke: var(--sys-cpu); }
.sysrow-mem  polyline { stroke: var(--sys-mem); }
.sysrow-swap polyline { stroke: var(--sys-swap); }
#sys-netrx-spark polyline { stroke: var(--sys-netrx); }
#sys-nettx-spark polyline { stroke: var(--sys-nettx); }
.sysrow-disk polyline { stroke: var(--sys-disk); }
.sysrow-temp polyline { stroke: var(--sys-temp); }
.sysrow.sys-warn .sysbar-fill { background: var(--warn); }
.sysrow.sys-warn polyline     { stroke: var(--warn); }
.sysrow.sys-crit .sysbar-fill { background: var(--bad); }
.sysrow.sys-crit polyline     { stroke: var(--bad); }
/* Per-BAR status (the per-core CPU grid colours each core on its own). */
.sysbar.sys-warn .sysbar-fill { background: var(--warn); }
.sysbar.sys-crit .sysbar-fill { background: var(--bad); }
.sys-info { font-size: .85rem; margin: .5rem 0 0; }
.sys-info span { white-space: nowrap; }   /* one flowing line that breaks ONLY between values —
                                             each value is an unbreakable span */
@media (max-width: 860px) {
  /* Exempt the metrics table from the generic `table { display:block; overflow-x:auto }` phone
     rule (cf. .radio-col .monitor table): the gauges SHRINK instead of scrolling — the page hides
     horizontal overflow globally, so a scrolling table would clip information. */
  .systab { display: table; overflow-x: visible; }
  .systab th { width: 3.6rem; }
  .sysbar { flex-basis: 3rem; }
  .sys-val { width: 8.5rem; }
}
.wsopen { margin: .4rem 0 .6rem; font-size: .9rem; }

