/* ============================================================================
   GuardM8 v2 — design system.
   CoreM8's Mission-Control register (dark field + violet bloom, rounded cards,
   real shadows, violet chrome) applied to an ARC operator console. Two rules
   survive from the original direction because this is life-safety kit:

   1. The status ramp (--p1..--p4/--ok/--test) is the ONLY colour that may say
      anything about an alarm. Violet is chrome and primary actions, never state.
   2. Priority is always colour AND glyph AND position — never colour alone.

   No framework, no icon font, no CDN, nothing external. Class names here are a
   shared vocabulary with the other console pages: refine them, never rename.
   ========================================================================== */

/* ── Reset ────────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

html {
  -webkit-text-size-adjust: 100%;
  /* Reserve the gutter so navigating from a long screen (audit) to a short one
     (help) does not shunt the whole layout sideways. */
  scrollbar-gutter: stable;
}

/* ── Scrollbars ────────────────────────────────────────────────────────────
   The browser default is an opaque grey slab, and on a graduated indigo field
   it reads as a hole punched through the surface. These are inset, translucent
   and violet, so a track belongs to the panel it sits in. Firefox gets the
   standard properties; the rest is WebKit/Blink. */

* { scrollbar-width: thin; scrollbar-color: var(--sb) transparent; }

::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--sb);
  border-radius: var(--r-pill);
  border: 3.5px solid transparent;   /* inset, via background-clip */
  background-clip: content-box;
  min-height: 38px;
}
::-webkit-scrollbar-thumb:hover,
::-webkit-scrollbar-thumb:active {
  background: var(--sb-hover);
  background-clip: content-box;
  border: 3px solid transparent;
}

/* The rail is chrome, not content. A permanent bar down the navigation is the
   most intrusive of the lot, so it only appears when the rail is in use. */
/* Hiding this bar was a mistake: the rail scrolls independently of the page, so
   a hidden bar means the last items are simply invisible and nothing on screen
   says otherwise. It is quiet, but it is there. */
.rail { scrollbar-color: var(--sb) transparent; }
.rail::-webkit-scrollbar-thumb {
  background: var(--sb); background-clip: content-box; border: 3px solid transparent;
}
.rail::-webkit-scrollbar { width: 10px; }

/* Horizontal bars under wide tables sit tighter so they do not eat a row of
   height, and a sideways scroll stays inside the table rather than dragging
   the page with it. */
.table-wrap { overscroll-behavior-x: contain; }
.table-wrap::-webkit-scrollbar { height: 10px; }

body {
  font-family: var(--f-text);
  font-size: var(--t-13);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--ink);
  background: var(--canvas);
  background-image: var(--bg-grad);
  background-attachment: fixed;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Times and counts must not jitter as they tick. */
  font-variant-numeric: tabular-nums lining-nums;
  min-height: 100vh;
}

img, svg, video { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
table { border-collapse: collapse; width: 100%; }

/* An explicit display on a class must never resurrect a [hidden] element. */
[hidden] { display: none !important; }

::selection { background: var(--accent-soft); color: var(--ink); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb {
  background: var(--hairline-strong);
  border-radius: 99px;
  border: 3px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); background-clip: content-box; }

:focus-visible {
  outline: 2px solid var(--accent-2);
  outline-offset: 1px;
  border-radius: 4px;
}

/* ── Type ─────────────────────────────────────────────────────────────────── */

.display {
  font-family: var(--f-display);
  font-variation-settings: "wdth" 112;   /* Archivo Expanded */
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* A number treated as the most important thing on screen. */
.numeral {
  font-family: var(--f-display);
  font-variation-settings: "wdth" 118;
  font-weight: 700;
  font-size: var(--t-44);
  letter-spacing: -0.03em;
  line-height: 1;
}

.mono { font-family: var(--f-mono); font-size: 0.95em; letter-spacing: -0.01em; }

.label {
  font-size: var(--t-11);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 600;
  color: var(--ink-faint);
}

.dim { color: var(--ink-dim); }
.faint { color: var(--ink-faint); }

h1, h2, h3, h4 {
  font-family: var(--f-display);
  font-variation-settings: "wdth" 108;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
h1 { font-size: var(--t-22); }
h2 { font-size: var(--t-18); }
h3 { font-size: var(--t-15); }
h4 { font-size: var(--t-13); }

/* ── Shell: command rail + annunciator bar + data surface ─────────────────── */

.shell {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  grid-template-rows: var(--annunciator-h) minmax(0, 1fr);
  grid-template-areas: "brand annunciator" "rail main";
  min-height: 100vh;
  position: relative;
  z-index: 1;
}
.shell.is-collapsed { grid-template-columns: var(--rail-w-collapsed) minmax(0, 1fr); }

/* Brand block sits in the rail column, level with the annunciator. */
.brand {
  grid-area: brand;
  position: sticky;
  top: 0;
  z-index: 41;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-4);
  border-right: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  background: var(--surface);
}
/* ── Echelon lockup ───────────────────────────────────────────────────────
   Two PNGs, not one: the artwork is flat white or flat dark ink with no
   colour of its own, so a single file vanishes against one of the two
   grounds. Which one shows follows the SAME three-state pattern as the
   colour tokens — bare :root is the light default, `prefers-color-scheme`
   covers the un-stamped system setting, and the two data-theme stamps let an
   explicit choice win in both directions. Anything less and the logo is
   invisible for whichever state was forgotten. */
.brand-logo { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
/* No `display` here on purpose: `.brand-logo img` is (0,1,1) and would
   outrank the (0,1,0) theme rules below, showing BOTH logos at once in
   light mode. Display belongs to the theme rules alone. */
.brand-logo img { height: auto; }
.brand-lockup { width: 148px; max-width: 100%; }
.brand-v {
  font-family: var(--f-mono);
  font-size: var(--t-11);
  letter-spacing: .06em;
  color: var(--ink-faint);
  padding: 2px 6px;
  border: 1px solid var(--hairline-strong);
  border-radius: 999px;
  flex: none;
}

/* Light is the default state, so the dark-ink art shows and the white hides. */
.logo-for-dark { display: none; }
.logo-for-light { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .logo-for-dark { display: block; }
  :root:not([data-theme="light"]) .logo-for-light { display: none; }
}
:root[data-theme="dark"] .logo-for-dark { display: block; }
:root[data-theme="dark"] .logo-for-light { display: none; }
:root[data-theme="light"] .logo-for-dark { display: none; }
:root[data-theme="light"] .logo-for-light { display: block; }

/* The login's identity panel carries it larger. */
.auth-logo .brand-lockup, .auth-logo img { width: 232px; }

/* The gradient mark — CoreM8 family resemblance. Violet chrome, not status. */
.brand-badge {
  width: 30px; height: 30px;
  border-radius: 9px;
  flex: none;
  display: grid;
  place-items: center;
  background: var(--accent-fill);
  color: #fff;
  font-family: var(--f-display);
  font-variation-settings: "wdth" 112;
  font-weight: 700;
  font-size: var(--t-13);
  box-shadow: 0 4px 16px -4px rgba(124, 108, 255, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.brand-mark {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--f-display);
  font-variation-settings: "wdth" 112;
  font-weight: 700;
  font-size: var(--t-15);
  letter-spacing: -0.01em;
  white-space: nowrap;
}
.brand-mark .brand-mark-full { color: var(--ink-faint); font-weight: 600; }
.is-collapsed .brand-mark-full { display: none; }

/* ── Annunciator: a row of lamps read by position and colour together ─────── */

.annunciator {
  grid-area: annunciator;
  position: sticky;
  top: 0;
  z-index: 40;
  /* A grid item defaults to min-width:auto, so it refuses to shrink below its
     own content — the lamp cluster is ~430px and was pushing the whole page
     wide below 390px, giving the console a horizontal scrollbar on a phone.
     `overflow-x: auto` below cannot do its job without this. */
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-5);
  border-bottom: 1px solid var(--hairline);
  background: var(--surface); /* fallback where color-mix is unsupported */
  background: color-mix(in srgb, var(--canvas) 78%, transparent);
  backdrop-filter: blur(16px) saturate(140%);
  overflow-x: auto;
  scrollbar-width: none;
}
.annunciator::-webkit-scrollbar { display: none; }

/* The status lamps read as one instrument, not three loose words — a recessed
   panel with hairline dividers between the cells, the way a real annunciator
   groups its indicators. */
.lamp-cluster {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  align-self: center;
  /* It is an instrument, not a block of text: it must be allowed to shrink,
     or it stays at its content width (416px) on every screen and simply
     overflows the bar on a phone. */
  min-width: 0;
  height: 32px;
  padding: 0 var(--sp-1);
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  background: color-mix(in srgb, var(--surface-2) 60%, transparent);
}
.lamp-cluster .lamp {
  padding: 0 var(--sp-3);
}
.lamp-cluster .lamp + .lamp {
  box-shadow: inset 1px 0 0 var(--hairline);
}

/* Below the point where the bar runs out of room, the lamps keep the dot and
   the numeral and drop the word. Colour + position + a number is the whole
   read; "critical" is the part you already know by where it sits. The lamp's
   aria-label and title still say it in full. */
@media (max-width: 700px) {
  .lamp-label { display: none; }
  .lamp-cluster .lamp { padding: 0 var(--sp-2); }
}

.lamp {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--t-11);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-weight: 600;
  color: var(--ink-dim);
  white-space: nowrap;
}
.lamp-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--ink-faint);
  flex: none;
}
/* Lamp colour is health/alarm state — the ramp, never the accent. */
.lamp[data-state="ok"] .lamp-dot { background: var(--ok); }
.lamp[data-state="warn"] .lamp-dot { background: var(--p2); }
.lamp[data-state="fail"] .lamp-dot { background: var(--p1); }
.lamp[data-state="ok"] { color: var(--ink); }

