/* ============================================================================
   S3 LoRa Monitor — styles.css
   Drop-in replacement for wwwroot/styles.css, restyled to the S3 ID design system.

   WHAT THIS IS
   Every selector the existing app.js emits is kept, with the same meaning, so this
   file can replace the old one with no JS change. Where the design system asks for
   markup the app does not emit yet (KPI row, ink status bar, nav rail, status pills,
   icons), the selectors are defined here and the markup is described in
   docs/design/markup-notes.md — nothing in this file depends on that markup existing.

   TOKENS
   The S3 ID tokens are inlined below rather than @import-ed, and both webfonts are
   self-hosted under fonts/, so the page fetches nothing from any other host.
   Do not hand-edit a colour here: change the token.

   FLAGGED SUBSTITUTIONS (carried over from the design system's own readme)
   - Quicksand is the open-source OFL release, not the brand's licensed binaries.
   - IBM Plex Mono stands in for a mono face the brand never specified.
   - Icons are Lucide 0.544.0, vendored as lib/lucide.min.js.
   - The six chart line colours are re-stepped from the S3 ID palette and are NOT
     yet colour-blind validated — see --chart-1..6 below.
   ========================================================================== */

/* Self-hosted webfonts, copied unmodified from npm @fontsource-variable/quicksand 5.3.0
   and @fontsource/ibm-plex-mono 5.3.0; licences beside them in fonts/, provenance in the
   README's "Browser assets". Latin and Latin Extended only, with the packages' own
   unicode-range values, so a browser fetches the extended subset only when the page uses
   a character from it. The family names are the ones --font-brand and --font-mono ask
   for: the package's own "Quicksand Variable" would download a face nothing uses. */