.annunciator-count {
  font-family: var(--f-display);
  font-variation-settings: "wdth" 115;
  font-weight: 700;
  font-size: var(--t-15);
}
.annunciator .spacer { margin-left: auto; flex: 1 1 auto; }

/* The right-hand cluster: clock, theme, account — the operator's own controls,
   held together and set apart from the estate-state lamps by a hairline. */
.annunciator-tools {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  align-self: center;
}
.annunciator-tools .lamp--clock { color: var(--ink-dim); }
.tool-divider {
  width: 1px;
  height: 20px;
  background: var(--hairline-strong);
  flex: none;
}

/* Account chip: initials badge + name/role stacked, so identity reads at a
   glance without a stray status dot pretending the operator is a lamp. */
.account {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0 var(--sp-2) 0 0;
}
.account-badge {
  width: 26px; height: 26px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--ink);
  font-family: var(--f-display);
  font-variation-settings: "wdth" 112;
  font-weight: 700;
  font-size: var(--t-13);
}
.account-id {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}
.account-name {
  font-size: var(--t-13);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}
.account-role {
  font-size: var(--t-11);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-weight: 600;
  color: var(--ink-faint);
}

/* ── Command rail ─────────────────────────────────────────────────────────── */

.rail {
  grid-area: rail;
  position: sticky;
  top: var(--annunciator-h);
  align-self: start;
  /* dvh, not vh: on a phone the browser chrome eats the difference and the last
     rail item sits under it permanently. */
  height: calc(100dvh - var(--annunciator-h));
  max-height: calc(100dvh - var(--annunciator-h));
  border-right: 1px solid var(--hairline);
  background: linear-gradient(180deg, var(--surface), var(--canvas));
  padding: var(--sp-3) var(--sp-3) var(--sp-4);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.rail-group { padding: var(--sp-4) var(--sp-2) var(--sp-1); }
.is-collapsed .rail-group { padding-left: 0; text-align: center; }
.is-collapsed .rail-group span { display: none; }

.rail a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 8px 11px;
  border-radius: var(--r);
  color: var(--ink-dim);
  font-weight: 500;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
  white-space: nowrap;
}
.rail a svg { flex: none; opacity: 0.85; }
.rail a:hover { background: var(--hover); color: var(--ink); }
.rail a.is-active { color: var(--ink); background: var(--accent-soft); }
/* Gradient notch on the active item — chrome, so gradient is allowed. */
.rail a.is-active::before {
  content: "";
  position: absolute;
  left: -12px;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--accent);
}
.is-collapsed .rail a { justify-content: center; padding-left: 0; padding-right: 0; }
.is-collapsed .rail a span { display: none; }

.rail-signout { margin-top: auto; padding-top: var(--sp-4); border-top: 1px solid var(--hairline); }
.rail-signout button {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 8px 11px;
  background: none;
  border: 0;
  border-radius: var(--r);
  color: var(--ink-faint);
  font-weight: 500;
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.rail-signout button:hover { background: var(--hover); color: var(--ink); }
.is-collapsed .rail-signout button { justify-content: center; padding-left: 0; padding-right: 0; }
.is-collapsed .rail-signout span { display: none; }

.rail-badge {
  margin-left: auto;
  font-family: var(--f-mono);
  font-size: var(--t-11);
  color: var(--ink-dim);
  background: var(--surface-3);
  border: 1px solid var(--hairline);
  border-radius: var(--r-pill);
  padding: 1px 7px;
}
.rail a.is-active .rail-badge { color: var(--ink); border-color: var(--accent-line); }
.is-collapsed .rail-badge { display: none; }

/* ── Main ─────────────────────────────────────────────────────────────────── */

.main {
  grid-area: main;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
/* `.main` is a column flex, so every page wrapper inside it is a flex item —
   and a flex item's `min-width: auto` means it will not shrink below its own
   min-content. One wide child (the alarm page's call list) therefore pushed the
   whole page wider than the phone, with no element visibly overflowing to point
   at. Page wrappers shrink; their own children already handle their overflow. */
.main > * { min-width: 0; }

.view-head {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4) 28px;
  border-bottom: 1px solid var(--hairline);
  flex-wrap: wrap;
}
.view-head > :first-child { min-width: 0; }
/* A subtitle is a caption, not a paragraph. Capped so a long one wraps instead
   of stretching the header and shoving the buttons onto their own row. */
.view-head .sub { color: var(--ink-dim); font-size: var(--t-13); font-weight: 500;
                  max-width: 68ch; margin-top: 2px; }
.view-head .actions { margin-left: auto; display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
/* Inline controls in a header row size to their content, not to 100%. */
.view-head .actions .input, .view-head .actions .select { width: auto; min-width: 0; }

.view-body { padding: var(--sp-5) 28px 56px; }
.view-body.flush { padding: 0; }

/* A page wrapper is a flex COLUMN, so every panel in it is a flex item, and a
   flex item's `min-width: auto` means it refuses to shrink below its own
   content's minimum. One long unbreakable thing inside a panel therefore sets
   the width of the whole page, and nothing visibly overflows to point at — the
   page just scrolls sideways, which is how a status column ends up off the
   right-hand edge. `.main > *` was fixed for this; panels need the same.
   Content that genuinely cannot shrink owns its own scroll container. */
.view-body > *, .view-body.col > * { min-width: 0; }

/* ── Pager ────────────────────────────────────────────────────────────────
   Under a list, never over it: the count is the payload ("51-100 of 204"),
   the numbers are navigation. Previous/Next are words — an icon pair at 3am is
   a guess. Renders only when there is more than one page; the macro decides.
   No motion, nothing to hover-reveal. See DESIGN.md 2026-09-01. */
.pager {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-3) var(--sp-4);
  padding: var(--sp-4) 28px var(--sp-5);
}
.view-body.flush .pager { padding-left: 28px; padding-right: 28px; }
.panel .pager { padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--hairline); }
.pager-count { margin: 0; color: var(--ink-dim); font-size: var(--t-13); }
.pager-count .mono { color: var(--ink); font-variant-numeric: tabular-nums; }
.pager-nav { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; }
.pager-pages { display: flex; align-items: center; gap: var(--sp-1);
  margin: 0; padding: 0; list-style: none; }
.pg {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 34px; height: 34px; padding: 0 10px;
  border: 1px solid var(--hairline-strong); border-radius: var(--r);
  background: var(--surface); color: var(--ink-dim);
  font-size: var(--t-13); font-weight: 500; font-variant-numeric: tabular-nums;
  text-decoration: none; white-space: nowrap;
}
a.pg:hover { border-color: var(--accent); color: var(--ink); }
a.pg:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pg.is-here { background: var(--accent-soft); border-color: var(--accent); color: var(--ink); }
/* An unavailable step stays in place rather than vanishing — the row must not
   reflow under the cursor as you page. */
.pg.is-off { opacity: .4; }
.pg-gap { color: var(--ink-faint); padding: 0 2px; }
@media (max-width: 560px) {
  /* Previous + page numbers + Next do not fit one line on a small phone, and
     the 28px page gutter is most of what is left. Wrap rather than push the
     page sideways — a pager that makes the whole page scroll is worse than no
     pager, because it hides the last column of the table it belongs to. */
  .pager { flex-direction: column; align-items: stretch; padding-left: var(--sp-3);
           padding-right: var(--sp-3); }
  .view-body.flush .pager { padding-left: var(--sp-3); padding-right: var(--sp-3); }
  .pager-nav { margin-left: 0; justify-content: space-between; flex-wrap: wrap;
               gap: var(--sp-2) var(--sp-1); }
  .pager-pages { flex-wrap: wrap; }
  .pg { min-width: 30px; padding: 0 7px; }
}

/* ── Panels — CoreM8 cards: rounded, bordered, genuinely shadowed ─────────── */

.panel {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
  /* A panel is a container; it must never be the thing that sets its parent's
     width. As a flex or grid item it inherits `min-width: auto`, so its own
     min-content width wins over the column it was placed in — one panel on
     /admin/settings/signals/ came out 320px inside a 278px grid cell and
     scrolled the whole page sideways, with nothing visibly overflowing to
     point at. Content that genuinely cannot shrink wraps, or owns its own
     scroll container; the panel yields. */
  min-width: 0;
}
.panel-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--hairline);
}
.panel-head .label { color: var(--ink-dim); }
.panel-head .actions { margin-left: auto; display: flex; gap: var(--sp-2); }
.panel-body { padding: var(--sp-4); }

.grid { display: grid; gap: var(--sp-4); }
/* A row of panels that wraps. The `min()` is the whole point: a bare
   `minmax(320px, 1fr)` keeps a 320px track even in a 278px container, so the
   panel sits 42px past the edge and the page scrolls sideways with nothing
   visibly overflowing. Four settings pages each carried their own inline copy
   of that mistake. */
.cardgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: var(--sp-3);
}
.cardgrid > * { min-width: 0; }

.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* 62/38 — the event and its workflow, then context that sticks */
.split { display: grid; grid-template-columns: 62fr 38fr; gap: var(--sp-4); align-items: start; }
.split > aside { position: sticky; top: calc(var(--annunciator-h) + var(--sp-4)); }

/* ── KPI counters — a number, its meaning, and its trend ──────────────────── */

.counter {
  position: relative;
  overflow: hidden;
  padding: var(--sp-4);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  transition: border-color var(--fast) var(--ease);
}
.counter:hover { border-color: var(--hairline-strong); }
.counter .numeral { font-size: var(--t-32); }
.counter.has-spark { padding-bottom: 56px; }
/* Colour means "there is something here". A zero is not a warning, so it stays
   in ink — a red 0 next to a live 2097 says the opposite of what is true. */
.counter[data-priority="1"]:not([data-zero]) .numeral { color: var(--p1); }
.counter[data-priority="2"]:not([data-zero]) .numeral { color: var(--p2); }
.counter[data-priority="3"]:not([data-zero]) .numeral { color: var(--p3); }
.counter[data-tone="ok"]:not([data-zero]) .numeral { color: var(--ok); }
.counter[data-zero] .numeral { color: var(--ink-faint); }
.counter .meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--t-11);
  color: var(--ink-faint);
  flex-wrap: wrap;
}

/* Delta vs yesterday. Deliberately NEUTRAL ink: on a life-safety board green/red
   belongs to alarm state, and "more signals than yesterday" is not an alarm. */
.delta { font-family: var(--f-mono); font-weight: 500; color: var(--ink-dim); white-space: nowrap; }

/* Server-rendered inline SVG sparkline — no chart library reaches an ARC
   network, and this way the trend renders with JavaScript disabled too. */
.counter .spark {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  width: 100%;
  height: 40px;
  opacity: 0.9;
  pointer-events: none;
}

/* The received timestamp is a fixed-shape value. Left unstyled it took a share
   of the table's spare width from the site name, which is the column an
   operator actually reads. */
.th-received { width: 1%; white-space: nowrap; }

/* ── Data table ───────────────────────────────────────────────────────────── */

.table-wrap { overflow-x: auto; }

table.data { font-size: var(--t-13); }
table.data thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  text-align: left;
  font-size: var(--t-11);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--ink-faint);
  background: var(--surface);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--hairline-strong);
  white-space: nowrap;
}
table.data tbody td {
  padding: 0 var(--sp-4);
  height: var(--row-h);
  border-bottom: 1px solid var(--hairline);
  vertical-align: middle;
  white-space: nowrap;
}
table.data tbody td.wrap { white-space: normal; min-width: 20ch; }
/* Long free text in a dense table clips with an ellipsis; the full text rides
   on the title, and the detail page has all of it. Desktop only — the card
   form below 700px wraps it onto its own line instead. */
@media (min-width: 701px) {
  table.data tbody td.cell-clip { width: 100%; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
table.data tbody tr { transition: background var(--fast) var(--ease); }
table.data tbody tr:hover { background: var(--hover); }
table.data tbody tr.is-clickable { cursor: pointer; }
table.data tbody tr:last-child td { border-bottom: 0; }

/* Priority gutter — 3px of colour down the row's leading edge, always paired
   with a glyph so it never depends on colour alone. */
td.gutter {
  width: 0;
  padding: 0 !important;
  border-left: 3px solid transparent;
}
tr[data-priority="1"] td.gutter { border-left-color: var(--p1); }
tr[data-priority="2"] td.gutter { border-left-color: var(--p2); }
tr[data-priority="3"] td.gutter { border-left-color: var(--p3); }
tr[data-priority="4"] td.gutter { border-left-color: var(--p4); }
tr[data-status="closed"] td.gutter,
tr[data-status="cancelled"] td.gutter { border-left-color: var(--hairline-strong); }

/* The only looping animation in the product, and only on live P1. */
tr[data-priority="1"][data-live="1"] td.gutter { animation: breathe 1.4s var(--ease) infinite; }
@keyframes breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) {
  tr[data-priority="1"][data-live="1"] td.gutter { animation: none; opacity: 1; }
}

/* Arrival should be noticed — that is a safety function, not decoration. */
tr.is-new { animation: enter var(--enter) var(--ease) both; }
@keyframes enter {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

/* Keyboard focus on a row — a state you can find again at a glance. Violet is
   chrome: it marks where YOU are, and says nothing about the alarm. */
table.data tbody tr.is-kfocus {
  background: var(--accent-soft);
  outline: 1px solid var(--accent-line);
  outline-offset: -1px;
}

/* A queue that just stops, with no marker, reads as a page that failed to
   finish loading. This says the list ended on purpose. */
.rule-end {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--hairline);
  color: var(--ink-faint);
  font-size: var(--t-11);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* ── Priority glyph ───────────────────────────────────────────────────────── */

.glyph {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--t-11);
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.glyph::before { font-size: 9px; line-height: 1; }
.glyph[data-priority="1"] { color: var(--p1); }
.glyph[data-priority="1"]::before { content: "\25A0"; }   /* ■ */
.glyph[data-priority="2"] { color: var(--p2); }
.glyph[data-priority="2"]::before { content: "\25B2"; }   /* ▲ */
.glyph[data-priority="3"] { color: var(--p3); }
.glyph[data-priority="3"]::before { content: "\25C6"; }   /* ◆ */
.glyph[data-priority="4"] { color: var(--p4); }
.glyph[data-priority="4"]::before { content: "\25CF"; }   /* ● */
/* The ramp does not stop at P4 — `ok` and `test` are states an operator reads
   just as often, and they were the only two in the language with no glyph of
   their own. Same rule: colour AND shape, never colour alone. */
.glyph[data-priority="ok"] { color: var(--ok); }
.glyph[data-priority="ok"]::before { content: "\25B6"; }   /* ▶ */
.glyph[data-priority="test"] { color: var(--test); }
.glyph[data-priority="test"]::before { content: "\25A1"; }   /* □ */

/* ── Tags — pill badges; tone = alarm state only ──────────────────────────── */

.tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-pill);
  font-size: var(--t-11);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
  background: var(--hover);
  white-space: nowrap;
}
.tag[data-tone="p1"] { color: var(--p1); border-color: var(--p1); background: var(--p1-bed); }
.tag[data-tone="p2"] { color: var(--p2); border-color: var(--p2); background: var(--p2-bed); }
.tag[data-tone="p3"] { color: var(--p3); border-color: var(--p3); background: var(--p3-bed); }
.tag[data-tone="ok"] { color: var(--ok); border-color: var(--ok); background: var(--ok-bed); }
.tag[data-tone="test"] { color: var(--test); border-color: var(--test); background: var(--test-bed); }

/* ── Controls ─────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: 32px;
  padding: 0 var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r);
  color: var(--ink);
  font-size: var(--t-13);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease),
              transform var(--fast) var(--ease), filter var(--fast) var(--ease);
}
.btn:hover { background: var(--surface-3); border-color: var(--ink-faint); }
.btn:active { background: var(--surface-2); }
.btn[disabled] { opacity: 0.4; cursor: not-allowed; }
.btn.is-quiet { background: transparent; border-color: transparent; color: var(--ink-dim); }
.btn.is-quiet:hover { background: var(--hover); color: var(--ink); }
/* Primary = the gradient. Chrome only — an alarm action is primary because of
   its placement, not because it borrows a status colour. */
.btn.is-primary {
  background: var(--accent-fill);
  color: #fff;
  border: 0;
  box-shadow: 0 6px 18px -6px rgba(124, 108, 255, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.btn.is-primary:hover { filter: brightness(1.07); }
.btn.is-danger { color: var(--p1); border-color: var(--p1); background: var(--p1-bed); }
.btn.is-danger:hover { background: rgba(255, 59, 47, 0.2); }
.btn.is-lg { height: 40px; padding: 0 var(--sp-4); font-size: var(--t-15); }
.btn.is-block { width: 100%; }
.btn.is-sm { height: 24px; padding: 0 var(--sp-2); font-size: var(--t-11); border-radius: var(--r-sm); }

@media (prefers-reduced-motion: no-preference) {
  .btn:hover { transform: translateY(-1px); }
  .btn:active { transform: translateY(0); }
}

.key {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  background: var(--surface-3);
  border: 1px solid var(--hairline-strong);
  border-radius: 5px;
  font-family: var(--f-mono);
  font-size: 10px;
  color: var(--ink-dim);
}

.field { display: flex; flex-direction: column; gap: 6px; }
.input, .select, .textarea {
  width: 100%;
  min-height: 34px;
  padding: 6px var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r);
  color: var(--ink);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
.input::placeholder, .textarea::placeholder { color: var(--ink-faint); }
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--accent-line);
  box-shadow: 0 0 0 3px var(--accent-soft);
  background: var(--surface);
}
.textarea { min-height: 72px; resize: vertical; line-height: 1.55; }
.select {
  appearance: none;
  padding-right: var(--sp-5);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none' stroke='%23a6a6b8' stroke-width='1.5'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
}

.check { display: inline-flex; align-items: center; gap: var(--sp-2); cursor: pointer; }
.check input { accent-color: var(--accent); width: 14px; height: 14px; }

/* ── Segmented scope control ──────────────────────────────────────────────── */