@font-face {
  font-family: "Quicksand"; font-style: normal; font-display: swap; font-weight: 300 700;
  src: url("fonts/quicksand-latin-ext-wght-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Quicksand"; font-style: normal; font-display: swap; font-weight: 300 700;
  src: url("fonts/quicksand-latin-wght-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-display: swap; font-weight: 400;
  src: url("fonts/ibm-plex-mono-latin-ext-400-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-display: swap; font-weight: 400;
  src: url("fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-display: swap; font-weight: 500;
  src: url("fonts/ibm-plex-mono-latin-ext-500-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-display: swap; font-weight: 500;
  src: url("fonts/ibm-plex-mono-latin-500-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-display: swap; font-weight: 600;
  src: url("fonts/ibm-plex-mono-latin-ext-600-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-display: swap; font-weight: 600;
  src: url("fonts/ibm-plex-mono-latin-600-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* brand */
  --s3-yellow: #efba1e;
  --s3-ink: #061324;

  /* yellow ramp */
  --yellow-50: #fdf8e7; --yellow-100: #fbedbb; --yellow-300: #f4d05a;
  --yellow-400: #efba1e; --yellow-500: #d4a316; --yellow-600: #ab8211;

  /* ink ramp */
  --ink-900: #061324; --ink-850: #0a1a2f; --ink-800: #0f243d;
  --ink-700: #16314f; --ink-600: #204064; --ink-500: #2e537c;

  /* cool neutrals */
  --grey-0: #ffffff; --grey-25: #f7f9fb; --grey-50: #eff3f7; --grey-100: #e2e8ef;
  --grey-200: #cdd6e0; --grey-300: #aab7c6; --grey-400: #8593a4; --grey-500: #667487;
  --grey-600: #4b5869; --grey-700: #343f4e; --grey-800: #202a37;

  /* status primitives */
  --green-100: #d6f0e2; --green-400: #29a36a; --green-600: #1b7149;
  --amber-100: #fbe6cd; --amber-400: #e8871a; --amber-600: #a35d0f;
  --red-100: #fadcd9;   --red-400: #d63b30;   --red-600: #97251d;
  --blue-100: #d8e7f7;  --blue-400: #2f7fd1;  --blue-600: #1f5790;

  /* semantic */
  --surface-page: var(--grey-25);
  --surface-card: var(--grey-0);
  --surface-sunken: var(--grey-50);
  --surface-rail: var(--ink-900);
  --surface-rail-hover: var(--ink-800);
  --surface-rail-active: var(--ink-700);
  --surface-inverse: var(--ink-900);
  --surface-overlay: rgba(6, 19, 36, 0.56);
  --surface-accent-soft: var(--yellow-50);

  --text-strong: var(--ink-900);
  --text-body-color: var(--grey-700);
  --text-muted: var(--grey-500);
  --text-faint: var(--grey-400);
  --text-inverse: var(--grey-0);
  --text-inverse-muted: rgba(255, 255, 255, 0.64);
  --text-on-accent: var(--ink-900);

  --border-subtle: var(--grey-100);
  --border-default: var(--grey-200);
  --border-strong: var(--grey-300);
  --border-inverse: rgba(255, 255, 255, 0.14);
  --border-accent: var(--yellow-400);

  --action-primary-bg: var(--yellow-400);
  --action-primary-bg-hover: var(--yellow-300);
  --action-primary-bg-active: var(--yellow-500);
  --action-primary-fg: var(--ink-900);
  --action-ghost-bg-hover: var(--grey-50);
  --action-ghost-bg-active: var(--grey-100);

  --disabled-bg: var(--grey-100);
  --disabled-fg: var(--grey-400);
  --disabled-border: var(--grey-200);

  --field-bg: var(--grey-0);
  --field-border: var(--grey-200);
  --field-border-hover: var(--grey-300);
  --field-border-focus: var(--yellow-500);
  --field-placeholder: var(--grey-400);

  --feedback-success-fg: var(--green-600); --feedback-success-bg: var(--green-100); --feedback-success-solid: var(--green-400);
  --feedback-warning-fg: var(--amber-600); --feedback-warning-bg: var(--amber-100); --feedback-warning-solid: var(--amber-400);
  --feedback-danger-fg: var(--red-600);    --feedback-danger-bg: var(--red-100);    --feedback-danger-solid: var(--red-400);
  --feedback-info-fg: var(--blue-600);     --feedback-info-bg: var(--blue-100);     --feedback-info-solid: var(--blue-400);

  /* type */
  --font-brand: "Quicksand", "Trebuchet MS", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
  --weight-medium: 500; --weight-semibold: 600; --weight-bold: 700;
  --text-2xs: 11px; --text-xs: 12px; --text-sm: 13px; --text-md: 15px; --text-lg: 17px;
  --text-xl: 20px; --text-2xl: 24px; --text-3xl: 30px;
  --tracking-display: -0.02em; --tracking-label: 0.08em; --tracking-eyebrow: 0.14em;
  --type-body: var(--weight-medium) var(--text-md) / 1.65 var(--font-brand);
  --type-body-sm: var(--weight-medium) var(--text-sm) / 1.5 var(--font-brand);
  --type-label: var(--weight-semibold) var(--text-xs) / 1.5 var(--font-brand);
  --type-data: var(--weight-medium) var(--text-sm) / 1.5 var(--font-mono);

  /* space, shape, layout */
  --space-2: 4px; --space-3: 8px; --space-4: 12px; --space-5: 16px; --space-6: 20px; --space-7: 24px;
  --radius-xs: 3px; --radius-sm: 6px; --radius-md: 10px; --radius-lg: 14px;
  --radius-xl: 20px; --radius-pill: 999px;
  --rail-width: 232px; --topbar-height: 56px; --statusbar-height: 32px;
  --row-height: 40px; --row-height-compact: 32px;
  --control-height-sm: 28px; --control-height-md: 36px; --control-height-lg: 44px;

  /* elevation */
  --shadow-xs: 0 1px 2px rgba(6, 19, 36, 0.06);
  --shadow-sm: 0 1px 3px rgba(6, 19, 36, 0.08), 0 1px 2px rgba(6, 19, 36, 0.04);
  --shadow-md: 0 4px 12px rgba(6, 19, 36, 0.1), 0 1px 3px rgba(6, 19, 36, 0.06);
  --shadow-overlay: 0 24px 56px rgba(6, 19, 36, 0.28);
  --ring-focus: 0 0 0 3px rgba(239, 186, 30, 0.45);

  /* motion — flat and mechanical; no bounce, no overshoot */
  --dur-instant: 80ms; --dur-fast: 130ms; --dur-medium: 200ms; --dur-slow: 320ms;
  --ease-standard: cubic-bezier(0.2, 0, 0.2, 1);

  /* ---- Chart line colours, by Module 1-6 slot ----
     RE-STEPPED FROM THE S3 ID PALETTE AND NOT YET VALIDATED. The set these replace
     was checked under deuteranopia at a floor of DeltaE 8; this one carries no such
     claim. Re-run before shipping:
       node <dataviz-skill>/scripts/validate_palette.js \
         "#2f7fd1,#e8871a,#29a36a,#d4a316,#97251d,#2e537c" --mode light --surface "#fff"
     Nothing reads these tokens yet: colourForSlot in graph-core.js draws from its own
     validated GRAPH_COLOURS, so re-stepping a value here changes no chart until
     graph-core.js is changed to read them. */
  --chart-1: #2f7fd1;
  --chart-2: #e8871a;
  --chart-3: #29a36a;
  --chart-4: #d4a316;
  --chart-5: #97251d;
  --chart-6: #2e537c;
  --chart-unassigned: var(--grey-400);
  --chart-grid: rgba(6, 19, 36, 0.08);
  --chart-axis-text: var(--grey-500);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font: var(--type-body);
  font-size: var(--text-sm);
  color: var(--text-body-color);
  background: var(--surface-page);
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--ink-700); text-decoration: none; }
a:hover { color: var(--yellow-600); text-decoration: underline; }

/* Focus is a 3px yellow ring on everything focusable, always visible on keyboard focus. */
:focus-visible { outline: none; box-shadow: var(--ring-focus); border-radius: var(--radius-xs); }

/* ---------------------------------------------------------------- top bar ---
   The ink strip. The active tab carries a 3px yellow inset bar — the system's one
   recurring signature — not a pale blue fill. */
#topbar {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  height: var(--topbar-height);
  padding: 0 var(--space-6);
  background: var(--surface-inverse);
  border-bottom: 0;
  position: sticky;
  top: 0;
  z-index: 20;
}
#topbar > strong {
  font: var(--type-label);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--grey-0);
  white-space: nowrap;
}
/* Optional: replace the wordmark with the knockout lockup — see docs/design/markup-notes.md */
#topbar .s3-logo { height: 22px; width: auto; display: block; }

/* The supplied DataWolf.png is the full-colour lockup on an OPAQUE WHITE background, and the "Wolf"
   half of the wordmark is black. Dropped straight onto --surface-inverse it renders as a white block
   with half the name invisible. The chip makes that white a deliberate surface rather than an
   accident, and leaves the brand asset untouched. Replace both with a knockout lockup if one is ever
   supplied: delete this rule and the wrapping <span>, nothing else depends on them. */
#topbar .s3-logo-chip {
  display: flex;
  align-items: center;
  background: var(--grey-0);
  border-radius: var(--radius-md);
  padding: 6px 10px;
  flex: none;
}

#tabs { display: flex; align-items: stretch; height: var(--topbar-height); gap: 0; flex: 1; }
#tabs button {
  border: 0;
  background: transparent;
  padding: 0 var(--space-4);
  height: var(--topbar-height);
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  border-radius: 0;
  font: var(--type-body-sm);
  color: var(--text-inverse-muted);
  transition: color var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard);
}
#tabs button:hover { color: var(--grey-0); background: rgba(255, 255, 255, 0.08); }
#tabs button:active { background: rgba(255, 255, 255, 0.12); }
#tabs button.active {
  background: transparent;
  color: var(--grey-0);
  font-weight: var(--weight-semibold);
  box-shadow: inset 0 -3px 0 var(--s3-yellow);
}

.badge {
  background: var(--feedback-danger-solid);
  color: var(--grey-0);
  border-radius: var(--radius-pill);
  padding: 0 7px;
  min-width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: var(--type-data);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
}

/* !important because this is a utility class competing with component rules of EQUAL specificity, where
   the later declaration wins on source order alone. `.rp-err { display: flex }` sits far below this and
   silently beat it, leaving both dialogs' empty validation panels painted on screen as a red bar under
   the title of Manage Readers and of Change settings. Nothing in this stylesheet uses .hidden to SHOW
   an element, so this cannot strand anything. dashboard-assets.test.js fails if a co-class overrides it. */
.hidden { display: none !important; }