.seg {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: calc(var(--r) + 1px);
  /* The scopes are one control and must not wrap into two ragged rows, so on a
     narrow phone the control scrolls sideways within itself rather than
     widening the page. Without this the queue had a horizontal scrollbar at
     320px — the whole console shifting under the thumb. */
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
.seg::-webkit-scrollbar { display: none; }
.seg a, .seg button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: calc(var(--r) - 2px);
  color: var(--ink-dim);
  font-weight: 600;
  font-size: var(--t-13);
  white-space: nowrap;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.seg button { background: transparent; border: 0; font-family: inherit; cursor: pointer; }
.seg a:hover, .seg button:hover { color: var(--ink); }
.seg a.is-on, .seg button.is-on { background: var(--surface-3); color: var(--ink); box-shadow: var(--shadow); }
.seg .n, .chip .n {
  font-family: var(--f-mono);
  font-size: var(--t-11);
  color: var(--ink-faint);
}
.seg a.is-on .n, .chip.is-on .n { color: var(--ink-dim); }

/* ── Priority filter chips — glyph carries the priority, count says what you
      are filtering into ──────────────────────────────────────────────────── */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 11px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r);
  background: var(--surface-2);
  color: var(--ink-dim);
  font-weight: 600;
  white-space: nowrap;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.chip:hover { background: var(--surface-3); }
.chip.is-on { background: var(--surface-3); border-color: var(--ink-faint); color: var(--ink); }

/* ── Definition rows ──────────────────────────────────────────────────────── */

.defs { display: grid; grid-template-columns: minmax(min(90px, 100%), auto) minmax(0, 1fr); gap: var(--sp-2) var(--sp-4); }
.defs dt { color: var(--ink-faint); font-size: var(--t-11); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; padding-top: 2px; }
.defs dd { min-width: 0; overflow-wrap: anywhere; }

/* ── Flash messages ───────────────────────────────────────────────────────── */

/* Notifications: a toast stack in the top-right of the work surface. Fixed, so
   a confirmation never shoves the page down under the operator's cursor;
   dismissable; cleared by console.js after a beat. Tone is a left rule, never
   a filled block — the status ramp stays reserved for alarm state. */
.flashes {
  position: fixed; bottom: var(--sp-4); right: var(--sp-4);
  z-index: 90; display: flex; flex-direction: column; gap: var(--sp-2);
  width: min(420px, calc(100vw - 2 * var(--sp-4))); pointer-events: none;
}
.flash {
  pointer-events: auto;
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--hairline-strong);
  border-left-width: 3px;
  border-radius: var(--r);
  background: var(--panel);
  box-shadow: var(--shadow-lg);
  font-weight: 500; font-size: var(--t-13); line-height: 1.4;
}
.flash[data-tone="success"], .flash[data-tone="ok"] { border-left-color: var(--ok); }
.flash[data-tone="danger"] { border-left-color: var(--p1); }
.flash[data-tone="warning"], .flash[data-tone="warn"] { border-left-color: var(--p2); }
.flash[data-tone="info"] { border-left-color: var(--accent); }
.toast-x {
  margin-left: auto; flex: none; width: 24px; height: 24px; border-radius: var(--r-sm);
  border: 0; background: transparent; color: var(--ink-faint); font-size: 18px; line-height: 1;
  cursor: pointer;
}
.toast-x:hover { color: var(--ink); background: var(--hover); }
@media (max-width: 900px) {
  .flashes { top: auto; bottom: calc(80px + env(safe-area-inset-bottom, 0px)); right: var(--sp-3); left: var(--sp-3); width: auto; }
}
@media (prefers-reduced-motion: no-preference) {
  .flash { animation: toast-in .18s var(--ease, ease) both; }
  @keyframes toast-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
}

/* ── Empty states — state the fact, give the time, say nothing cheerful ───── */

.empty {
  padding: var(--sp-7) var(--sp-5);
  text-align: center;
  color: var(--ink-dim);
}
.empty .display { font-size: var(--t-22); color: var(--ink); margin-bottom: var(--sp-2); }
.empty .empty-actions {
  margin-top: var(--sp-4);
  display: flex;
  gap: var(--sp-2);
  justify-content: center;
  flex-wrap: wrap;
}

/* ── Timeline ─────────────────────────────────────────────────────────────── */

.timeline { display: flex; flex-direction: column; }
.timeline li { display: grid; grid-template-columns: 74px minmax(0, 1fr) auto; gap: var(--sp-4); padding: var(--sp-3) 0; border-bottom: 1px solid var(--hairline); list-style: none; align-items: start; }
/* The delete control is the third column. Without it the form was a block-level
   third grid item with nowhere to go, so it wrapped onto the next row and sat
   under the timestamp — bottom left of the entry it was meant to remove. */
/* A small square control that is an icon and nothing else. Shared vocabulary:
   it lives here rather than in dashboard.css because the alarm page uses it for
   the activity delete and every dialog close, and does not load that file. */
.icon-btn {
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-dim);
  border-radius: var(--r-sm);
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--t-13); line-height: 1;
}
.icon-btn:hover:not(:disabled) { background: var(--hover); color: var(--ink); border-color: var(--hairline-strong); }
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.icon-btn:disabled { opacity: .3; cursor: not-allowed; }
.icon-btn.is-danger:hover:not(:disabled) { color: var(--p1); border-color: var(--p1); }

.timeline-del { grid-column: 3; }
.timeline-del .icon-btn { opacity: 0; }
.timeline li:hover .timeline-del .icon-btn,
.timeline-del .icon-btn:focus-visible { opacity: 1; }
/* Touch has no hover, so on a tablet the control would never appear at all. */
@media (hover: none) { .timeline-del .icon-btn { opacity: .55; } }
.timeline li:last-child { border-bottom: 0; }
.timeline time { font-family: var(--f-mono); font-size: var(--t-11); color: var(--ink-faint); padding-top: 2px; }

/* ── Priority header band: full-bleed status colour on the alarm page ─────── */

.band {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4) 28px;
  border-bottom: 1px solid var(--hairline-strong);
  background: var(--band-bg, var(--surface));
  flex-wrap: wrap;
}
/* The band is a calm neutral strip. Only the genuinely urgent priorities keep a
   coloured bottom edge; medium and low get a plain hairline — no yellow line —
   with priority still carried by the prio chip's colour + glyph. */
.band[data-priority="1"] { border-bottom: 2px solid var(--p1); }
.band[data-priority="2"] { border-bottom: 2px solid var(--p2); }
.band[data-priority="3"] { border-bottom-color: var(--hairline-strong); }
.band[data-priority="4"] { border-bottom-color: var(--hairline-strong); }
.band .actions { margin-left: auto; display: flex; gap: var(--sp-2); align-items: center; }

/* ── Camera ───────────────────────────────────────────────────────────────── */

.cam {
  position: relative;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  aspect-ratio: 16 / 9;
  display: grid;
  place-items: center;
  overflow: hidden;
}
.cam img { width: 100%; height: 100%; object-fit: cover; }
.cam .stamp {
  position: absolute;
  left: 6px; bottom: 6px;
  font-family: var(--f-mono);
  font-size: var(--t-11);
  /* This label sits on video, not on the page, so it stays light-on-dark in
     both themes — inheriting --ink would make it black text on a black scrim
     the moment the console is switched to light. */
  color: var(--scrim-ink);
  background: var(--scrim);
  border-radius: 5px;
  padding: 1px 6px;
}

/* ── Theme toggle ──────────────────────────────────────────────────────────
   The button offers the theme you would switch TO, so in dark it shows a sun.
   Driven purely by CSS off the same signals the palette uses, which means it
   is already correct at first paint — including for someone whose OS is set to
   light and who has never touched the toggle. */
.theme-toggle { padding: 0 var(--sp-2); }
.theme-toggle .t-light { display: none; }
.theme-toggle .t-dark { display: inline-flex; }

:root[data-theme="light"] .theme-toggle .t-dark { display: none; }
:root[data-theme="light"] .theme-toggle .t-light { display: inline-flex; }

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .theme-toggle .t-dark { display: none; }
  :root:not([data-theme="dark"]) .theme-toggle .t-light { display: inline-flex; }
}

/* ── Bars (reports, busiest sites) ────────────────────────────────────────── */

.bars { display: flex; flex-direction: column; gap: var(--sp-2); }
.bar-row { display: grid; grid-template-columns: minmax(min(90px, 100%), 26%) 1fr auto; gap: var(--sp-3); align-items: center; }
.bar-track { height: 6px; background: var(--surface-3); border-radius: var(--r-pill); overflow: hidden; min-width: 40px; }
.bar-fill { height: 100%; border-radius: var(--r-pill); background: var(--ink-faint); }
.bar-fill[data-priority="1"] { background: var(--p1); }
.bar-fill[data-priority="2"] { background: var(--p2); }
.bar-fill[data-priority="3"] { background: var(--p3); }
.bar-fill[data-priority="4"] { background: var(--p4); }
.bar-row .n { font-family: var(--f-mono); font-size: var(--t-11); color: var(--ink-dim); }

/* ══════════════════════════════════════════════════════════════════════════
   Queue workspace — the screen an operator stares at for twelve hours. The
   estate is usually near-empty (that is the job going well), so the queue is
   one card in a working dashboard, not a table marooned in a void.
   ════════════════════════════════════════════════════════════════════════ */

.queue-layout {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: var(--sp-4);
  align-items: start;
}
.queue-side { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }

.queue-tools {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--hairline);
  flex-wrap: wrap;
}
.queue-tools .input { flex: 1 1 200px; max-width: 360px; }
.queue-tools .count {
  margin-left: auto;
  font-family: var(--f-mono);
  font-size: var(--t-11);
  color: var(--ink-faint);
  white-space: nowrap;
}

/* Quick acknowledge from the row — quiet until the row is looked at. */
.qa { display: inline-flex; vertical-align: middle; margin-left: var(--sp-2); }
table.data .qa .btn { background: transparent; border-color: var(--hairline-strong); color: var(--ink-dim); }
table.data tr:hover .qa .btn, table.data tr.is-kfocus .qa .btn { background: var(--surface-3); color: var(--ink); }
table.data .qa .btn:hover { border-color: var(--ink-faint); color: var(--ink-bright); }

/* Age — how long it has waited, scannable without reading. Display face and
   ink weight, not hue: saturation stays reserved for priority. */
td.c-age { font-family: var(--f-mono); color: var(--ink-dim); }
tr[data-live="1"] td.c-age {
  font-family: var(--f-display);
  font-variation-settings: "wdth" 112;
  font-weight: 600;
  font-size: var(--t-15);
  color: var(--ink);
}
tr[data-live="1"][data-age-tier="2"] td.c-age { color: var(--ink-bright); }
tr[data-live="1"][data-age-tier="3"] td.c-age { color: var(--ink-bright); font-weight: 700; }
/* Age tiers brighten and embolden the age; the word "overdue" itself is the
   escalation tag (escalation.py), rendered from the same threshold the queue
   scope and the dashboard tile use — one definition, not two. */

/* Compact side lists — recently closed, overdue supervision */
.side-list { list-style: none; display: flex; flex-direction: column; }
.side-list > li { border-top: 1px solid var(--hairline); }
.side-list > li:first-child { border-top: 0; }
.side-list a, .side-list .side-row {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-2);
  border-radius: var(--r);
  transition: background var(--fast) var(--ease);
}
.side-list a:hover { background: var(--hover); }
.side-list time { font-family: var(--f-mono); font-size: var(--t-11); color: var(--ink-faint); padding-top: 3px; }
.side-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.side-main > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side-main strong { font-weight: 600; font-size: var(--t-13); }

/* Keyboard reference — a spec sheet, not a tooltip */
.kbd-list {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-2) var(--sp-3);
  align-items: center;
  color: var(--ink-dim);
  font-size: var(--t-13);
}
.kbd-list .keys { display: inline-flex; gap: 3px; justify-content: flex-end; }

/* ── Utilities ────────────────────────────────────────────────────────────── */

.row { display: flex; align-items: center; gap: var(--sp-3); }
.row.wrap { flex-wrap: wrap; }
.col { display: flex; flex-direction: column; gap: var(--sp-3); }
.push { margin-left: auto; }
.sep { color: var(--ink-faint); }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40ch; }
.mt-2 { margin-top: var(--sp-2); }
.mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ══════════════════════════════════════════════════════════════════════════
   Wall mode — the unattended ops-room screen, read from across the room.
   Four priority lanes; a clear board reads as deliberate, not broken.
   ════════════════════════════════════════════════════════════════════════ */

.wallboard { min-height: 100vh; display: flex; flex-direction: column; position: relative; z-index: 1; }

.wall-top {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4) 28px;
  border-bottom: 1px solid var(--hairline);
  flex-wrap: wrap;
}
.wall-top .wall-clock {
  font-family: var(--f-mono);
  font-size: var(--t-22);
  color: var(--ink);
  letter-spacing: 0.02em;
}

.wall-lanes {
  flex: 1;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-4);
  padding: var(--sp-5) 28px 40px;
  align-items: start;
}
.wall-lane { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
.lane-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  padding-bottom: var(--sp-2);
  border-bottom: 2px solid var(--hairline-strong);
}
.lane-head .lane-count {
  margin-left: auto;
  font-family: var(--f-display);
  font-variation-settings: "wdth" 118;
  font-weight: 700;
  font-size: var(--t-26);
  letter-spacing: -0.02em;
  color: var(--ink-faint);
}
/* Lane identity: position (fixed order) + glyph + colour, never colour alone. */
.lane-head[data-priority="1"] { border-bottom-color: var(--p1); }
.lane-head[data-priority="2"] { border-bottom-color: var(--p2); }
.lane-head[data-priority="3"] { border-bottom-color: var(--p3); }
.lane-head[data-priority="4"] { border-bottom-color: var(--p4); }
.lane-head[data-priority="1"] .lane-count:not([data-zero]) { color: var(--p1); }
.lane-head[data-priority="2"] .lane-count:not([data-zero]) { color: var(--p2); }
.lane-head[data-priority="3"] .lane-count:not([data-zero]) { color: var(--p3); }
.lane-head[data-priority="4"] .lane-count:not([data-zero]) { color: var(--ink); }
.lane-clear {
  padding: var(--sp-5) 0;
  text-align: center;
  font-size: var(--t-11);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-weight: 600;
  color: var(--ink-faint);
}

.wall { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); align-content: start; }
/* Inside a lane the cards simply stack. */
.wall-lane .wall { grid-template-columns: minmax(0, 1fr); }

.wall-card {
  border: 1px solid var(--hairline-strong);
  border-left-width: 4px;
  border-radius: var(--r-lg);
  padding: var(--sp-4) var(--sp-4) var(--sp-4) var(--sp-4);
  background: var(--surface);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  transition: border-color var(--fast) var(--ease);
}
.wall-card:hover { border-color: var(--ink-faint); }
.wall-card[data-priority="1"] { border-left-color: var(--p1); background: var(--p1-bed); }
.wall-card[data-priority="2"] { border-left-color: var(--p2); background: var(--p2-bed); }
.wall-card[data-priority="3"] { border-left-color: var(--p3); background: var(--p3-bed); }
.wall-card[data-priority="4"] { border-left-color: var(--p4); }
.wall-card .site {
  font-family: var(--f-display);
  font-variation-settings: "wdth" 112;
  font-weight: 700;
  font-size: var(--t-22);
  line-height: 1.15;
  overflow-wrap: anywhere;
}
.wall-card .age {
  font-family: var(--f-display);
  font-variation-settings: "wdth" 118;
  font-weight: 700;
  font-size: var(--t-32);
  letter-spacing: -0.03em;
  line-height: 1;
}
/* The board's one instrument-panel moment: a P1 age is unmissable. */
.wall-card[data-priority="1"] .age { font-size: var(--t-44); }

/* A clear board is most of the job. It must read as reassuring and deliberate,
   never as a page that failed to load. */
.wall-empty {
  flex: 1;
  display: grid;
  place-content: center;
  text-align: center;
  gap: var(--sp-3);
  padding: var(--sp-7);
  color: var(--ink-dim);
}
.wall-empty .display { font-size: var(--t-44); color: var(--ink); }
.wall-empty .mono { font-size: var(--t-15); }

/* ── Sign in — split canvas, with the priority ramp as a calibration strip ──
   Replaces a lone card floating on two blurred violet orbs: purple-gradient
   ground plus one rounded card is close to the stock generated-login look, and
   on a wide screen it used a fifth of the width to say nothing about the
   product. See DESIGN.md 2026-09-01 for the three directions and why this one.

   Left panel is identity and the ramp; right is the form. No motion — reduced
   motion is the operator default and this is the first screen seen at 3am. */

.auth {
  min-height: 100vh;
  min-height: 100dvh;                /* phone chrome must not crop the button */
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  background: var(--canvas);
  position: relative;
  z-index: 1;
}

/* ── The identity half ─────────────────────────────────────────────────── */
.auth-side {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Centred as a GROUP, not pinned top-and-bottom. space-between left a large
     dead middle on a tall screen and stopped the two halves of the page
     reading as one composition. */
  justify-content: center;
  gap: clamp(40px, 9vh, 104px);
  padding: clamp(28px, 4.5vw, 64px);
  border-right: 1px solid var(--hairline);
  background: var(--surface);
  overflow: hidden;
}
/* A hairline grid, not a blur. It reads as an instrument face and survives
   both themes because it is drawn from the hairline token, not a literal. */
.auth-side::before {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(var(--hairline) 1px, transparent 1px) 0 0 / 100% 56px,
    linear-gradient(90deg, var(--hairline) 1px, transparent 1px) 0 0 / 56px 100%;
  opacity: .55;
  pointer-events: none;
}
/* One low anchor behind the mark — a hint of the console's violet, at a
   fraction of the old orbs' weight. */
.auth-side::after {
  content: "";
  position: absolute;
  top: -18%; left: -12%;
  width: 62%; aspect-ratio: 1;
  background: radial-gradient(circle, var(--accent-soft), transparent 68%);
  pointer-events: none;
}
.auth-side > * { position: relative; z-index: 1; }