/* Status dot. The pulse runs only while something is genuinely live. */
.dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--grey-400);
  flex: none;
}
.dot.up { background: var(--feedback-success-solid); }
.dot.down { background: var(--feedback-danger-solid); }
.dot.live { animation: s3-pulse 1600ms linear infinite; }
@keyframes s3-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* #status in the status bar: mono, so the msg/s figure does not jitter as it ticks. */
#status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font: var(--type-data);
  font-size: var(--text-xs);
  color: var(--grey-0);
  white-space: nowrap;
}
#status-text { font-variant-numeric: tabular-nums; }

/* The Manage Readers button lives on ink, so it is the ghost-on-ink variant. */
#topbar-manage {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  height: var(--control-height-md);
  padding: 0 var(--space-4);
  border: 1px solid var(--border-inverse);
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.06);
  color: var(--grey-0);
  font: var(--type-body-sm);
  font-weight: var(--weight-semibold);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-standard);
}
#topbar-manage:hover { background: rgba(255, 255, 255, 0.12); }
#topbar-manage:active { background: rgba(255, 255, 255, 0.06); }

main { padding: var(--space-6); flex: 1; min-height: 0; display: flex; flex-direction: column; }
.tab { display: none; }
.tab.active { display: block; }

/* ------------------------------------------------------------------ tables ---
   40px rows, 32px on group heads. Headers are uppercase micro-labels; data cells
   are mono and tabular so columns of numbers never jitter. */
table { width: 100%; border-collapse: collapse; background: var(--surface-card); }
th, td {
  text-align: left;
  padding: 0 14px;
  height: var(--row-height);
  border-bottom: 1px solid var(--border-subtle);
  font: var(--type-data);
  color: var(--text-body-color);
  font-variant-numeric: tabular-nums;
}
th {
  height: var(--row-height-compact);
  color: var(--text-muted);
  font: var(--type-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  border-bottom: 1px solid var(--border-default);
  background: var(--surface-card);
  position: sticky;
  top: 0;
  z-index: 1;
}
th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.sortable:hover { color: var(--text-strong); }
tbody tr { transition: background-color var(--dur-instant) var(--ease-standard); }
tbody tr:hover { background: var(--surface-sunken); }

/* Stale: red text AND a red inset bar, so the state does not rest on colour of text alone. */
tr.stale td { color: var(--feedback-danger-fg); background: var(--feedback-danger-bg); }
tr.stale td:first-child { box-shadow: inset 3px 0 0 var(--feedback-danger-solid); }
tr.stale:hover td { background: var(--feedback-danger-bg); }

.group-head td {
  background: var(--surface-sunken);
  color: var(--text-strong);
  font: var(--type-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  height: var(--row-height-compact);
  border-bottom: 1px solid var(--border-subtle);
}
.group-head:hover td { background: var(--surface-sunken); }

/* ------------------------------------------------------------- banner etc. ---
   The banner carries three different kinds of news. Default is warning; add
   .banner-danger / .banner-ok from app.js where the message warrants it. */
.banner {
  position: fixed;
  /* On top of the status bar, not over it: the bar carries the connection state the banner is most
     often about, and covering it would hide the very line that says so. */
  bottom: var(--statusbar-height); left: 0; right: 0;
  z-index: 40;
  background: var(--feedback-warning-bg);
  border-top: 1px solid var(--feedback-warning-solid);
  color: var(--feedback-warning-fg);
  padding: var(--space-4) var(--space-6);
  font: var(--type-body-sm);
}
.banner-danger { background: var(--feedback-danger-bg); border-top-color: var(--feedback-danger-solid); color: var(--feedback-danger-fg); }
.banner-ok { background: var(--feedback-success-bg); border-top-color: var(--feedback-success-solid); color: var(--feedback-success-fg); }

.loading, .empty {
  padding: var(--space-7);
  color: var(--text-muted);
  font: var(--type-body-sm);
}

/* ---------------------------------------------------------------- controls --- */
.controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  margin-bottom: var(--space-4);
  font: var(--type-body-sm);
  color: var(--text-body-color);
}
/* .hidden alone loses to .controls's own display:flex at equal specificity, since
   .controls is declared later — this compound selector beats both regardless of order. */
.controls.hidden { display: none; }
.controls label { display: inline-flex; align-items: center; gap: var(--space-2); white-space: nowrap; }

.controls input, .controls select,
.modal input, .modal select,
input, select {
  height: var(--control-height-md);
  padding: 0 10px;
  border: 1px solid var(--field-border);
  border-radius: var(--radius-md);
  background: var(--field-bg);
  color: var(--text-strong);
  font: var(--type-body-sm);
  font-family: var(--font-brand);
  transition: border-color var(--dur-fast) var(--ease-standard);
}
input { font-family: var(--font-mono); }
input::placeholder { color: var(--field-placeholder); }
input:hover, select:hover { border-color: var(--field-border-hover); }
input:focus, select:focus { outline: none; border-color: var(--field-border-focus); box-shadow: var(--ring-focus); }
input[type="checkbox"] { height: auto; accent-color: var(--s3-yellow); }
.id-range { width: 6.5em; }

/* Buttons. One primary (yellow) per view; everything else is quiet.
   Hover lightens, press darkens — no scale, no lift: a gloved hand on a marine
   tablet needs a colour change, not a physics simulation. */
button.page {
  height: var(--control-height-md);
  padding: 0 14px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--surface-card);
  color: var(--text-strong);
  font: var(--type-body-sm);
  font-weight: var(--weight-semibold);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-standard);
}
button.page:hover { background: var(--action-ghost-bg-hover); }
button.page:active { background: var(--action-ghost-bg-active); }
button.page:disabled {
  background: var(--disabled-bg);
  border-color: var(--disabled-border);
  color: var(--disabled-fg);
  cursor: not-allowed;
  opacity: 1;               /* never just fade a safety control */
}
/* Add .primary in app.js to the one action that commits: Save, Send, Start. */
button.page.primary {
  background: var(--action-primary-bg);
  border-color: transparent;
  color: var(--action-primary-fg);
  font-weight: var(--weight-bold);
}
button.page.primary:hover { background: var(--action-primary-bg-hover); }
button.page.primary:active { background: var(--action-primary-bg-active); }
/* A disabled primary must still read as disabled. button.page.primary has the same specificity
   as button.page:disabled and is declared later, so without this rule a disabled Send stays
   yellow while the typed-confirmation gate is holding it off. Declared after :hover and :active,
   which share its specificity, so pointing at a disabled button changes nothing either. */