.auth-lede { max-width: 30ch; }
.auth-lede h1 {
  margin: var(--sp-5) 0 0;
  font-size: clamp(28px, 3.2vw, 40px);
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.auth-lede p {
  margin: var(--sp-3) 0 0;
  color: var(--ink-dim);
  font-size: var(--t-15);
  max-width: 42ch;
}

/* ── The calibration strip — the ramp, captioned as a SCALE ─────────────
   Never as state: an operator must not be able to read the front door as
   "there is a P1 running". Colour AND glyph, the same rule as everywhere
   else in the console; the login gets no exemption. */
.ramp { display: flex; flex-direction: column; gap: var(--sp-3); }
.ramp-scale { display: flex; gap: 3px; }
.ramp-step {
  flex: 1 1 0;
  display: flex; flex-direction: column; gap: 7px;
  padding-top: 9px;
  border-top: 2px solid var(--step, var(--p4));
  min-width: 0;
}
.ramp-step .glyph { font-size: var(--t-11); }
.ramp-step .n {
  font-family: var(--f-mono);
  font-size: var(--t-11);
  letter-spacing: .06em;
  color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ramp-cap {
  font-size: var(--t-11);
  line-height: 1.6;
  color: var(--ink-faint);
  max-width: 52ch;
}

/* ── The form half ─────────────────────────────────────────────────────── */
.auth-main {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(28px, 4.5vw, 64px);
  min-width: 0;
}
.auth-form { width: min(380px, 100%); }
.auth-form .eyebrow {
  font-family: var(--f-mono);
  font-size: var(--t-11);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 var(--sp-2);
}
.auth-form h2 {
  margin: 0 0 var(--sp-5);
  font-size: var(--t-22);
  letter-spacing: -0.01em;
}
.auth-note {
  margin-top: var(--sp-5);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--hairline);
  border-left: 2px solid var(--test);
  border-radius: var(--r);
  font-size: var(--t-11);
  line-height: 1.6;
  color: var(--ink-dim);
  background: var(--hover);
}

@media (max-width: 900px) {
  /* The form leads on a phone; identity follows it. Nobody signing in at 3am
     should have to scroll past the branding to reach the password field. */
  .auth { grid-template-columns: minmax(0, 1fr); }
  .auth-side { order: 2; border-right: 0; border-top: 1px solid var(--hairline); }
  .auth-main { order: 1; }
  .auth-lede h1 { font-size: var(--t-22); }
  .auth-side { gap: var(--sp-5); }
}
@media (max-width: 420px) {
  /* Six labelled steps do not fit; the glyph row still carries the ramp. */
  .ramp-step .n { display: none; }
}

/* ── Responsive: the rail becomes a drawer; nothing is ever cut off ───────── */

/* List-row form: the same table markup, reflowed as readable rows. The queue
   card carries it at every width (`.is-list`) because a seven-column table
   cannot fit a half-width card without clipping the State column — and any
   `.data--cards` table takes the same form under 700px. Age leads the row's
   right edge; the exact received timestamp is on the alarm page. */
table.is-list, table.is-list tbody { display: block; width: 100%; }
table.is-list thead { display: none; }
/* The row has a fixed shape, not an order of flex items. Reflowed with
   `flex-wrap` the cells landed wherever the content pushed them, so a long
   description shoved the state onto its own line and two rows never looked
   alike. A grid means every row reads in the same places:

       ▌ ◆  SITE NAME  code                      zone      2m 04s
             FIRE  E110  sprinkler flow, zone 4            pending [Ack]

   Site first because it is what an operator recognises; event and its detail on
   the second line; age hard right where the eye returns to it. */
table.is-list tbody tr {
  display: grid;
  grid-template-columns: 3px auto minmax(0, 1fr) auto auto;
  grid-template-areas:
    "gut pri site  zone  age"
    "gut pri event event state";
  align-items: baseline;
  column-gap: var(--sp-3);
  row-gap: 3px;
  padding: var(--sp-3) var(--sp-4) var(--sp-3) 0;
  border-bottom: 1px solid var(--hairline);
}
table.is-list tbody tr:last-child { border-bottom: 0; }
table.is-list tbody tr:hover { background: var(--hover); }
table.is-list tbody td {
  display: block; height: auto; padding: 0; border: 0;
  white-space: normal; min-width: 0;
}
table.is-list td.gutter {
  grid-area: gut; align-self: stretch;
  width: 3px; border: 0; border-radius: 0 2px 2px 0;
}
table.is-list td.c-pri   { grid-area: pri; }
table.is-list td.c-site  { grid-area: site; font-size: var(--t-15); }
table.is-list td.c-site strong { font-weight: 600; }
table.is-list td.c-zone  { grid-area: zone; font-size: var(--t-11); color: var(--ink-faint);
                           text-align: right; white-space: nowrap; }
table.is-list td.c-age   { grid-area: age; text-align: right; white-space: nowrap;
                           font-variant-numeric: tabular-nums; }
table.is-list td.c-event { grid-area: event; overflow-wrap: anywhere; }
table.is-list td.c-state { grid-area: state; text-align: right; white-space: nowrap; }
table.is-list td.c-received { display: none; }

/* The event line: type and code carry the weight, the description is context. */
table.is-list .event-head { display: inline-flex; gap: var(--sp-2); align-items: baseline; }
table.is-list .event-head b { font-weight: 500; }
table.is-list .event-desc {
  display: inline; margin-left: var(--sp-2); color: var(--ink-faint);
}

/* Age is the thing you triage on, so it gets the size — and only while the
   alarm is still open. A closed row does not need a big clock. */
.queue-layout tr[data-live="1"] td.c-age .age-rel { font-size: var(--t-18); color: var(--ink); }
table.is-list td.c-age { display: block; }
table.is-list .age-abs { display: block; }
table.is-list .state-by { display: block; }

/* Quick-ack sits under the state chip rather than beside it, so the chip stays
   the thing you read and the button is where your hand already is. */
table.is-list .qa { margin-top: 4px; }

@media (max-width: 640px) {
  table.is-list tbody tr {
    grid-template-columns: 3px auto minmax(0, 1fr) auto;
    grid-template-areas:
      "gut pri site  age"
      "gut pri event event"
      "gut pri zone  state";
  }
  table.is-list td.c-zone { text-align: left; }
}

.rail-toggle { display: none; }

@media (max-width: 1160px) {
  .queue-layout { grid-template-columns: minmax(0, 1fr); }
  .wall-lanes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
  .shell {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "brand" "annunciator" "main";
    grid-template-rows: var(--annunciator-h) auto minmax(0, 1fr);
  }
  /* flex-start, not space-between: with the menu button now first in the
     DOM, space-between would fling the logo to the far right and leave a
     gulf between them. Button then logo, together on the left. */
  .brand { border-right: 0; justify-content: flex-start; gap: var(--sp-3);
           position: sticky; top: 0; }
  .annunciator { position: static; }
  .rail-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; border: 1px solid var(--hairline-strong);
    border-radius: var(--r); background: var(--surface-2); cursor: pointer;
    /* No z-index games here: `.brand` is position:sticky with z-index 41, which
       makes it a stacking context, so nothing inside it can rise above the
       drawer's 60 however high you set it. On the right the button sat outside
       the 280px panel and stayed tappable; on the left the drawer covers it.
       That is fine — the button is not visible while the drawer is open, so
       nobody reaches for it — but it does mean dismissal has to come from
       somewhere, hence outside-tap and Escape in console.js. */
  }
  .rail {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(280px, 85vw);
    height: 100vh;
    top: 0;
    z-index: 60;
    padding-top: var(--sp-5);
    transform: translateX(-102%);
    transition: transform var(--fast) var(--ease);
    border-right: 1px solid var(--hairline-strong);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
  }
  .rail.is-open { transform: none; }
  .annunciator { gap: var(--sp-4); padding: 0 var(--sp-4); }
  /* The clock and the signed-in name are the first things a phone can live
     without; the lamps that carry alarm state stay. Scroll catches the rest. */
  .annunciator { overflow-x: auto; }
  .split, .grid-2, .grid-3 { grid-template-columns: minmax(0, 1fr); }
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .split > aside { position: static; }
  .view-body { padding: var(--sp-4); }
  .view-head { padding: var(--sp-4); }
  .band { padding: var(--sp-4); }
  /* The header actions wrap onto their own line rather than being clipped. */
  .view-head .actions, .band .actions { margin-left: 0; width: 100%; flex-wrap: wrap; }
  .wall-lanes { grid-template-columns: minmax(0, 1fr); padding: var(--sp-4); }
  .wall-top { padding: var(--sp-4); }
}

@media (max-width: 700px) {
  .annunciator .lamp--clock, .annunciator .tool-divider,
  .annunciator .account-id { display: none; }
  .grid-4 { grid-template-columns: minmax(0, 1fr); }
  .queue-tools .input { max-width: none; flex: 1 1 100%; }
  .queue-tools .count { margin-left: 0; }

  /* Rows become readable cards. Same markup, no duplication: the cells
     reflow, the gutter becomes the card's leading edge, nothing is clipped. */
  table.data--cards, table.data--cards tbody { display: block; width: 100%; }
  table.data--cards thead { display: none; }
  table.data--cards tbody tr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: var(--sp-3);
    row-gap: 2px;
    position: relative;
    padding: var(--sp-3) var(--sp-4);
    padding-left: calc(var(--sp-4) + 3px);
    border-bottom: 1px solid var(--hairline);
  }
  table.data--cards tbody tr:last-child { border-bottom: 0; }
  table.data--cards tbody td {
    display: block;
    height: auto;
    width: auto; max-width: none;   /* desktop column hints (width: 1%) would crush a flex item */
    padding: 0;
    border: 0;
    white-space: normal;
    min-width: 0;
  }
  /* Desktop column hints (width: 1%) are set on higher-specificity selectors;
     in the card form every cell sizes to its content. */
  table.data.data--cards tbody td, table.data.data--cards tbody td[class] { width: auto; max-width: none; }
  table.data--cards td.gutter {
    position: absolute;
    top: 0; bottom: 0; left: 0;
    width: 3px;
    border-left-width: 3px;
  }
  /* Queue card: priority · age · state on the first line, then the substance. */
  table.data--cards td.c-pri { order: 1; }
  table.data--cards td.c-age { order: 2; margin-left: auto; }
  table.data--cards td.c-state { order: 3; }
  table.data--cards td.c-site { order: 4; flex: 1 0 100%; }
  table.data--cards td.c-event { order: 5; flex: 1 0 100%; overflow-wrap: anywhere; }
  table.data--cards td.c-zone { order: 6; font-size: var(--t-11); }
  table.data--cards td.c-received { order: 7; font-size: var(--t-11); }
  /* Signals card: time · code first, then site, then what it said. */
  table.data--cards td.c-time { order: 1; }
  table.data--cards td.c-code { order: 2; margin-left: auto; }
  table.data--cards td.c-type { order: 5; }
  table.data--cards td.c-desc { order: 6; flex: 1 0 100%; overflow-wrap: anywhere; font-size: var(--t-11); color: var(--ink-faint); }
  table.data--cards td.c-state { flex: none; white-space: nowrap; }
}