button.page.primary:disabled { background: var(--disabled-bg); border-color: var(--disabled-border); color: var(--disabled-fg); }
button.page.danger { color: var(--feedback-danger-fg); }
button.page.danger:hover { background: var(--feedback-danger-bg); }
/* Small square toggles (the +/- expanders) */
button.page.icon-only { width: var(--control-height-sm); height: var(--control-height-sm); padding: 0; }

pre {
  white-space: pre-wrap;
  word-break: break-all;
  margin: var(--space-2) 0;
  padding: var(--space-4);
  background: var(--surface-sunken);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  font: var(--type-data);
  font-size: var(--text-xs);
  color: var(--text-strong);
}
code { font: var(--type-data); color: var(--text-strong); }

.muted { opacity: 1; color: var(--text-muted); font: var(--type-body-sm); margin: var(--space-2) 0; }

/* --------------------------------------------------------------- new parts ---
   Selectors for markup the app does not emit yet. Defining them here costs
   nothing and means the JS change is markup-only. See docs/design/markup-notes.md. */

/* Card: the wrapper every table, graph and form group should sit in. */
.s3-card {
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  margin-bottom: var(--space-5);
}
.s3-card-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border-subtle);
}
.s3-card-title { font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-strong); }
/* A caption strip inside a card: "Latest reading per module…", "Showing 25 of 613…" */
.s3-caption {
  padding: 10px var(--space-5);
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--border-subtle);
  font: var(--type-body-sm);
  color: var(--text-body-color);
}

/* KPI row. Replaces reading the estate's health out of the status line. */
.s3-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: var(--space-4); margin-bottom: var(--space-5); }
.s3-kpi { background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: 14px var(--space-5); }
.s3-kpi-label { font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-muted); }
.s3-kpi-value { font: var(--weight-semibold) var(--text-3xl) / 1.12 var(--font-mono); color: var(--text-strong); font-variant-numeric: tabular-nums; margin-top: var(--space-2); }
.s3-kpi-value.is-bad { color: var(--feedback-danger-fg); }
.s3-kpi-unit { font-size: var(--text-lg); color: var(--text-muted); }
.s3-kpi-note { font: var(--type-body-sm); color: var(--text-muted); }

/* Status pill. Use for CRC OK/FAIL, alarm state, queue state, command stage. */
.s3-pill {
  display: inline-flex; align-items: center; gap: var(--space-2);
  height: 22px; padding: 0 10px;
  border-radius: var(--radius-pill);
  font: var(--type-label); font-size: var(--text-2xs);
  white-space: nowrap;
}
.s3-pill-ok      { background: var(--feedback-success-bg); color: var(--feedback-success-fg); }
.s3-pill-bad     { background: var(--feedback-danger-solid); color: var(--grey-0); }
.s3-pill-warn    { background: var(--feedback-warning-bg); color: var(--feedback-warning-fg); }
.s3-pill-info    { background: var(--feedback-info-bg); color: var(--feedback-info-fg); }
.s3-pill-quiet   { background: var(--surface-sunken); color: var(--text-body-color); border: 1px solid var(--border-default); }
.s3-pill-locate  { background: var(--surface-accent-soft); color: var(--text-strong); border: 1px solid var(--border-accent); }
/* The pulsing dot inside a red pill (an active alarm) is white; the default grey would barely show on red. */
.s3-pill-bad .dot { background: var(--grey-0); }

/* Ink status bar, pinned to the bottom of every product screen. */
.s3-statusbar {
  height: var(--statusbar-height);
  display: flex; align-items: center; gap: var(--space-5);
  padding: 0 var(--space-6);
  background: var(--surface-inverse);
  color: var(--text-inverse);
  font: var(--type-data); font-size: var(--text-xs);
}
.s3-statusbar .sep { color: var(--border-inverse); }
.s3-statusbar .right { margin-left: auto; color: var(--text-inverse-muted); }
/* Pinned to the bottom of the viewport. Without this the bar sits at the end of the page — below the fold
   on a long Readers table — and the connection state it carries, moved here out of the sticky top bar,
   would be out of sight exactly when the page is longest. */
.s3-statusbar { position: sticky; bottom: 0; z-index: 30; flex: none; }
.s3-statusbar .sep { margin: 0 var(--space-2); }

/* Optional 232px ink nav rail, if the top strip is replaced. */
.s3-rail { width: var(--rail-width); flex: 0 0 var(--rail-width); background: var(--surface-rail); display: flex; flex-direction: column; }
.s3-rail-head { padding: var(--space-5) var(--space-5) var(--space-4); border-bottom: 1px solid var(--border-inverse); }
.s3-rail-item {
  display: flex; align-items: center; gap: 10px;
  height: var(--row-height); padding: 0 var(--space-5);
  color: var(--text-inverse-muted); font: var(--type-body-sm);
  cursor: pointer; border: 0; background: transparent; width: 100%; text-align: left;
  transition: background-color var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}
.s3-rail-item:hover { background: var(--surface-rail-hover); color: var(--grey-0); }
.s3-rail-item.active { background: var(--surface-rail-active); color: var(--grey-0); font-weight: var(--weight-semibold); box-shadow: inset 3px 0 0 var(--s3-yellow); }
.s3-rail-item.active .s3-icon { color: var(--s3-yellow); }
.s3-rail-item .count { margin-left: auto; }

/* Lucide icons: stroke only, never filled. */
.s3-icon { display: inline-flex; width: 18px; height: 18px; flex: 0 0 auto; color: currentColor; }
.s3-icon-sm { width: 14px; height: 14px; }
/* An icon beside the label it illustrates: the hidden-reader notices, the downlink dialog's
   warnings, "Stops tag detection". The label is its own flex item, so a long line wraps
   beside the icon rather than flowing underneath it. */
.s3-icon-line { display: flex; align-items: center; gap: var(--space-3); }
.s3-icon-line > .s3-icon { flex: none; }
/* A warning block can run to several lines; its icon belongs beside the first. */
.dl-block.s3-icon-line { align-items: flex-start; }

/* ---------------------------------------------------------------- canvases --- */
canvas { max-width: 100%; background: var(--surface-card); border: 0; border-radius: 0; margin-top: 0; }
/* Charts sit inside .s3-card, so the card supplies the frame the old border did. */