/* Each option renders its own sample at its own real size — you choose by
   reading it, not by reading the word "Large". */


/* ══ WALL STATS ═══════════════════════════════════════════════════════════
   Sized for the far side of the room, not for a desk. The numerals use the
   display face at a width the type scale does not otherwise reach, and the
   labels stay small — from ten feet you read the number and the colour, and the
   word is there for whoever walks up to it. */

.wall-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
  gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-5) var(--sp-4);
}
.wstat {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  background: var(--panel);
  box-shadow: var(--shadow);
}
.wstat-n {
  font-family: var(--f-display);
  font-size: clamp(2rem, 4.2vw, 3.4rem);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.wstat-l {
  font-family: var(--f-mono);
  font-size: var(--t-11);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Colour only when there is something to say. A red 0 next to a live 40 says
   the opposite of what is true. */
.wstat.is-hot { border-color: var(--p1); background: var(--p1-bed); }
.wstat.is-hot .wstat-n { color: var(--p1); }
.wstat.is-warn .wstat-n { color: var(--p2); }
.wstat.is-test { border-color: var(--test); }
.wstat.is-test .wstat-n { color: var(--test); }

/* ── Health band ──────────────────────────────────────────────────────────
   The one thing a wall of alarm cards cannot tell you: whether the quiet is
   real. Three watchdogs and the device checks, in a row, always visible. */
.wall-health {
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  margin: 0 var(--sp-5) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  background: var(--surface);
}
.wh-item { display: flex; align-items: baseline; gap: var(--sp-2); font-size: var(--t-15); }
.wh-dot {
  width: 10px; height: 10px; border-radius: 50%; flex: none;
  background: var(--ok); box-shadow: 0 0 12px var(--ok);
  align-self: center;
}
.wh-item.is-fault .wh-dot { background: var(--p1); box-shadow: 0 0 14px var(--p1); }
.wh-item.is-unknown .wh-dot { background: var(--p4); box-shadow: none; }
.wh-name { color: var(--ink-dim); }
.wh-val { font-family: var(--f-mono); font-weight: 500; color: var(--ok); }
.wh-item.is-fault .wh-val { color: var(--p1); }
.wh-item.is-unknown .wh-val { color: var(--ink-faint); }
.wh-code { font-family: var(--f-mono); font-size: var(--t-11); color: var(--ink-faint); }

@media (max-width: 900px) {
  .wall-stats { padding: var(--sp-4); }
  .wall-health { margin: 0 var(--sp-4) var(--sp-3); }
}

/* ── Queue rows ───────────────────────────────────────────────────────────
   The queue renders as cards at every width (.is-list), not as a table — so
   column widths do not apply and setting them squashes flex items into a mess.
   These rules describe the CARD.

   Age reads relative, with the absolute time beside it for quoting. The
   description is context under the event, not the headline it had become. */

/* Only meaningful if the table is ever rendered as a real table. */
table:not(.is-list) .th-age,
table:not(.is-list) .th-state,
table:not(.is-list) td.c-age,
table:not(.is-list) td.c-state,
table:not(.is-list) td.c-zone,
table:not(.is-list) td.c-pri { width: 1%; white-space: nowrap; }

.age-rel { font-family: var(--f-mono); }
.age-abs { font-size: var(--t-11); color: var(--ink-faint); }
/* On a card the two sit side by side; stacking them made the row taller than
   its neighbours and broke the rhythm down the list. */
table.is-list td.c-age { display: flex; align-items: baseline; gap: var(--sp-2); }

.event-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.event-desc { display: block; margin-top: 1px; font-size: var(--t-11); color: var(--ink-faint); }
table:not(.is-list) .event-desc {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 46ch;
}
.state-by { display: block; font-size: var(--t-11); color: var(--ink-faint); margin-top: 1px; }
table.is-list td.c-state { display: flex; align-items: baseline; gap: var(--sp-2); }
table.is-list .state-by { display: inline; margin-top: 0; }

/* ── Filling the counter ──────────────────────────────────────────────────
   The grid stretches every tile to the tallest. The label / numeral / meta
   block anchors at the top of every tile, so the numerals sit on one line
   across the row; whatever a tile carries beneath (split bars, the oldest
   waiting, a sparkline) takes the slack at the bottom. */
.counter { justify-content: flex-start; }
.counter .mini-split, .counter .kpi-aside { margin-top: auto; }

/* The split bars are a second block of information, not a continuation of the
   meta line — they need the separation to read as one. */
.mini-split {
  list-style: none; display: flex; flex-direction: column; gap: var(--sp-2);
  margin-top: var(--sp-4); padding-top: var(--sp-3);
  border-top: 1px solid var(--hairline);
}
.mini-split li { display: grid; grid-template-columns: 30px minmax(0, 1fr) 24px;
                 align-items: center; gap: var(--sp-3); }
.ms-lab .glyph { font-size: var(--t-11); }
.ms-track { height: 6px; border-radius: var(--r-pill); background: var(--hover); overflow: hidden; }
.ms-fill { display: block; height: 100%; border-radius: var(--r-pill); background: var(--p4); }
.mini-split .p1 .ms-fill { background: var(--p1); }
.mini-split .p2 .ms-fill { background: var(--p2); }
.mini-split .p3 .ms-fill { background: var(--p3); }
.mini-split .is-zero .ms-n { color: var(--ink-faint); }
.ms-n { font-size: var(--t-11); color: var(--ink-dim); text-align: right; }

.kpi-aside { margin-top: var(--sp-3); padding-top: var(--sp-3);
             border-top: 1px solid var(--hairline); display: flex;
             align-items: baseline; gap: var(--sp-2); }
.kpi-aside-v { font-size: var(--t-15); color: var(--ink); font-variant-numeric: tabular-nums; }

/* Hooks used by the queue markup. */
.queue-table { width: 100%; }
.ua-assign, .ua-block { display: flex; flex-direction: column; gap: var(--sp-1); }

/* ── Map links ────────────────────────────────────────────────────────────
   An address is a place, so it behaves like one. The arrow marks that it
   leaves the console — an operator following it mid-alarm gets a new tab, not
   a lost alarm. */
.maplink { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--hairline-strong); }
.maplink:hover { color: var(--accent); border-bottom-color: var(--accent); }
.maplink:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.maplink-i { font-size: 0.85em; margin-left: 3px; opacity: .6; }
.maplink:hover .maplink-i { opacity: 1; }

/* ── Login: Microsoft Entra sign-in (shown only when configured + readable) ── */
.sso-split { display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-4) 0 var(--sp-3); color: var(--ink-faint); font-size: var(--t-11); text-transform: uppercase; letter-spacing: .08em; }
.sso-split::before, .sso-split::after { content: ""; flex: 1; border-top: 1px solid var(--hairline); }
.sso-btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); }


/* ── Dialogs ──────────────────────────────────────────────────────────────
   A dialog is one action with room to read it: a 560px card, a plain header,
   and a vertical rhythm the CSS owns — the markup carries no spacing classes,
   so every dialog breathes the same way. Controls are 44px+ because the hand
   on them belongs to an operator under pressure, possibly on glass. */
.dlg { position: fixed; inset: 0; z-index: 80; display: grid; place-items: center; padding: var(--sp-4); }
.dlg-backdrop { position: absolute; inset: 0; background: rgba(8,4,24,.72);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
.dlg-box {
  position: relative; width: 100%; max-width: 560px; max-height: 90dvh; overflow: auto;
  background: var(--panel); border: 1px solid var(--hairline-strong);
  border-radius: var(--r-lg); box-shadow: var(--shadow-lg);
}
.dlg-head { display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-5) var(--sp-5) var(--sp-4); }
.dlg-head h3 { font-size: var(--t-20, 1.25rem); font-weight: 650; letter-spacing: -.02em; }
.dlg-head p { margin-top: 4px; color: var(--ink-dim); font-size: var(--t-13); max-width: 44ch; }
.dlg-head .icon-btn { margin-left: auto; flex: none; }
.dlg-body { padding: 0 var(--sp-5) var(--sp-5); }
body.has-modal { overflow: hidden; }

/* The rhythm: a label opens a field group; whatever follows sits close under
   it; the next label brings the next group after a full beat. */
.dlg-body .field-label { margin: var(--sp-5) 0 var(--sp-2); }
.dlg-body > .field-label:first-child,
.dlg-body form > .field-label:first-child { margin-top: 0; }
.dlg-body .input, .dlg-body .select, .dlg-body textarea.input {
  width: 100%; min-height: 44px; font-size: var(--t-15); padding: 0 var(--sp-3);
}
.dlg-body textarea.input { padding: var(--sp-2) var(--sp-3); min-height: 0; line-height: 1.5; }
.dlg-body .check { display: flex; align-items: flex-start; gap: var(--sp-2);
  padding: var(--sp-2) 0; font-size: var(--t-15); }
.dlg-body .check input { margin-top: 4px; }
.dlg-body .btn.is-primary.is-block { margin-top: var(--sp-5); min-height: 48px; font-size: var(--t-15); }
.dlg-body > a.btn.is-primary.is-block:first-child { margin-top: 0; min-height: 52px; }
.dlg-body .faint { margin-top: var(--sp-3); }

.btn.is-block { display: flex; width: 100%; justify-content: center; }

/* Outcomes: seven honest answers, none clipped. The pills grow to their text
   and wrap to two lines rather than spilling out of their border. */
.outcome-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
  gap: var(--sp-2); }
.outcome-btn { min-height: 46px; height: auto; padding: var(--sp-2) var(--sp-3);
  justify-content: center; text-align: center; white-space: normal; line-height: 1.25;
  font-size: var(--t-14, 0.875rem); }
.outcome-btn:hover { border-color: var(--accent); background: var(--accent-soft); }
.faint.sm { font-size: var(--t-11); }

/* No JavaScript: dialogs are ordinary sections at the foot of the page. */
html:not(.has-dialogs) .dlg { position: static; display: block; padding: 0; margin-bottom: var(--sp-4); }
html:not(.has-dialogs) .dlg[hidden] { display: block; }
html:not(.has-dialogs) .dlg-backdrop { display: none; }
html:not(.has-dialogs) .dlg-box { max-width: none; max-height: none; }
html:not(.has-dialogs) .dialogs { padding: 0 var(--sp-5) var(--sp-6); }

/* ── Dialogs on a phone: a bottom sheet, not a floating card ───────────── */
@media (max-width: 640px) {
  html.has-dialogs .dlg { place-items: end stretch; padding: 0; }
  html.has-dialogs .dlg-box {
    max-width: none; max-height: 92dvh;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    border-bottom: 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
}


/* ── Menu: a button that opens a small list of choices ─────────────────── */
.menu { position: relative; }
.menu > summary { list-style: none; cursor: pointer; user-select: none; }
.menu > summary::-webkit-details-marker { display: none; }
.menu[open] > summary { background: var(--accent-fill-hover, var(--accent)); }
.menu-pop {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 60; min-width: 240px;
  background: var(--panel); border: 1px solid var(--hairline-strong);
  border-radius: var(--r-lg); box-shadow: var(--shadow-lg); padding: 6px; display: flex; flex-direction: column;
}
.menu-pop a { display: flex; flex-direction: column; gap: 1px; padding: 8px 10px;
  border-radius: var(--r-sm); color: inherit; text-decoration: none; }
.menu-pop a:hover { background: var(--hover); }
.menu-pop a b { font-size: var(--t-13); }
.menu-pop a span { font-size: var(--t-11); color: var(--ink-faint); }

/* ── Command palette ──────────────────────────────────────────────────────
   Ctrl+K. Sits above everything, centred near the top so the results grow
   downward into the eye's path rather than pushing the input around. Uses the
   same surface/line tokens as the rest of the console so it inherits both
   themes without a second palette of its own. */
.kbd-hint { margin-left: 6px; font-size: var(--t-11); color: var(--ink-faint); }
@media (max-width: 900px) { .kbd-hint { display: none; } }

.palette { position: fixed; inset: 0; z-index: 90; display: grid;
  align-items: start; justify-items: center; padding: 10vh 16px 16px; }
.palette-scrim { position: absolute; inset: 0; background: rgba(6, 2, 24, 0.55);
  backdrop-filter: blur(2px); }
.palette-card { position: relative; width: min(640px, 100%);
  background: var(--surface-2); border: 1px solid var(--line-2);
  border-radius: var(--r-lg, 14px); box-shadow: var(--shadow-lg); overflow: hidden; }
.palette-input { width: 100%; padding: 16px 18px; border: 0; outline: 0;
  background: transparent; color: var(--ink); font: inherit;
  font-size: var(--t-15, 1rem); border-bottom: 1px solid var(--line); }
.palette-input::placeholder { color: var(--ink-faint); }

.palette-results { list-style: none; margin: 0; padding: 6px;
  max-height: min(52vh, 420px); overflow-y: auto; }
.palette-item { display: flex; gap: 10px; align-items: baseline; padding: 9px 12px;
  border-radius: var(--r-sm); cursor: pointer; }
.palette-item.is-active { background: var(--hover); }
.palette-kind { flex: none; min-width: 6ch; font-size: var(--t-11);
  color: var(--ink-faint); text-transform: uppercase; letter-spacing: .04em; }
.palette-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.palette-text b { font-size: var(--t-13); }
.palette-sub { font-size: var(--t-11); color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.palette-foot { display: flex; gap: 14px; padding: 8px 14px;
  border-top: 1px solid var(--line); font-size: var(--t-11); }

/* The palette is a jump, not an animation. Nothing here depends on motion. */

/* ── Responsive: rows become cards at phone width ──────────────────────────
   CSS only — the same markup, re-flowed. Cell labels come from data-th, the
   priority gutter becomes the card's left edge, and nothing scrolls sideways.

   This lives in app.css, which every page loads, and NOT in a page-scoped
   stylesheet. It used to live in estate.css: the identical markup became cards
   on the pages that happened to load that file and stayed a sideways-scrolling
   table everywhere else. On the alarm page that put a signal's "on test"
   status off the right-hand edge on a phone, so a screen full of test traffic
   read as a screen full of intruder alarms. A table that scrolls sideways can
   hide the one word that changes what it means. */
@media (max-width: 640px) {
  .table-wrap:has(> table.cards) { overflow-x: visible; }
  table.cards, table.cards tbody, table.cards tr, table.cards td { display: block; }
  table.cards thead { display: none; }
  table.cards tbody tr {
    position: relative;
    border: 1px solid var(--hairline);
    border-radius: var(--r);
    background: var(--surface);
    box-shadow: var(--shadow);
    margin: 0 var(--sp-4) var(--sp-2);
    padding: var(--sp-2) var(--sp-3) calc(var(--sp-2) + 1px);
  }
  .view-body:not(.flush) table.cards tbody tr,
  .panel table.cards tbody tr { margin-left: var(--sp-3); margin-right: var(--sp-3); box-shadow: none; }
  table.cards tbody tr:last-child td { border-bottom: 0; }
  /* The base table sets nowrap with tbody in the selector, which outranks a
     plain `table.cards td` — match it, or long site names push the page wide. */
  /* Tighter than the surrounding page on purpose. A card is one table ROW,
     and six label/value pairs at body line-height came to ~205px per row —
     the audit log's 204 rows were 41,820px of it. Nothing is dropped: on a
     phone the whole point of the card is that no field goes off the right
     edge, so the saving comes from the leading and the label, not the data.
     Paired with real pagination in gm8/paging.py. */
  table.cards td, table.data.cards tbody td, table.data.cards tbody td[class] {
    height: auto;
    min-height: 0;
    border: 0;
    padding: 1px 0;
    line-height: 1.35;
    white-space: normal;
    overflow-wrap: anywhere;
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
  }
  table.cards td[data-th]::before {
    content: attr(data-th);
    flex: 0 0 78px;
    font-size: var(--t-11);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-weight: 600;
    color: var(--ink-faint);
  }
  table.cards td:empty:not(.gutter) { display: none; }
  /* The 3px priority edge survives as the card's own left edge. */
  table.cards td.gutter {
    display: block;
    position: absolute;
    top: -1px; bottom: -1px; left: -1px;
    width: 0;
    border-left-width: 3px;
    border-bottom: 0;
    border-top-left-radius: var(--r);
    border-bottom-left-radius: var(--r);
  }
  table.cards .truncate { max-width: none; white-space: normal; }

  /* Page stylesheets pin some cells to one line or a fixed share of the table
     width — right for a real table, wrong for a card, where they push the row
     wider than the screen and put the far cell out of reach. Neutralised here
     rather than in each page's file, so a new page-scoped width rule cannot
     quietly reintroduce a sideways-scrolling card.
     One element deeper than the rules it overrides, so file order is
     irrelevant. */
  table.cards td.th-narrow,
  table.cards td.cell-clip,
  table.cards td.cell-clip-sm,
  table.cards td.cell-wrap,
  table.cards td[data-th] {
    width: auto;
    max-width: none;
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
  }
  /* Row actions are a button group: on a card they wrap onto the next line
     rather than running off the edge. Same for any inline form in a cell —
     the users table's role picker is a fixed-width select plus a button, which
     together are wider than a phone. */
  table.cards .row-actions,
  table.cards td form { flex-wrap: wrap; }
  table.cards td form .select,
  table.cards td form .input { width: auto; min-width: 0; flex: 1 1 8rem; }