/* ----------------------------------------------------------------- modals --- */
.modal-overlay {
  position: fixed; inset: 0;
  background: var(--surface-overlay);
  backdrop-filter: blur(2px);
  display: flex; align-items: flex-start; justify-content: center;
  overflow: auto; z-index: 50;
}
.modal-overlay:focus { outline: none; }
.modal {
  background: var(--surface-card);
  color: var(--text-body-color);
  border: 0;
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-overlay);
  padding: var(--space-5) var(--space-6) var(--space-6);
  margin: 40px 0;
  width: min(620px, 92vw);
}
.modal h3 { margin: 0 0 var(--space-4); font: var(--weight-semibold) var(--text-xl) / 1.28 var(--font-brand); letter-spacing: var(--tracking-display); color: var(--text-strong); }
.modal h4 { margin: var(--space-5) 0 var(--space-3); font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-muted); }

.rp-reader, .rp-row { display: flex; align-items: center; gap: var(--space-3); margin: var(--space-3) 0; }
.rp-reader .rp-name { flex: 1; min-width: 0; font-family: var(--font-brand); }
.rp-hide { display: flex; align-items: center; gap: var(--space-2); font: var(--type-body-sm); color: var(--text-body-color); white-space: nowrap; }
.rp-row span { flex: 1; font: var(--type-data); color: var(--text-body-color); }
.rp-actions { margin-top: var(--space-5); display: flex; gap: var(--space-3); padding-top: var(--space-5); border-top: 1px solid var(--border-subtle); }
.rp-err {
  display: flex; gap: 10px;
  background: var(--feedback-danger-bg);
  border: 1px solid var(--feedback-danger-solid);
  color: var(--feedback-danger-fg);
  border-radius: var(--radius-sm);
  padding: var(--space-4) 14px;
  margin-bottom: var(--space-3);
  font: var(--type-body-sm);
}
.rp-group { border-top: 1px solid var(--border-subtle); padding: var(--space-3) 0; }
.rp-group:first-child { border-top: 0; }
.rp-group-head { font: var(--type-body-sm); font-weight: var(--weight-semibold); color: var(--text-strong); padding-bottom: var(--space-2); }
.rp-row .rp-slot { margin-left: var(--space-2); }

/* ------------------------------------------------- Tags: table/graph split --- */
#tags.tab.active { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: auto; }
#tag-table-pane { flex: 4 1 0; min-height: 180px; overflow: auto; }
#tag-graph-pane {
  flex: 6 1 0; min-height: 160px; display: flex; flex-direction: column;
  border-top: 1px solid var(--border-default);
  margin-top: var(--space-3); padding-top: var(--space-3);
}
.tag-graph-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-3); margin-bottom: var(--space-2);
}
.tag-graph-head strong { font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-strong); }
.tag-graph-head select { height: var(--control-height-md); }
#tag-graph-area { flex: 1; min-height: 0; position: relative; }
/* !important is not redundant: Chart.js writes inline style.height as it draws. */
#tag-graph-area canvas { margin-top: 0; height: 100% !important; }
#tag-graph-caption { padding-top: var(--space-2); }

/* Selection is a 3px yellow inset bar plus the soft yellow surface. */
tr.tag-selected > td { background: var(--surface-accent-soft); }
tr.tag-selected > td:first-child { box-shadow: inset 3px 0 0 var(--s3-yellow); }
tr.tag-selected:hover > td { background: var(--surface-accent-soft); }
tr.tag-row { cursor: pointer; }

/* Pane height segmented control */
#tag-graph-opts { display: flex; align-items: center; gap: var(--space-3); }
#tags.tags-mode-table #tag-graph-opts { display: none; }
#tags.tags-mode-table #tag-graph-pane { display: none; }
#tags.tags-mode-graph #tag-table-pane { display: none; }
#tag-pane-modes { display: flex; align-items: center; margin-left: auto; }
#tag-pane-modes .pane-label { font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-muted); margin-right: var(--space-2); white-space: nowrap; }
#tag-pane-modes button {
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  color: var(--text-body-color);
  height: var(--control-height-md);
  padding: 0 14px;
  font: var(--type-body-sm);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-standard);
}
#tag-pane-modes button:hover { background: var(--action-ghost-bg-hover); }
#tag-pane-modes button + button { margin-left: -1px; }
#tag-pane-modes button:first-of-type { border-radius: var(--radius-md) 0 0 var(--radius-md); }
#tag-pane-modes button:last-of-type { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
#tag-pane-modes button.active {
  background: var(--action-primary-bg);
  color: var(--action-primary-fg);
  font-weight: var(--weight-semibold);
  border-color: var(--action-primary-bg-active);
  position: relative; z-index: 1;
}

/* --------------------------------------------------------- recency heatmap ---
   Bands come from the semantic feedback ramp, never from the accountability
   mapping — these tiles are about read recency, not whether a person is mustered.
   Type on every filled tile is full-opacity ink (or white on red), per the
   contrast rule; the yellow band keeps ink, never white. */
#tag-period { width: 5.5em; }
.heat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: var(--space-3);
  position: absolute; inset: 0;
  overflow-y: auto;
  padding: var(--space-2) 0;
}
.heat-box {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  height: 52px;
  border-radius: var(--radius-sm);
  font: var(--type-data); font-size: var(--text-xs);
  line-height: 1.3;
  color: var(--ink-900);
}
.heat-id { font-weight: var(--weight-semibold); font-size: var(--text-sm); }
.heat-age { font-size: var(--text-2xs); }
.heat-recent { background: var(--feedback-success-solid); }
.heat-soon   { background: var(--s3-yellow); }
.heat-late   { background: var(--feedback-warning-solid); }
.heat-stale  { background: var(--feedback-danger-solid); color: var(--grey-0); }
.heat-none   { background: var(--grey-300); color: var(--ink-900); }
/* Never read at all — hollow, deliberately NOT the solid grey of .heat-none, which
   means "seen, but no period is set so no band could be computed". */
.heat-never  { background: var(--surface-card); color: var(--text-muted); border: 1px dashed var(--border-strong); }

/* Compact: both dimensions halved. The age's worst case is RetentionDays x 1440
   minutes ("40,320m" at the default 28 days), which still fits — do not reach for
   overflow:hidden if that is ever exceeded; widen the tile. */
.heat-grid.heat-compact { grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); gap: var(--space-2); }
.heat-grid.heat-compact .heat-box { height: 26px; font-size: 9px; line-height: 1.15; }
.heat-grid.heat-compact .heat-id { font-size: 10px; }

/* Band-count key. The swatches carry the band classes above for their colour, so
   there is exactly one definition of each band in this file. */
.heat-key { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; font: var(--type-data); font-size: var(--text-xs); color: var(--text-body-color); }
.heat-key-item { display: flex; align-items: center; gap: var(--space-2); }
.heat-key-swatch { width: 11px; height: 11px; border-radius: 2px; flex: none; }
.heat-key-total { font-weight: var(--weight-semibold); color: var(--text-strong); }
.heat-key-sep { color: var(--border-strong); }

/* -------------------------------------------------------- Reader Settings --- */
.rs-controls { display: flex; align-items: center; gap: 10px; padding: 0 0 var(--space-4); flex-wrap: wrap; font: var(--type-body-sm); }
.rs-controls input { width: 72px; }

.rs-card {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  background: var(--surface-card);
  padding: 0;
  margin-bottom: var(--space-5);
  overflow: hidden;
}
.rs-card-head {
  display: flex; align-items: center; justify-content: flex-start; flex-wrap: wrap;
  gap: var(--space-4);
  padding: 14px var(--space-5);
  border-bottom: 1px solid var(--border-subtle);
  font: var(--weight-semibold) var(--text-lg) / 1.28 var(--font-brand);
  letter-spacing: var(--tracking-display);
  color: var(--text-strong);
  margin-bottom: 0;
}
.rs-card-actions { display: flex; gap: var(--space-3); align-items: center; margin-left: auto; font-weight: var(--weight-medium); }
.rs-inflight {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font: var(--type-label); font-size: var(--text-2xs);
  color: var(--feedback-warning-fg);
  background: var(--feedback-warning-bg);
  border: 0;
  border-radius: var(--radius-pill);
  padding: 0 10px; height: 24px;
  white-space: nowrap;
}
.rs-nochange { font: var(--type-body-sm); color: var(--text-muted); font-weight: var(--weight-medium); }

/* Headline bands: three labelled groups, side by side, each wrapping internally. */
.rs-headline {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0;
  padding: 0;
  border-bottom: 1px solid var(--border-subtle);
}
.rs-band-section {
  display: flex; flex-wrap: wrap; align-items: flex-start; align-content: flex-start;
  gap: var(--space-3) var(--space-6);
  padding: 14px var(--space-5);
  border-left: 1px solid var(--border-subtle);
}
.rs-band-section:first-child { border-left: 0; }
.rs-band-label {
  flex: 1 0 100%;
  font: var(--type-label); font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  white-space: nowrap;
}
.rs-cell { white-space: nowrap; }
.rs-cell-key { display: block; font: var(--type-body-sm); font-size: var(--text-2xs); color: var(--text-muted); }
/* white-space:normal is NOT redundant with overflow-wrap below — nowrap suppresses
   every soft wrap opportunity, including the ones overflow-wrap exists to insert. */
.rs-cell b { font: var(--type-data); color: var(--text-strong); white-space: normal; overflow-wrap: anywhere; }
.rs-cell.rs-red, .rs-cell.rs-red b, .rs-cell.rs-red .rs-cell-key { color: var(--feedback-danger-fg); }
/* The Module cell declares the gap itself. min-width:62px alone cannot separate the
   slot label from the device name beside it: at 11px uppercase with 0.08em tracking
   "Module 1" measures 65-67px, so the min-width is never the larger of the two and
   the two strings touch ("MODULE 1Muster A Module 1"). */
.rs-pane td:first-child { display: flex; align-items: center; gap: var(--space-3); }
.rs-slot {
  min-width: 62px;
  font: var(--type-label); font-size: var(--text-2xs);
  text-transform: uppercase; letter-spacing: var(--tracking-label);
  color: var(--text-muted);
}
.rs-toggle { flex: none; }
.rs-pane { overflow-x: auto; }
.rs-pane table { min-width: 100%; }
.rs-pane th, .rs-pane td { white-space: nowrap; }
/* The seven data columns are nowrap on purpose. The expanded detail row is a <td colspan="7"> in the
   same table and is NOT a data column: left to inherit that rule it rendered its whole change history
   as one unwrappable line, one row high and far wider than the window. */
/* height:auto is the load-bearing one. `th, td` above gives every cell a SPECIFIED height of
   var(--row-height); a data cell is that tall anyway so it behaves as a minimum and nobody notices,
   but this cell holds a panel hundreds of pixels tall and was pinned to 40px by it, painting its
   contents straight over the rows below. Measured on the live page: td 40 / content 570 before,
   td 571 after. */
.rs-pane td.rs-detail-cell { white-space: normal; padding: 0; height: auto; }
.rs-detail-inner {
  /* Pinned to the left edge of the scrolling pane, so the panel stays readable even if the data
     columns are wide enough to scroll. The max-width stops the panel itself ever being the thing that
     widens the table. */
  position: sticky;
  left: 0;
  max-width: 100vw;
  /* A module with months of history scrolls HERE rather than pushing the page down. */
  max-height: 60vh;
  overflow-y: auto;
  padding: var(--space-3) var(--space-4);
}
.rs-toggle {
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  border-radius: var(--radius-xs);
  width: 24px; height: 24px; line-height: 1;
  cursor: pointer;
  color: var(--text-strong);
  font-family: var(--font-brand);
}
.rs-toggle:hover { background: var(--action-ghost-bg-hover); }
/* Stacked, not a flex row. Side by side, the nowrap timestamp and the change list set the row's width
   and pushed the table past the window. Timestamp above, keys beneath, each on its own line. */
.rs-change { display: block; padding: var(--space-2) 0; border-top: 1px solid var(--border-subtle); }
.rs-change:first-child { border-top: 0; }
.rs-when { display: block; margin-bottom: var(--space-2); font: var(--type-data); font-size: var(--text-xs); color: var(--text-muted); white-space: nowrap; }
.rs-key { font: var(--type-data); color: var(--text-strong); font-weight: var(--weight-semibold); }
.rs-other { padding: var(--space-2) 0 var(--space-3); display: flex; flex-wrap: wrap; gap: var(--space-2) 20px; align-items: baseline; }
.rs-pair { white-space: nowrap; font: var(--type-data); color: var(--text-body-color); }
.rs-cmds { padding: var(--space-3) 0 var(--space-2); border-top: 1px solid var(--border-subtle); margin-top: var(--space-3); }
/* .rs-stale itself keeps no visual rule of its own — the marking is the "*" plus the
   title, which carries the real last-seen time. Do not give it one back. */
.rs-stale-mark { color: var(--feedback-danger-fg); font-weight: var(--weight-bold); margin-left: var(--space-2); }

/* Module History / Setting History tabs — same idiom as the dialog's tab strip. */
.rs-tabs { display: flex; gap: var(--space-2); border-bottom: 1px solid var(--border-subtle); margin: 0; padding: 0 var(--space-5); }
.rs-tab {
  border: 0; background: transparent;
  padding: 0 14px; height: var(--row-height);
  cursor: pointer;
  font: var(--type-body-sm);
  color: var(--text-muted);
  font-family: var(--font-brand);
  transition: color var(--dur-fast) var(--ease-standard);
}
.rs-tab:hover { color: var(--text-strong); }
.rs-tab-on { font-weight: var(--weight-semibold); color: var(--text-strong); box-shadow: inset 0 -3px 0 var(--s3-yellow); }
.rs-settings-view { padding: var(--space-4) var(--space-5) var(--space-5); }
.rs-groups { display: flex; gap: var(--space-4); align-items: flex-start; }
.rs-group { flex: 1 1 0; min-width: 0; }
.rs-group-wide { margin-top: var(--space-4); }
.rs-group-title { font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-muted); padding-bottom: var(--space-2); }
.rs-setting-row { cursor: pointer; }
.rs-setting-detail td { background: var(--surface-sunken); }
tr.rs-setting-open > td { background: var(--surface-accent-soft); }
tr.rs-setting-open > td:first-child { box-shadow: inset 3px 0 0 var(--s3-yellow); }

/* ------------------------------------------------------- Settings downlink --- */
.dl-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  position: sticky; top: 0; z-index: 1;
  background: var(--surface-card);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
}
.dl-head h3 { margin: 0; }
.dl-close {
  border: 0; background: transparent;
  font-size: 20px; line-height: 1; cursor: pointer;
  width: 32px; height: 32px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
}
.dl-close:hover { background: var(--action-ghost-bg-hover); color: var(--text-strong); }
/* .dl-head is sticky and paints over what scrolls beneath, so anything scrolled or
   focused into view programmatically needs this margin or it lands underneath. */
#dl-err, #dl-confirm, .dl-tab { scroll-margin-top: 48px; }

.dl-tabs { display: flex; gap: var(--space-2); border-bottom: 1px solid var(--border-subtle); margin: var(--space-4) 0 0; }
.dl-tab {
  border: 0; background: transparent;
  padding: 0 14px; height: var(--control-height-lg);
  cursor: pointer; font: var(--type-body-sm);
  color: var(--text-muted);
  font-family: var(--font-brand);
}
.dl-tab:hover { color: var(--text-strong); }
.dl-tab-on { font-weight: var(--weight-semibold); color: var(--text-strong); box-shadow: inset 0 -3px 0 var(--s3-yellow); }
.dl-panel { padding-top: var(--space-4); }

.dl-settings {
  display: flex; flex-direction: column; gap: 0;
  margin: var(--space-3) 0;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  overflow: hidden;
  container-type: inline-size;
}
.dl-row {
  display: grid;
  grid-template-columns: minmax(0, 1.8fr) minmax(0, 1.2fr) minmax(0, 2fr) minmax(0, 1.1fr);
  gap: 14px;
  padding: 10px 14px;
  border-top: 1px solid var(--border-subtle);
  align-items: start;
}
.dl-row:first-child { border-top: 0; }
.dl-edited { background: var(--surface-accent-soft); box-shadow: inset 3px 0 0 var(--s3-yellow); }
.dl-stamp { display: block; font: var(--type-data); font-size: var(--text-2xs); color: var(--text-muted); }
.dl-key code { font: var(--type-data); color: var(--text-strong); font-weight: var(--weight-semibold); }
.dl-input { width: 100%; }
.dl-badge {
  display: inline-flex; align-items: center;
  font: var(--type-label); font-size: var(--text-2xs);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-pill);
  padding: 0 8px; height: 20px;
  margin-left: var(--space-3);
  background: var(--surface-sunken);
  color: var(--text-muted);
}
.dl-bad { color: var(--feedback-danger-fg); }
.dl-warn { color: var(--feedback-warning-fg); }

/* Not colour alone: a bordered block with its own heading, so the consequence is
   legible to an operator who cannot rely on colour. */
.dl-gate, .dl-block {
  border: 2px solid var(--feedback-danger-solid);
  border-radius: var(--radius-lg);
  padding: 14px var(--space-5);
  margin: var(--space-4) 0;
  background: var(--feedback-danger-bg);
}
.dl-gate-title { margin: 0 0 var(--space-2); font: var(--weight-semibold) var(--text-lg) / 1.28 var(--font-brand); color: var(--feedback-danger-fg); }
.dl-gate label { display: block; margin-top: var(--space-3); max-width: 420px; }
.dl-gate input { margin-top: var(--space-2); width: 100%; height: var(--control-height-lg); border-color: var(--feedback-danger-solid); }
.dl-note {
  display: flex; gap: 10px;
  font: var(--type-body-sm);
  color: var(--feedback-warning-fg);
  background: var(--feedback-warning-bg);
  border: 1px solid var(--feedback-warning-solid);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  margin: var(--space-3) 0;
}
.dl-module {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  margin: var(--space-4) 0 0;
  padding: 14px var(--space-5);
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
}
.dl-module label { display: inline-flex; align-items: center; gap: var(--space-2); }
.dl-cmd { border-top: 1px solid var(--border-subtle); padding: var(--space-4) var(--space-2); }

/* The five-stage command timeline. Wrapping flex row, never a fixed-column table. */
.dl-timeline { display: flex; flex-wrap: wrap; gap: var(--space-2) 0; margin: var(--space-3) 0 var(--space-2); }
.dl-stage { min-width: 118px; flex: 1 1 118px; border-left: 2px solid var(--border-default); padding: 2px 0 2px 10px; }
.dl-stage-current { border-left-color: var(--s3-yellow); }
.dl-stage-label { font: var(--type-label); font-size: var(--text-2xs); font-weight: var(--weight-bold); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--tracking-label); }
.dl-stage-stamp { font: var(--type-data); color: var(--text-strong); }
.dl-stage-duration { font: var(--type-data); font-size: var(--text-xs); color: var(--text-muted); }
.dl-stage-current .dl-stage-duration { color: var(--yellow-600); font-weight: var(--weight-semibold); }
.dl-total { font: var(--type-data); font-weight: var(--weight-semibold); }
.dl-ok .dl-total      { color: var(--feedback-success-fg); }
.dl-bad .dl-total     { color: var(--feedback-danger-fg); }
.dl-warn .dl-total    { color: var(--feedback-warning-fg); }
.dl-pending .dl-total { color: var(--feedback-info-fg); }
.dl-muted .dl-total   { color: var(--text-muted); }

.dl-timing-rows { margin-top: var(--space-3); }
.dl-hop { border-top: 1px solid var(--border-subtle); padding: var(--space-3) var(--space-2); display: flex; flex-wrap: wrap; gap: 2px var(--space-4); align-items: baseline; }
.dl-hop:first-child { border-top: 0; }
.dl-hop-label { font: var(--type-body-sm); font-weight: var(--weight-semibold); color: var(--text-strong); min-width: 190px; }

.modal.dl-modal { width: min(1180px, 96vw); }
/* Above this container width, pinning the control column to 300px costs the other
   three nothing; below it the fractional layout above does the job on its own. */
@container (min-width: 1083px) {
  .dl-row { grid-template-columns: minmax(0, 2.2fr) minmax(0, 1.6fr) 300px minmax(0, 1fr); }
}
@container (max-width: 620px) {
  .dl-row { grid-template-columns: 1fr; }
}
/* Retained for browsers without container queries, where the blocks above are
   ignored. NOT a substitute on the desktop, which is the case that was broken. */
@media (max-width: 700px) {
  .dl-row { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------- Messages --- */
#messages.tab.active { display: flex; flex-direction: column; flex: 1; min-height: 0; }
#msg-panes { display: flex; flex: 1; min-height: 0; gap: var(--space-5); }
#msg-table-pane, #msg-payload-pane { flex: 1 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
#msg-body { flex: 1; min-height: 0; overflow: auto; }
#msg-body table { min-width: 100%; }
#msg-body th, #msg-body td { white-space: nowrap; }
#msg-position { padding: 0 0 var(--space-3); font: var(--type-body-sm); color: var(--text-muted); }
#msg-pager { display: flex; gap: var(--space-3); padding: var(--space-4) 0 0; }
#msg-pager button[disabled] { opacity: 1; cursor: not-allowed; }
.msg-row { cursor: pointer; }
tr.msg-selected > td { background: var(--surface-accent-soft); }
tr.msg-selected > td:first-child { box-shadow: inset 3px 0 0 var(--s3-yellow); }
tr.msg-selected:hover > td { background: var(--surface-accent-soft); }
/* Detail panels push, they don't float: the payload is a docked panel, not an overlay. */
#msg-payload-pane {
  flex: 0 1 400px; min-width: 320px;
  border-left: 0;
  padding: 0;
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.msg-payload-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-3);
  margin: 0;
  padding: 14px var(--space-5);
  border-bottom: 1px solid var(--border-subtle);
}
.msg-payload-head strong { font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-strong); }
#msg-payload { flex: 1; min-height: 0; overflow: auto; padding: var(--space-5); }
@media (max-width: 900px) {
  #msg-panes { flex-direction: column; }
  #msg-payload-pane { flex: 1 1 auto; min-width: 0; }
}

/* ------------------------------------------------------------------ Queue --- */
.q-group {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  background: var(--surface-card);
  padding: 0;
  margin-bottom: var(--space-4);
  overflow: hidden;
}
.q-group-head {
  font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label);
  color: var(--text-strong);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border-subtle);
  margin-bottom: 0;
}
/* Two fixed lines per entry — flex-wrap on EACH line, never across them, so a long
   devEui/setting pair wraps within line one instead of pushing line two aside. */
.q-entry { border-top: 1px solid var(--border-subtle); padding: var(--space-4) var(--space-5); }
.q-entry:first-child { border-top: 0; }
.q-entry-top, .q-entry-bottom { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); align-items: baseline; }
.q-entry-bottom { margin-top: var(--space-1, 2px); }

/* ---------------------------------------------- Application Settings form ---
   The tab's single column becomes three grouped cards. Works with the existing
   markup as a one-column form; see docs/design/markup-notes.md for the grouped version. */
#settings .controls {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-4);
  /* Half the screen on a desktop, but never narrower than the 520px this panel already had:
     a bare 50% would SHRINK it on a laptop or phone, where half the viewport is less than that. */
  max-width: max(520px, 50%);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-6);
}
/* align-items is declared here deliberately, not left to inherit. `.controls label` (above) sets
   `align-items: center`, and this rule previously declared display and flex-direction but not that
   property -- so the lower-specificity rule still won it, and in a COLUMN flex container `center`
   centres children horizontally. That was the whole cause of the settings fields rendering centred,
   and of their being narrow: `center` also replaces the default `stretch`, collapsing every box to
   the browser's intrinsic size=20 width. */
#settings label { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-muted); }
/* An explicit base width rather than the browser's default size=20, so that "twice as wide" below is
   a defined quantity instead of whatever a given browser happens to render. max-width keeps a field
   inside the panel at small viewports, where 40ch is wider than the panel's own inner width. */
#settings label input, #settings label select { text-transform: none; letter-spacing: 0; width: 20ch; max-width: 100%; }
/* Fields holding a string: host, username, the four topic filters, the gRPC endpoint and the
   application ID. Exactly double the base. 40ch also fits a 36-character application ID GUID, which
   at the base width was unreadable without scrolling the field. Numeric fields keep the base -- a
   retention day count needs no more room than it had. */
#settings label.wide input { width: 40ch; }
#settings small { font: var(--type-body-sm); color: var(--text-muted); margin-top: -6px; }
#settings h3 { margin: var(--space-5) 0 0; font: var(--type-label); text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--text-strong); padding-top: var(--space-5); border-top: 1px solid var(--border-subtle); }
#settings #s-save { align-self: flex-start; margin-top: var(--space-3); background: var(--action-primary-bg); border-color: transparent; color: var(--action-primary-fg); font-weight: var(--weight-bold); padding: 0 20px; }
#settings #s-save:hover { background: var(--action-primary-bg-hover); }
#settings #s-save:active { background: var(--action-primary-bg-active); }

/* -------------------------------------------------------- reduced motion --- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}
