/* ═══════════════════════════════════════════════════════════════════
 *  Helios VMS — Components
 *  ─────────────────────────────────────────────────────────────────
 *  Reusable component patterns. Every alive moment on every Helios
 *  page is built from these primitives. When designing a new page,
 *  consume these first — only add page-specific CSS for things that
 *  truly are unique to that page.
 *
 *  Requires: tokens.css, animations.css (load in that order).
 *
 *  Component groups:
 *    1. Reset + base
 *    2. Buttons
 *    3. Status pills, badges, tags
 *    4. Cards
 *    5. Modals + toasts
 *    6. Form controls
 *    7. Sticky bars + banners
 *    8. Empty states + loading
 *    9. Camera tile primitives
 *   10. Navigation + topbar
 * ═════════════════════════════════════════════════════════════════ */
@import url("health-pill.css");


/* ── 1. Reset + base ─────────────────────────────────────────── */

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

html, body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

/* ── 2. Buttons ──────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  color: var(--fg-1);
  font-family: inherit;
  font-size: var(--text-md);
  font-weight: 500;
  cursor: pointer;
  transition: all var(--t-base) var(--ease);
}
.btn:hover {
  border-color: var(--line-3);
  background: var(--surface-2);
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.btn svg {
  width: 13px;
  height: 13px;
  stroke-width: 2;
}

/* ── Focus: ONE ring, and it is the accent's job (2026-09-23) ──────
 * Keyboard focus is ACTION, so it is orange — and it is the only orange
 * ring in the product: selection is a neutral ring, attention an
 * amber-yellow inset. :where() keeps the specificity at zero, so a
 * component that draws its own focus treatment still wins, while any
 * element that draws none gets this instead of the UA default. */
:where(:focus-visible) {
  outline: var(--focus-w) solid var(--focus);
  outline-offset: var(--focus-offset);
}

.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  /* --accent-ink, never a literal: the ink pair is what the contrast
     gate checks (check_contrast.py, on --accent AND --accent-hover).
     The brand's web pair — white on the brand orange — is 3.35:1 and fails AA,
     so the label is the brand INK in both themes (5.4:1 light, 8.4:1
     dark), and the hover stops at the darkest orange that keeps it
     above 4.5. */
  color: var(--accent-ink);
}
.btn.primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

.btn.danger {
  /* a TEXT-BEARING danger fill uses the strong crimson: the label is
     5.9:1 there and only 4.1:1 on the base crimson in dark */
  background: var(--bad-strong);
  border-color: var(--bad-strong);
  color: var(--bad-ink);
}
.btn.danger:hover {
  background: var(--bad-strong);
  border-color: var(--bad-strong);
}

.btn.ghost {
  background: transparent;
  border-color: var(--line-2);
}
.btn.ghost:hover {
  background: var(--surface-3);
}

/* Compact icon-only button — for tile overlays, card hover actions */
.icon-btn {
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  color: var(--fg-2);
  cursor: pointer;
  transition: all var(--t-fast) var(--ease);
}
.icon-btn:hover {
  background: var(--surface-3);
  color: var(--fg);
  border-color: var(--line-3);
}
.icon-btn svg {
  width: 12px;
  height: 12px;
  stroke-width: 1.8;
}

/* Glass icon button — for camera tile overlays where we need
 *  to read against video content */
.icon-btn.glass {
  background: var(--cam-glass);
  border-color: color-mix(in srgb, var(--cam-tint) 10%, transparent);
  color: color-mix(in srgb, var(--cam-tint) 85%, transparent);
}
.icon-btn.glass:hover {
  background: color-mix(in srgb, var(--cam-tint) 10%, transparent);
  border-color: color-mix(in srgb, var(--cam-tint) 20%, transparent);
  color: var(--cam-fg);
}

/* ── 2.5. Deterrent buttons — armed/active/cooling states ─────
 *  Talk, Strobe, Siren use these. They have THREE visual states:
 *    default — "click to arm"
 *    .armed  — "I am hot, click again to fire" (warning glow)
 *    .active — "I am firing right now" (strong color, sustained)
 *  Will be wired to real backends in future sessions. */

.btn-deter {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  color: var(--fg-1);
  font-family: inherit;
  font-size: var(--text-md);
  font-weight: 500;
  cursor: pointer;
  transition: all var(--t-base) var(--ease);
}
.btn-deter:hover {
  border-color: var(--line-3);
  background: var(--surface-2);
}
.btn-deter svg {
  width: 14px;
  height: 14px;
  stroke-width: 1.8;
}

/* Talk — the deterrence family (talk-down IS deterrence), never the
   accent: armed and active are STATES of the deterrent */
.btn-deter.talk.armed {
  color: var(--fg);
  border-color: var(--evt-deterrence);
  background: var(--evt-deterrence-soft);
}
.btn-deter.talk.active {
  background: var(--evt-deterrence-soft);
  border-color: var(--evt-deterrence);
  color: var(--fg);
  box-shadow: 0 0 16px color-mix(in srgb, var(--evt-deterrence) 45%, transparent);
}

/* Strobe — warning amber, with pulse when active */
.btn-deter.strobe.armed {
  color: var(--warn-strong);
  border-color: var(--warn-line);
  background: var(--warn-soft);
}
.btn-deter.strobe.active {
  background: var(--warn);
  border-color: var(--warn);
  color: var(--warn-ink);
  animation: pulse 0.5s ease-in-out infinite;
}

/* Siren — bad red, strongest visual state */
.btn-deter.siren.armed {
  color: var(--bad-strong);
  border-color: var(--bad-line);
  background: var(--bad-soft);
}
.btn-deter.siren.active {
  background: var(--bad-strong);
  border-color: var(--bad-strong);
  color: var(--bad-ink);
  box-shadow: 0 0 20px var(--rec-glow);
  animation: rec-pulse 0.8s ease-in-out infinite;
}

/* Coming-soon overlay — for features whose backend isn't wired yet.
 *  Apply alongside .btn-deter or any button. PRESENT, STATED, NOT
 *  CLICKABLE (ABSENT vs UNUSABLE): a dashed frame and legible ink —
 *  never an opacity wash, which dragged the label under 4.5:1. Pairs
 *  with a title="Coming Q3 2026" tooltip. */
.btn.coming-soon,
.btn-deter.coming-soon {
  cursor: not-allowed;
  position: relative;
  background: transparent;
  color: var(--fg-2);
  border: 1px dashed var(--line-strong);
  box-shadow: none;
}
.btn.coming-soon::after,
.btn-deter.coming-soon::after {
  content: 'soon';
  position: absolute;
  top: -6px;
  right: -6px;
  padding: 1px 5px;
  background: var(--fg-3);
  color: var(--surface);
  border-radius: var(--r-pill);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ── 3. Status pills, badges, tags ────────────────────────────
 *  Three distinct concepts:
 *    .pill    — large, semantic, shows status ("All systems healthy")
 *    .badge   — small, on dark or video backgrounds (REC, PTZ, NEW)
 *    .tag     — inline next to text, signals capability (PTZ, AI, AAC)
 */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 8px;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--fg-1);
}
.pill .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fg-3);
}
.pill.ok {
  background: var(--ok-soft);
  border-color: var(--ok-line);
  color: var(--ok-text);
}
.pill.ok .dot {
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--ok-soft);
  animation: pulse 2s var(--ease) infinite;
}
.pill.warn {
  background: var(--warn-soft);
  border-color: var(--warn-line);
  color: var(--warn-text);
}
.pill.warn .dot { background: var(--warn); }
.pill.bad {
  background: var(--bad-soft);
  border-color: var(--bad-line);
  color: var(--bad-text);
}
.pill.bad .dot { background: var(--bad); }
.pill.scanning {
  background: var(--info-soft);
  border-color: var(--info-line);
  color: var(--info-text);
}
.pill.scanning .dot {
  background: var(--info);
  animation: pulse 1.2s var(--ease) infinite;
}

/* Small badge for video overlays. A solid --cam-glass chip (>= .75, the
 *  §4 floor) so it stays readable on any feed without a backdrop blur. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  background: var(--cam-glass);
  border: 1px solid color-mix(in srgb, var(--tint) 8%, transparent);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: color-mix(in srgb, var(--tint) 85%, transparent);
}
.badge.rec {
  border-color: var(--bad-line);
  color: var(--bad-text);
}
.badge.rec .dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--rec);
  box-shadow: 0 0 6px var(--rec-glow);
  animation: rec-pulse 1.4s var(--ease) infinite;
}

/* NEW badge — celebration for freshly-added cameras */
.badge.new-badge {
  background: var(--info);
  color: var(--info-ink);
  border: none;
  font-weight: 700;
  letter-spacing: 0.08em;
  box-shadow: 0 0 12px color-mix(in srgb, var(--info) 55%, transparent);
  animation: new-pulse 2s ease-in-out infinite;
}
.badge.new-badge .sparkle {
  display: inline-block;
  width: 6px;
  height: 6px;
  background: var(--info-ink);
  border-radius: 50%;
  box-shadow:
    -3px -3px 0 -1.5px var(--info-ink),
     3px -3px 0 -1.5px var(--info-ink),
    -3px  3px 0 -1.5px var(--info-ink),
     3px  3px 0 -1.5px var(--info-ink);
  animation: sparkle-spin 1.6s linear infinite;
}

/* Inline capability tag */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 6px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  color: var(--fg-2);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
/* category tags: the border carries the category (>= 3:1), the label
   stays neutral ink so it clears the text law on every tint */
.tag.audio {
  color: var(--fg-1);
  border-color: var(--evt-audio);
  background: var(--evt-audio-soft);
}
.tag.ptz {                 /* a capability, not a state: neutral */
  color: var(--fg-2);
  border-color: var(--line-2);
  background: var(--surface-3);
}
.tag.ai {
  color: var(--info-text);
  border-color: var(--info-line);
  background: var(--info-soft);
}

/* ── 4. Cards ─────────────────────────────────────────────────
 *  Generic card primitive. Use for camera tiles, event rows,
 *  discovery devices, any "this is a thing" UI surface. */

.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  transition:
    transform var(--t-slow) var(--ease-out-expo),
    box-shadow var(--t-base) var(--ease);
}
.card:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow);
}
.card.selected {
  box-shadow: 0 0 0 2px var(--select);   /* selection is a NEUTRAL ring */
}

/* Fresh / just-added card — celebrates for 5 minutes after
 *  the camera/device was added */
.card.fresh,
.cam.fresh {
  border: none;
  box-shadow: 0 0 0 1.5px var(--info), var(--shadow);   /* "new" is info */
  animation: fresh-breathe 3.2s ease-in-out infinite;
}
.card.fresh:hover,
.cam.fresh:hover {
  box-shadow:
    0 0 0 1.5px var(--info),
    0 0 32px 6px var(--info-line),
    var(--shadow-lg);
}

/* Highlight pulse — short attention-grab on a specific card */
.card.highlight,
.cam.highlight {
  animation: highlight-pulse 2.2s var(--ease) 0s 2;
}

/* ── 5. Modals + toasts ──────────────────────────────────────── */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: var(--dim);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 50;
}
.modal-backdrop.show {
  display: flex;
  animation: fade-in 0.18s var(--ease);
}
.modal {
  width: 100%;
  max-width: 420px;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: modal-in 0.28s var(--ease-spring);
}
.modal-head {
  padding: 16px 18px 12px;
}
.modal-title {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--fg);
}
.modal-body {
  padding: 0 18px 18px;
  font-size: var(--text-base);
  color: var(--fg-2);
  line-height: 1.5;
}
.modal-body b {
  color: var(--fg);
  font-weight: 600;
}
.modal-foot {
  padding: 12px 18px;
  border-top: 1px solid var(--line);
  background: var(--bg);
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

.toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%) translateY(140px);
  padding: 10px 16px;
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  color: var(--fg);
  border-radius: var(--r);
  font-size: var(--text-base);
  font-weight: 500;
  box-shadow: var(--shadow-lg);
  transition: transform var(--t-slow) var(--ease-spring);
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 8px;
}
.toast .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ok);
}
.toast.show {
  transform: translateX(-50%) translateY(0);
}
.toast.error .dot { background: var(--bad); }
.toast.warn .dot  { background: var(--warn); }

/* ── 6. Form controls ────────────────────────────────────────── */

.input,
.input-wrap input,
.input-wrap textarea {
  width: 100%;
  padding: 8px 10px;
  background: var(--surface-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r);
  color: var(--fg);
  font-family: inherit;
  font-size: var(--text-md);
  transition: all var(--t-fast) var(--ease);
}
.input::placeholder,
.input-wrap input::placeholder {
  color: var(--fg-3);
}
.input:focus,
.input-wrap input:focus {
  outline: none;
  border-color: var(--focus);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--focus-soft);
}

/* ── 7. Sticky bars + banners ────────────────────────────────
 *  Sticky bulk-action bar (slides up when something is selected)
 *  and pulsing CTA banner (one-click easy wins). */

.bulk-bar {
  position: fixed;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%) translateY(140px);
  width: calc(100% - 48px);
  max-width: 720px;
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  padding: 12px 14px;
  display: flex;
  align-items: center;
  gap: 12px;
  z-index: 90;
  transition: transform var(--t-slow) var(--ease-spring);
}
.bulk-bar.show {
  transform: translateX(-50%) translateY(0);
}

/* CTA banner — the "✨ N ready, one click" pattern */
.cta-banner {
  width: 100%;
  margin-bottom: 14px;
  padding: 14px 18px;
  background: var(--surface);
  border: 1.5px solid var(--accent-mark);   /* a CTA: action */
  border-radius: var(--r-lg);
  color: var(--fg);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 14px;
  animation: cta-breathe 3s ease-in-out infinite;
  transition: transform var(--t-base) var(--ease), box-shadow var(--t-base) var(--ease);
}
.cta-banner:hover {
  transform: translateY(-1px);
  box-shadow:
    0 0 0 4px var(--accent-soft),
    0 0 28px 8px var(--accent-line);
  animation: none;
}
.cta-banner .ring {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--accent-soft);
  display: grid;
  place-items: center;
  flex-shrink: 0;
  color: var(--accent-mark);
}
.cta-banner .cta-headline {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--fg);
  letter-spacing: -0.01em;
}
.cta-banner .cta-sub {
  font-size: var(--text-md);
  color: var(--fg-2);
}

/* ── 8. Empty states + loading ──────────────────────────────── */

.empty-state {
  padding: 80px 24px;
  text-align: center;
  color: var(--fg-2);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.empty-state svg {
  width: 44px;
  height: 44px;
  color: var(--fg-3);
  margin-bottom: 14px;
  stroke-width: 1.4;
}
.empty-state .title {
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--fg);
}
.empty-state .sub {
  font-size: var(--text-base);
  max-width: 380px;
}

.loading-state {
  padding: 60px 24px;
  text-align: center;
  color: var(--fg-2);
  font-size: var(--text-base);
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 1.5px solid var(--fg-3);
  border-top-color: var(--fg-1);   /* activity is state, not action */
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

/* ── 9. Camera tile primitives ────────────────────────────────
 *  Used on Live View, Cameras page, Discovery (placeholder
 *  tiles). The video frame is always dark, regardless of theme. */

.cam-tile,
.cam {
  position: relative;
  background: var(--cam-bg);
  border-radius: var(--r-tile);
  overflow: hidden;
  box-shadow: var(--shadow);
  isolation: isolate;
  transition:
    transform var(--t-slow) var(--ease-out-expo),
    box-shadow var(--t-base) var(--ease);
}
.cam-tile:hover,
.cam:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-lg);
}

/* Hairline edge so the tile reads cleanly against light bg */
.cam-tile::after,
.cam::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--shade) 4%, transparent) inset,
    0 -1px 0 0 color-mix(in srgb, var(--cam-tint) 4%, transparent) inset;
  z-index: 4;
}

/* Top + bottom gradient overlays so badges + name are legible */
.cam-top {
  position: absolute;
  top: 0; left: 0; right: 0;
  padding: 10px 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  z-index: 5;
  background: linear-gradient(180deg, color-mix(in srgb, var(--shade) 50%, transparent), transparent);
  pointer-events: none;
}
.cam-bottom {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 10px 12px;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 8px;
  z-index: 5;
  background: linear-gradient(0deg, color-mix(in srgb, var(--shade) 55%, transparent), transparent);
  pointer-events: none;
}

/* ── 10. Navigation + topbar ─────────────────────────────────── */

.topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  height: var(--topbar-h);
  padding: 0 18px 0 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  position: relative;   /* anchor for the centered search */
  z-index: 60;          /* above the toolbar reveal hot-zone (z-49) */
}

.crumbs {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--fg-2);
  font-size: var(--text-md);
}
.crumbs .sep {
  color: var(--fg-3);
  font-size: var(--text-sm);
}
.crumbs .here {
  color: var(--fg);
  font-weight: 500;
}

/* ═══════════════════════════════════════════════════════════════════
 *  Canonical .rail sidebar (B.3.5a)
 *  ─────────────────────────────────────────────────────────────────
 *  64px-footprint left rail that reveals a 224px panel on hover.
 *  Pages that link this sheet AND {% include "partials/_sidebar.html" %}
 *  get the rail rendered consistently.
 * ═════════════════════════════════════════════════════════════════ */

.rail {
  /* THE PANEL IS LAID OUT ONCE (2026-10-16, the owner's /live recording).
     The box is always 224px; the -160px margin keeps its layout footprint
     at 64px, so page content never reflows. Expanding changes NO layout
     property: .rail-surface slides in on the compositor (transform),
     labels fade (opacity), and everything wider than the 64px column — the
     active pill, hover fills, hairlines, badges — either fades or tracks
     the surface edge (the transform-origin:left lines and the ::after
     fills below). clip-path bounds paint AND hit-testing to the column at
     rest; it opens at the START of an expand and closes at the END of a
     collapse and never animates (Chromium does not composite clip-path).
     WHY: animating width + margin-right under a 26px backdrop blur over
     /live's 42-tile wall dropped 16 of 24 frames per open — the first
     painted frame was already 77% open (docs/DESIGN.md §5h).
     The surface is OPAQUE: the expanded rail floats over the video wall,
     and a menu over the wall is solid (§5h, 2026-10-11). */
  --rail-open: .32s cubic-bezier(.16, 1, .3, 1);
  /* collapse starts slow, so the labels (gone in .1s) are never left
     standing over video after the surface has slid out from under them */
  --rail-close: .24s cubic-bezier(.5, 0, .75, 0);
  display: flex;
  flex-direction: column;
  padding: 12px 8px;
  gap: 2px;
  z-index: 140;   /* above page topbars (rail expansion is an overlay) */
  isolation: isolate;
  width: 224px;
  margin-right: -160px;             /* footprint stays 64px — overlay, not reflow */
  clip-path: inset(0 160px 0 0);
  transition: clip-path 0s linear .24s;   /* collapse: close after the slide */
  flex-shrink: 0;
  position: sticky;
  top: 0;
  height: 100vh;
}
.rail[data-expanded="1"] {
  clip-path: inset(0 -64px 0 0);    /* -64px: room for the surface's shadow */
  transition: clip-path 0s;
}
.rail-surface {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--surface);
  border-right: 1px solid var(--line);
  pointer-events: none;
  transform: translateX(-160px);
  transition: transform var(--rail-close);
}
.rail-surface::after {               /* the shadow fades, it never paints in */
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transition: opacity var(--rail-close);
}
.rail[data-expanded="1"] .rail-surface { transform: none; transition: transform var(--rail-open); }
.rail[data-expanded="1"] .rail-surface::after { opacity: 1; transition: opacity var(--rail-open); }

/* Things WIDER than the 64px column track the surface edge instead of
   appearing at full width the moment the clip opens: a line scaled from the
   left edge (48px of 208 at rest — the collapsed column), and the btn/group
   fill (::after below) translated by the same 160px. Same duration and
   curve as the surface, so each edge stays 8px inside it. */
.rail-sep, .rail-logo::after, .rail-footer::before,
.rail-scroll::before, .rail-scroll::after {
  transform-origin: left center;
  transition: transform var(--rail-close);
}
.rail-sep { transition: transform var(--rail-close), opacity .1s; }
.rail-logo::after, .rail-footer::before, .rail-scroll::before, .rail-scroll::after {
  transform: scaleX(.2308);         /* 48 / 208 */
}
.rail-sep { transform: scaleX(.1489); }   /* 28 / 188 (10px margins) */
.rail[data-expanded="1"] .rail-sep,
.rail[data-expanded="1"] .rail-logo::after,
.rail[data-expanded="1"] .rail-footer::before {
  transform: none;
  transition: transform var(--rail-open);
}
.rail[data-expanded="1"] .rail-sep { transition: transform var(--rail-open), opacity .2s .06s; }

/* The rail owns the logo (DESIGN.md §5 law 1 + Brand): ONE lockup, full at
   26px — "Helios", the macro omits "VMS" under a 32px mark — rendered by
   partials/helios_lockup.html; rules in /static/css/brand.css, inks in
   tokens.css. The mark is currentColor, so the anchor
   sets the brand ink and mark + "Helios" match in both themes. The
   component carries its own 14% clear-space padding (3.64px at 26px) —
   the 5px here puts the mark's centre at x≈29.6, on the rail-btn icon
   column (8 + 12 + 19/2). No drop-shadow, no filter, no radius on the mark. */
.rail-logo {
  display: flex;
  align-items: center;
  padding: 0 5px 9px;               /* 8px + the 1px hairline drawn by ::after */
  margin-bottom: 8px;
  position: relative;
  flex-shrink: 0;
  overflow: hidden;
  text-decoration: none;
  color: var(--lockup-ink);
}
.rail-logo::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--line);
}
/* collapsed rail = mark only: the WORDMARK of the one full render fades
   out (it is clipped by the 64px rail either way), the mark stays put.
   EVERY rail fade is opacity alone: .1s out, .22s in after .05s. */
.rail-logo .helios-lockup-wordmark {
  opacity: 0;
  transition: opacity .1s;
}
.rail[data-expanded="1"] .rail-logo .helios-lockup-wordmark { opacity: 1; transition: opacity .22s .06s; }

.rail-section {
  position: relative;
  padding: 10px 12px 4px;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .12em;
  /* QUIET, not accent (§5f rule 1 / rail-life 2026-08-08): the one accent
     instance visible at rest is the active page. A labeled section in
     accent would compete with it on every hover-expand. */
  color: var(--fg-3);
  white-space: nowrap;
  overflow: hidden;
  /* The heading's row is RESERVED in both states (it used to grow from
     height 0, which re-laid out the rail every frame and pushed every icon
     below it down mid-expand). The icon column now never moves. */
}
/* The WORDS fade (opacity on the span); the row itself stays, so it can
   carry the collapsed-state separator below. */
.rail-section > span { opacity: 0; transition: opacity .1s; }
.rail[data-expanded="1"] .rail-section > span { opacity: 1; transition: opacity .2s .06s; }
/* Collapsed, the reserved row reads as the group separator (the owner,
   2026-10-17): a hairline in the 64px column, where .rail-sep draws its
   own, fading out as the heading fades in. Never wider than the column,
   so nothing here has to track the surface. */
.rail-section::before {
  content: '';
  position: absolute;
  left: 10px;
  width: 28px;
  top: 50%;
  height: 1px;
  background: var(--line);
  transition: opacity .1s;
}
.rail[data-expanded="1"] .rail-section::before { opacity: 0; transition: opacity .2s .06s; }
/* A .rail-sep directly above a heading row would draw a second line 20px
   from the row's own while collapsed: it steps aside until the rail opens. */
.rail .rail-sep:has(+ .rail-section) { opacity: 0; }
.rail[data-expanded="1"] .rail-sep:has(+ .rail-section) { opacity: 1; }

/* Cluster separator — visible in BOTH rail states, so the icon-only rail
   still reads as grouped rather than one undifferentiated stack. */
.rail-sep {
  height: 1px;
  flex-shrink: 0;
  margin: 6px 10px;
  background: var(--line);
}

.rail-btn {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 42px;
  min-width: 42px;
  padding: 0 12px;
  border-radius: 11px;
  color: var(--fg-2);
  cursor: pointer;
  transition: color .18s cubic-bezier(.22, 1, .36, 1);
  border: none;
  background: none;
  font-family: inherit;
  position: relative;
  isolation: isolate;
  white-space: nowrap;
  overflow: hidden;
  text-decoration: none;
}
/* The fill (hover tint / active pill) is the ::after, not the background:
   the btn is 208px wide in both states, and a background would flash out
   to full width over the video the moment the clip opens. The fill is
   shifted 160px left at rest — the btn's own rounded overflow clips it to
   the collapsed 48px pill — and slides with the surface. */
.rail-btn::after, .rail-grp::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  transform: translateX(-160px);
  transition: transform var(--rail-close), background-color .18s cubic-bezier(.22, 1, .36, 1);
}
.rail[data-expanded="1"] .rail-btn::after,
.rail[data-expanded="1"] .rail-grp::after {
  transform: none;
  transition: transform var(--rail-open), background-color .18s cubic-bezier(.22, 1, .36, 1);
}
.rail-btn:hover { color: var(--fg); }
.rail-btn:hover::after, .rail-grp:hover::after {
  /* fg-derived so the hover tint exists in BOTH themes (a fixed light
     rgba is invisible-or-fog on white) */
  background-color: color-mix(in srgb, var(--fg) 8%, transparent);
}
.rail-btn.active {             /* active NAV: the accent's job */
  color: var(--accent-text);
}
.rail-btn.active::after { background-color: var(--accent-soft); }
.rail-btn.active::before {
  content: '';
  position: absolute;
  left: -8px;
  top: 9px;
  bottom: 9px;
  width: 2.5px;
  border-radius: 0 3px 3px 0;
  background: var(--accent-mark);
  box-shadow: 0 0 10px var(--accent-line);
}
.rail-btn svg {
  width: 19px;
  height: 19px;
  stroke-width: 1.7;
  flex-shrink: 0;
}
.rail-btn .label {
  font-size: 12.5px;
  font-weight: 500;
  opacity: 0;
  transition: opacity .1s;
}
.rail[data-expanded="1"] .rail-btn .label { opacity: 1; transition: opacity .22s .05s; }
/* A count on a rail item sits at the btn's right edge, out past the
   column: it fades with the labels. */
.rail .rail-btn-count { opacity: 0; transition: opacity .1s; }
.rail[data-expanded="1"] .rail-btn-count { opacity: 1; transition: opacity .22s .05s; }

/* ── Rail groups (the fold) ───────────────────────────────────────────
   The rail stopped scaling at 15 flat items. Four pinned surfaces stay flat;
   everything else lives in a collapsible group, and only ONE group is open at
   a time — so rail height is bounded by (pinned + headers + largest group)
   regardless of how many features ship.

   Height switches via grid-template-rows 0fr→1fr (a cut — see
   .rail-grp-items) rather than max-height: no magic number to outgrow, and
   no JS measurement. The children stay in the
   DOM and are only CLIPPED, never `display:none` — that is deliberate, it is
   what keeps them tabbable inside a collapsed group (rail-fold.js expands the
   group when focus lands inside it). */
.rail-group { display: flex; flex-direction: column; }

.rail-grp {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 42px;
  min-width: 42px;
  padding: 0 12px;
  border-radius: 11px;
  color: var(--fg-3);
  cursor: pointer;
  border: none;
  background: none;
  font-family: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-align: left;
  width: 100%;
  position: relative;
  isolation: isolate;
  transition: color .18s var(--ease);   /* the hover fill is the ::after (see .rail-btn::after) */
}
.rail-grp:hover { color: var(--fg); }
.rail-grp svg { width: 19px; height: 19px; flex-shrink: 0; }
.rail-grp .label {
  font-size: 11.5px;
  font-weight: 650;
  letter-spacing: .04em;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity .1s;
}
.rail[data-expanded="1"] .rail-grp .label { opacity: 1; transition: opacity .22s .05s; }

/* Chevron: only meaningful once labels are showing. */
.rail-chev {
  width: 14px !important;
  height: 14px !important;
  margin-left: auto;
  opacity: 0;
  transform: rotate(0deg);
  transition: transform .18s var(--ease), opacity .1s;
}
.rail[data-expanded="1"] .rail-chev { opacity: .55; transition: transform .18s var(--ease), opacity .2s .05s; }
.rail-group.open .rail-chev { transform: rotate(90deg); }

/* A group holding the ACTIVE page reads as current while COLLAPSED —
   otherwise folding hides where you are. Open, the child's pill carries
   the accent; keeping the header lit too would put two accent instances
   on one rail (§5f rule 1). */
.rail-group.has-active:not(.open) .rail-grp { color: var(--accent-text); }

.rail-grp-items {
  display: grid;
  grid-template-rows: 0fr;
  /* Open/close is a CUT (2026-10-16): grid-template-rows animated the
     rail's layout every frame. The children fade in instead. */
}
.rail-group.open .rail-grp-inner { animation: rail-fade-in .18s var(--ease); }
.rail-grp-inner { overflow: hidden; min-height: 0; display: flex;
                  flex-direction: column; gap: 2px; }
/* Deliberately NOT `visibility: hidden` on a collapsed group. It would satisfy
   the "no rendered child under a collapsed parent" invariant for free — and
   silently cost keyboard reach, because visibility:hidden is not focusable and
   the whole point of clipping rather than display:none is that Tab still walks
   into a closed group. grid-template-rows: 0fr already gives zero height AT
   REST; a stranded partial height is repaired in JS (assertInvariant). */
.rail-group.open .rail-grp-items { grid-template-rows: 1fr; }
/* Indent children so the group reads as a container once labels are visible.
   The LABEL carries the indent, in both states: the icon column stays
   aligned (a 12px indent on a 64px rail just misaligns it), and nothing's
   padding changes on expand — that was a per-frame re-layout. */
.rail-grp-inner .rail-btn .label { margin-left: 10px; }

/* Count badge. Sits on the CHILD when open, on the GROUP HEADER when
   collapsed — same number either way (label == contents, DESIGN.md). */
/* `display` on a class beats the UA's `[hidden] {display:none}`, so without
   this the badge renders whenever the markup sets hidden — which shipped a
   "0" on every empty group and left the rolled-up count showing next to an
   OPEN group that was already showing it on the child. */
.rail-grp-count[hidden], .rail-btn-count[hidden] { display: none !important; }
/* The Exports notice (helios-export-notice.js, EXPORTS §11): this person's exports in progress, on the icon's
   corner so it reads in the collapsed rail as well as the open one. In progress is information (§5g), never the
   alert amber the counts above carry; it moves no layout (absolute), so the rail's reveal is untouched. */
.rail-btn-pip {
  position: absolute;
  top: 5px;
  left: 23px;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--info);
  color: var(--info-ink);
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.rail-btn-pip[hidden] { display: none !important; }
.rail-grp-count.crit, .rail-btn-count.crit { background: var(--bad-strong); color: var(--bad-ink); }
.rail-grp-count, .rail-btn-count {
  min-width: 17px;
  height: 17px;
  padding: 0 5px;
  border-radius: 9px;
  /* the bell's rule, the bell's number (_sidebar.html setBadge): amber,
     the strong red once any unacked alert is critical */
  background: var(--warn);
  color: var(--warn-ink);
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  margin-left: auto;
}
/* With the rail collapsed there is no room for a number beside a 19px icon —
   it becomes a dot pinned to the icon's corner, which still says "something
   in here needs you". */
.rail:not([data-expanded="1"]) .rail-grp-count {
  position: absolute;
  margin: 0;
  transform: translate(9px, -9px);
  min-width: 8px;
  height: 8px;
  padding: 0;
  font-size: 0;
  border-radius: 50%;
}
/* Expanded, the number takes its place out past the column — fade it in
   rather than have it land at full opacity over the video. */
.rail[data-expanded="1"] .rail-grp-count { animation: rail-fade-in .22s .05s backwards; }
@keyframes rail-fade-in { from { opacity: 0; } }
.rail-chev + .rail-grp-count, .rail-grp-count + .rail-chev { margin-left: 6px; }

/* Flyout — the escape hatch for a COLLAPSED group. Hovering or focusing the
   header lists its children beside the rail, so a folded group never costs a
   click to look inside. */
.rail-grp-flyout {
  position: fixed;
  z-index: 200;
  min-width: 178px;
  padding: 6px;
  border-radius: 12px;
  background: var(--surface);       /* opens over /live's wall: solid (§5h) */
  border: 1px solid var(--line-2);
  box-shadow: var(--shadow-lg);
  display: flex;
  flex-direction: column;
  gap: 2px;
  opacity: 0;
  transform: translateX(-4px);
  pointer-events: none;
  transition: opacity .14s var(--ease), transform .14s var(--ease);
}
/* HOVER BRIDGE. The flyout is offset 10px from the rail, so a diagonal from
   the group icon to the flyout crossed ground owned by NEITHER — the header's
   pointerleave fired, and the arrival could not undo it. This transparent
   strip makes the gap part of the flyout's own hit area, so the pointer never
   leaves the live region on the way across. It is inert until the flyout is
   shown (pointer-events ride on .show below). */
.rail-grp-flyout::before {
  content: '';
  position: absolute;
  left: -14px;
  top: 0;
  bottom: 0;
  width: 14px;
}
.rail-grp-flyout.show { opacity: 1; transform: none; pointer-events: auto; }
.rail-grp-flyout a {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 34px;
  padding: 0 10px;
  border-radius: 8px;
  color: var(--fg-2);
  text-decoration: none;
  font-size: 12.5px;
  white-space: nowrap;
}
.rail-grp-flyout a:hover { background: color-mix(in srgb, var(--fg) 9%, transparent); color: var(--fg); }
.rail-grp-flyout a.active { color: var(--accent-text); background: var(--accent-soft); }
.rail-grp-flyout svg { width: 17px; height: 17px; flex-shrink: 0; }
.rail-grp-flyout .rail-grp-count, .rail-grp-flyout .rail-btn-count { margin-left: auto; }

/* The rail can out-grow the viewport on a short window (MacBook scaled
   modes). HEIGHT-CONSTRAINED LAYOUT, not a patch: logo and footer are
   fixed zones; this nav region is the ONE scroll container between them
   (flex: 1 / min-height: 0 / overflow-y: auto). The scroll path must be
   DISCOVERABLE — an invisible scrollbar made clipped items read as a
   jammed rail, not a scrollable one (2026-08-09): thin themed scrollbar
   plus top/bottom fade masks, both shown only when there is actually
   something to scroll to (.can-up/.can-down, JS-toggled). */
.rail-scroll { overflow-y: auto; overflow-x: hidden;
               flex: 1 1 auto; min-height: 0;
               display: flex; flex-direction: column; gap: 2px;
               scrollbar-width: thin;
               scrollbar-color: var(--line-3) transparent; }
.rail-scroll::-webkit-scrollbar { width: 5px; height: 0; }
.rail-scroll::-webkit-scrollbar-thumb { background: var(--line-3); border-radius: 4px; }
.rail-scroll::-webkit-scrollbar-track { background: transparent; }
/* Fade masks: sticky flex children occupying zero layout height. The
   gradient runs from the rail's own surface so it reads in both themes.
   Full rail width, so they scale with the surface (see .rail-sep). */
.rail-scroll::before, .rail-scroll::after {
  content: ''; position: sticky; flex: none; height: 18px; z-index: 2;
  pointer-events: none; opacity: 0;
  transition: opacity .2s, transform var(--rail-close);
}
.rail-scroll::before {
  top: 0; margin-bottom: -18px;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--surface) 88%, transparent), transparent);
}
.rail-scroll::after {
  order: 999; bottom: 0; margin-top: -18px;
  background: linear-gradient(to top,
    color-mix(in srgb, var(--surface) 88%, transparent), transparent);
}
.rail-scroll.can-up::before { opacity: 1; }
.rail-scroll.can-down::after { opacity: 1; }
.rail[data-expanded="1"] .rail-scroll::before,
.rail[data-expanded="1"] .rail-scroll::after {
  transform: none;
  transition: opacity .2s, transform var(--rail-open);
}

@media (prefers-reduced-motion: reduce) {
  .rail-grp-items, .rail-chev, .rail-grp-flyout { transition: none !important; }
  /* the reveal becomes a cut: the surface and everything tracking it */
  .rail-surface, .rail-surface::after, .rail-btn::after, .rail-grp::after,
  .rail-sep, .rail-logo::after, .rail-footer::before,
  .rail-scroll::before, .rail-scroll::after { transition: none !important; }
}

.rail-spacer { flex: 1; }

.rail-footer {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px 6px 2px;            /* 10px + the 1px hairline drawn by ::before */
  position: relative;
  overflow: hidden;
  white-space: nowrap;
  margin-top: auto;
}
.rail-footer::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
  background: var(--line);
}
.rail-avatar {
  width: 33px;
  height: 33px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--surface-3), var(--surface-2));
  border: 1px solid var(--line-2);
  display: grid;
  place-items: center;
  color: var(--fg);
  font-size: 11px;
  font-weight: 600;
  flex-shrink: 0;
}
.rail-user {
  opacity: 0;
  transition: opacity .1s;
  line-height: 1.25;
}
.rail[data-expanded="1"] .rail-user { opacity: 1; transition: opacity .22s .05s; }
.rail-user .name { font-size: 12px; font-weight: 600; color: var(--fg); }
.rail-user .org { font-size: 10px; color: var(--fg-2); }

/* ── Footer: clickable avatar + sign-out ─────────────────────────── */
.rail-avatar-link {
  display: flex;
  align-items: center;
  text-decoration: none;
  flex-shrink: 0;
  border-radius: 10px;
  transition: box-shadow 0.18s var(--ease);
}
.rail-avatar-link.active .rail-avatar {
  box-shadow: 0 0 0 2px var(--accent-mark);
}
.rail-avatar-link:hover .rail-avatar {
  filter: brightness(1.1);
}

.rail-footer-gear {
  margin-left: auto;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border-radius: 9px;
  color: var(--fg-3);
  text-decoration: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity .1s, color 0.18s var(--ease),
              background 0.18s var(--ease);
}
.rail[data-expanded="1"] .rail-footer-gear {
  opacity: 1;
  pointer-events: auto;
  transition: opacity .22s .05s, color 0.18s var(--ease),
              background 0.18s var(--ease);
}
.rail-footer-gear svg {
  width: 17px;
  height: 17px;
  stroke-width: 1.7;
}
.rail-footer-gear:hover {
  color: var(--fg-1);
  background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.rail-footer-gear.active {
  color: var(--accent-text);
  background: var(--accent-soft);
}

/* Stagger reveal — applied to grid children for the "alive" feel */
.stagger-children > * {
  animation: stagger-in var(--t-slow) var(--ease-out-expo) backwards;
}
.stagger-children > *:nth-child(1) { animation-delay: 0.04s; }
.stagger-children > *:nth-child(2) { animation-delay: 0.09s; }
.stagger-children > *:nth-child(3) { animation-delay: 0.14s; }
.stagger-children > *:nth-child(4) { animation-delay: 0.19s; }
.stagger-children > *:nth-child(5) { animation-delay: 0.24s; }
.stagger-children > *:nth-child(6) { animation-delay: 0.29s; }
.stagger-children > *:nth-child(7) { animation-delay: 0.34s; }
.stagger-children > *:nth-child(8) { animation-delay: 0.39s; }

/* Scrollbar polish */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 3px; }
::-webkit-scrollbar-track { background: transparent; }

/* ═══════════════════════════════════════════════════════════════════
 *  Phase B.2 — Additions for cameras_v2 + discovery migration
 *  ─────────────────────────────────────────────────────────────────
 *  Four component groups promoted from per-page CSS into the shared
 *  library. Now that tokens.css is canonical (palette A — deeper,
 *  more restrained), every value here references tokens.css.
 *
 *  Group 11. Page-header system   (.page-shell, .shell-head, .shell-title…)
 *  Group 12. Filter chips         (.filter-chip-row, .filter-chip, .filter-chip.active)
 *  Group 13. Status pill          (.stat-pill, .stat-pill.scanning…)
 *  Group 14. Summary bar          (.summary-bar, .summary-bar .stat…)
 * ═════════════════════════════════════════════════════════════════ */


/* ── 11. Page-header system ──────────────────────────────────── */

.page-shell {
  padding: 24px 24px 40px;
  max-width: 1500px;
  width: 100%;
  margin: 0 auto;
}
.shell-head {
  display: flex;
  align-items: flex-end;
  gap: 20px;
  margin-bottom: 20px;
}
.shell-head .spacer { flex: 1; }
.shell-title {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--fg);
}
.shell-sub {
  font-size: 12.5px;
  color: var(--fg-2);
  margin-top: 4px;
}
.shell-sub b {
  color: var(--fg-1);
  font-weight: 500;
}
.shell-stats {
  display: flex;
  align-items: center;
  gap: 18px;
  font-size: 12px;
  color: var(--fg-2);
}
.shell-stats .stat b {
  color: var(--fg);
  font-weight: 600;
  font-family: var(--font-mono);
  margin-right: 4px;
}


/* ── 12. Filter chips ────────────────────────────────────────── */

.filter-chip-row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  color: var(--fg-2);
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--t-fast) var(--ease);
}
.filter-chip:hover {
  color: var(--fg-1);
  border-color: var(--line-2);
  background: var(--surface-2);
}
.filter-chip.active {          /* a chosen filter is SELECTION: neutral */
  background: var(--select-soft);
  border-color: var(--select);
  color: var(--fg);
}
.filter-chip .count {
  font-family: var(--font-mono);
  font-size: 11px;
  margin-left: 1px;
  color: var(--fg-3);
}
.filter-chip.active .count {
  color: var(--fg-1);
  opacity: 0.8;
}


/* ── 13. Status pill ─────────────────────────────────────────── */
/* Generic dot-prefixed pill for state. Subclasses change semantics:
 *   .online / .ok / .done    — healthy mint
 *   .scanning / .active      — accent blue
 *   .warn                    — warn orange
 *   .bad / .error            — bad red
 *   (base, no modifier)      — neutral gray */

.stat-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: 12px;
  color: var(--fg-1);
}
.stat-pill .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fg-3);
  flex-shrink: 0;
}
.stat-pill.online,
.stat-pill.ok,
.stat-pill.done {
  border-color: var(--ok-line);
  background: var(--ok-soft);
  color: var(--ok-text);
}
.stat-pill.online .dot,
.stat-pill.ok .dot,
.stat-pill.done .dot {
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--ok-soft);
}
.stat-pill.online .dot {
  animation: pulse 2s var(--ease) infinite;
}
.stat-pill.scanning,
.stat-pill.active {          /* in progress: info, never the accent */
  border-color: var(--info-line);
  background: var(--info-soft);
  color: var(--info-text);
}
.stat-pill.scanning .dot,
.stat-pill.active .dot {
  background: var(--info);
  animation: pulse 1.4s var(--ease) infinite;
}
.stat-pill.warn {
  border-color: var(--warn-line);
  background: var(--warn-soft);
  color: var(--warn-text);
}
.stat-pill.warn .dot {
  background: var(--warn);
}
.stat-pill.bad,
.stat-pill.error {
  border-color: var(--bad-line);
  background: var(--bad-soft);
  color: var(--bad-text);
}
.stat-pill.bad .dot,
.stat-pill.error .dot {
  background: var(--bad);
}


/* ── 14. Summary bar ─────────────────────────────────────────── */
/* Dense one-line stats strip used on list pages. Dot separators
 * between stats, optional state-color dots. */

.summary-bar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  margin-bottom: 20px;
  font-size: 12px;
  color: var(--fg-2);
}
.summary-bar .stat {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.summary-bar .stat b {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--fg);
  font-size: 12.5px;
  margin-right: 2px;
}
.summary-bar .stat .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fg-3);
  flex-shrink: 0;
}
.summary-bar .stat.online .dot { background: var(--ok); }
.summary-bar .stat.offline .dot { background: var(--bad); }
.summary-bar .stat.ptz .dot { background: var(--fg-2); }
.summary-bar .stat.audio .dot { background: var(--evt-audio); }
.summary-bar .sep {
  color: var(--fg-3);
  font-size: 10px;
}
.summary-bar .spacer { flex: 1; }



/* ════════════════════════════════════════════════════════════════════
   B.5.5b — Alive topbar styles
   All scoped under .topbar so they only apply where the alive partial
   is included. The legacy .topbar-stats / inline rules in individual
   pages continue to work for un-migrated pages.
   ════════════════════════════════════════════════════════════════════ */

/* ── Site mode pill (deterrence scheduling) ──
   Deterrence responses are gated on the site mode, so it rides the topbar on
   every page. Mint = armed, accent = night, grey = disarmed; an amber border
   plus the "held" tag means an operator is overriding the schedule. Never let
   a held mode read as a scheduled one. */
.topbar__sitemode {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; margin-right: 2px;
  border: 1px solid var(--line-2); border-radius: var(--r-pill);
  background: var(--surface-2);
  font-size: 11px; font-weight: 600; letter-spacing: .02em;
  color: var(--fg-1); text-decoration: none; text-transform: capitalize;
  transition: border-color .15s ease, background .15s ease;
}
.topbar__sitemode:hover { border-color: var(--accent-mark); background: var(--surface-3); }
.topbar__sitemode .dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--unknown); flex: none;
}
.topbar__sitemode[data-mode="armed"] .dot { background: var(--ok); }
.topbar__sitemode[data-overridden="1"] { border-color: var(--warn); }
/* the why-sentence (2026-10-13): a sentence reads in sentence case, on one line */
.topbar__sitemode[data-mode="unknown"] { text-transform: none; white-space: nowrap; }
.topbar__sitemode em {
  font-style: normal; font-size: 9px; text-transform: uppercase;
  letter-spacing: .06em; color: var(--warn-text);
}

.topbar__clock {
  font-family: 'Geist Mono', ui-monospace, monospace;
  color: var(--fg-1);
  font-size: var(--text-md);
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  flex-shrink: 0;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}
.topbar__clock .secs { color: var(--fg-3); margin-left: -6px; }
.topbar__clock .ampm { margin-left: -6px; }   /* flush like the seconds; its own no-break space says "10:32 PM" when they fold */   /* seconds tick quietly, flush to hh:mm (the flex gap is for the tz pill: "13:05 :00", 2026-10-04) */
.topbar__clock .tz {
  font-size: 9.5px; font-weight: 600; letter-spacing: 0.06em;
  color: var(--fg-3); text-transform: uppercase;
  padding: 1px 5px; border-radius: 4px;
  background: var(--surface-3); border: 1px solid var(--line);
}

/* Site weather (2026-10-12): icon + temperature + one word beside the clock;
   a click opens the server's detail. Hidden until the server has a fresh
   forecast, and the FIRST thing to go when the bar is short (tb-c-wx). */
.topbar__wx { position: relative; flex: 0 0 auto; }
.topbar__wx[hidden] { display: none; }
.topbar__wx-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 28px; padding: 0 8px; border-radius: 6px;
  background: transparent; border: 1px solid transparent;
  color: var(--fg-1); font-size: var(--text-sm); white-space: nowrap; cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.topbar__wx-btn:hover, .topbar__wx-btn[aria-expanded="true"] { background: var(--surface-2); border-color: var(--line); }
.topbar__wx-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.topbar__wx-ic { display: inline-flex; width: 16px; height: 16px; color: var(--fg-2); }
.topbar__wx-ic svg { width: 16px; height: 16px; }
.topbar__wx-pop {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 120;
  width: 300px; padding: 12px 14px;
  background: var(--surface); border: 1px solid var(--line-2); border-radius: 8px;
  box-shadow: var(--shadow-lg); color: var(--fg-1); font-size: var(--text-sm); line-height: 1.45;
}
.topbar__wx-pop[hidden] { display: none; }
.topbar__wx-pop .wx-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.topbar__wx-pop .wx-head svg { width: 30px; height: 30px; color: var(--fg-2); }
.topbar__wx-pop .wx-temp { font-size: var(--text-lg); font-weight: 600; color: var(--fg); font-variant-numeric: tabular-nums; }
.topbar__wx-pop .wx-phrase { color: var(--fg-2); }
.topbar__wx-pop .wx-rows { margin: 0; display: grid; gap: 4px; }
.topbar__wx-pop .wx-rows > div { display: flex; justify-content: space-between; gap: 14px; }
.topbar__wx-pop .wx-rows dt { color: var(--fg-3); }
.topbar__wx-pop .wx-rows dd { margin: 0; color: var(--fg); text-align: right; }
.topbar__wx-pop .wx-hours { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px; margin-top: 12px;
  padding-top: 10px; border-top: 1px solid var(--line); }
.topbar__wx-pop .wx-h { display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 0; }
.topbar__wx-pop .wx-h svg { width: 18px; height: 18px; color: var(--fg-2); }
.topbar__wx-pop .wx-ht { color: var(--fg-3); font-size: var(--text-xs); white-space: nowrap; }
.topbar__wx-pop .wx-hv { color: var(--fg); font-variant-numeric: tabular-nums; }
.topbar__wx-pop .wx-hp { color: var(--fg-3); font-size: var(--text-xs); }
.topbar__wx-pop .wx-foot { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 10px; margin-top: 10px;
  color: var(--fg-3); font-size: var(--text-xs); }
.topbar__wx-pop .wx-foot > span:first-child { white-space: nowrap; }

/* the system-health pill + panel: styles/health-pill.css (@imported at the top) */

.topbar__cmd {
  /* Centered search field — absolute-centered in the topbar */
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: min(340px, 32vw);
  padding: 7px 12px 7px 11px;
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: 9px;
  color: var(--fg-3);
  font-size: var(--text-md);
  cursor: pointer;
  transition: background 0.2s var(--ease-out-quint),
              border-color 0.2s var(--ease-out-quint),
              color 0.2s, box-shadow 0.2s, transform 0.2s var(--ease-out-quint);
  font-family: inherit;
  z-index: 5;
}
.topbar__cmd .text { flex: 1; text-align: left; }
.topbar__cmd .kbd { margin-left: auto; }
.topbar__cmd:hover {
  background: var(--surface-2); color: var(--fg-1); border-color: var(--accent-line);
  transform: translate(-50%, -50%) translateY(-1px);
  box-shadow: 0 4px 14px -4px color-mix(in srgb, var(--shade) 35%, transparent), 0 0 0 3px var(--accent-soft);
}
.topbar__cmd svg { width: 12px; height: 12px; stroke-width: 2; flex-shrink: 0; }
.topbar__cmd .text { white-space: nowrap; }
.topbar__cmd .kbd {
  padding: 1px 5px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 10px;
  color: var(--fg-3);
}

.topbar__iconbtn {
  width: 30px; height: 30px;
  display: grid; place-items: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--fg-2);
  cursor: pointer;
  transition: background 0.18s, border-color 0.18s, color 0.18s;
  position: relative;
  flex-shrink: 0;
}
.topbar__iconbtn:hover { background: var(--surface-3); color: var(--fg); border-color: var(--line); }
.topbar__iconbtn svg { width: 16px; height: 16px; stroke-width: 1.7; }
.topbar__iconbtn .notif-dot {
  position: absolute;
  top: 6px; right: 6px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--attn);   /* a notification is attention — a STATE */
  box-shadow: 0 0 0 2px var(--surface);
  animation: helios-notif-pulse 2s ease-in-out infinite;
  display: none;
}
.topbar__iconbtn[data-has-notif="true"] .notif-dot { display: block; }
@media (prefers-reduced-motion: reduce) { .topbar__iconbtn .notif-dot { animation: none; } }

/* Theme toggle: exactly one glyph, keyed to the stored PREFERENCE
   (sun = light, moon = dark, half-disc = system). Pre-JS it shows the
   moon — the absent-preference default is dark (DESIGN.md §1). */
.topbar__theme svg { display: none; }
.topbar__theme[data-pref="light"] .th-sun,
.topbar__theme[data-pref="dark"] .th-moon,
.topbar__theme[data-pref="system"] .th-auto,
.topbar__theme:not([data-pref]) .th-moon { display: block; }

.topbar__avatar {
  width: 30px; height: 30px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--surface-3), var(--surface-2));
  border: 1px solid var(--line);
  display: grid; place-items: center;
  color: var(--fg);
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
  flex-shrink: 0;
  transition: border-color 0.2s;
}
.topbar__avatar:hover { border-color: var(--line-2); }

/* Responsive: collapse the secondary chrome on narrow viewports */
@media (max-width: 980px) {
  .topbar__solar, .topbar__netflow, .topbar__clock { display: none; }
}
@media (max-width: 720px) {
  .topbar .crumbs { display: none; }
  .topbar__cmd .text { display: none; }
  .topbar__cmd { padding: 5px 8px; }
}


/* ── B.5.5c — Topbar coming-soon stubs ─────────────────────── */
.topbar__avatar-menu {
  position: absolute;
  top: calc(var(--topbar-h) - 6px);
  right: 14px;
  min-width: 220px;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: 10px;
  box-shadow: var(--shadow-lg);
  padding: 6px;
  z-index: 90;
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;
  transition: opacity .18s, transform .18s;
}
.topbar__avatar-menu.open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.topbar__avatar-menu .head {
  padding: 10px 12px 12px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 4px;
}
.topbar__avatar-menu .head .name {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--fg);
}
.topbar__avatar-menu .head .org {
  font-size: var(--text-sm);
  color: var(--fg-3);
  margin-top: 2px;
}
.topbar__avatar-menu .item {
  display: block;
  padding: 8px 12px;
  border-radius: 6px;
  color: var(--fg-2);
  font-size: var(--text-md);
  text-decoration: none;
  cursor: pointer;
  background: none;
  border: none;
  width: 100%;
  text-align: left;
  font-family: inherit;
}
.topbar__avatar-menu .item:hover { background: var(--surface-3); color: var(--fg); }
/* Theme choice in the avatar menu (2026-10-11) */
.topbar__avatar-menu .theme-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 12px; }
.topbar__avatar-menu .theme-label { color: var(--fg-2); font-size: var(--text-md); }
.topbar__avatar-menu .theme-seg { display: inline-flex; border: 1px solid var(--line-2); border-radius: 6px; overflow: hidden; }
.topbar__avatar-menu .theme-seg button { background: transparent; border: 0; color: var(--fg-2); padding: 3px 8px; font: inherit; font-size: 12px; cursor: pointer; }
.topbar__avatar-menu .theme-seg button.active { background: var(--surface-3); color: var(--fg); }
.topbar__avatar-menu .theme-seg button:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.topbar__avatar-menu .item.disabled {
  color: var(--fg-4);
  cursor: not-allowed;
}
.topbar__avatar-menu .item.disabled:hover { background: transparent; color: var(--fg-4); }
.topbar__avatar-menu .item .hint {
  font-size: 10px;
  color: var(--fg-3);
  margin-left: 6px;
  font-style: italic;
}

/* Coming-soon modal — leans on existing .modal-backdrop/.modal */
.topbar__soon-modal .modal-body { padding: 18px 22px 22px; }
/* keyboard shortcuts sheet (partials/_topbar.html _keys_overlay) */
.topbar__keys .kbd-inline {
  display: inline-block;
  padding: 1px 6px;
  background: var(--surface-3);
  border: 1px solid var(--line-2);
  border-radius: 4px;
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 10px;
  color: var(--fg-1);
  margin: 0 2px;
}
.topbar__keys .keys-body { padding: 6px 22px 10px; max-height: 64vh; overflow-y: auto; }
.topbar__keys .keys-surface { padding: 12px 0 6px; border-top: 1px solid var(--line); }
.topbar__keys .keys-surface:first-child { border-top: 0; }
.topbar__keys .keys-surface-title { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 6px; }
.topbar__keys .keys-row { display: flex; align-items: center; gap: 14px; padding: 5px 0; }
.topbar__keys .keys-keys { flex: 0 0 150px; display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.topbar__keys .keys-sep { color: var(--fg-3); font-size: 11px; }
.topbar__keys .keys-does { color: var(--fg-1); font-size: 13px; }
.topbar__keys .keys-foot { padding: 10px 22px 16px; color: var(--fg-3); font-size: var(--text-sm); border-top: 1px solid var(--line); }

.topbar__soon-modal .modal-body p { color: var(--fg-2); line-height: 1.5; margin: 0; }
.topbar__soon-modal .modal-body p + p { margin-top: 10px; }
.topbar__soon-modal .modal-body .kbd-inline {
  display: inline-block;
  padding: 1px 6px;
  background: var(--surface-3);
  border: 1px solid var(--line-2);
  border-radius: 4px;
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 10px;
  color: var(--fg-1);
  margin: 0 2px;
}




/* ── B.5.5d — Workflow topbar chrome ───────────────────────────
   Promoted from discovery.html's inline styles so any workflow
   page can use these via the topbar_workflow macro. */

.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--fg-1);
  text-decoration: none;
  font-size: var(--text-md);
  font-weight: 500;
  transition: all 0.15s var(--ease-out-quint);
  flex-shrink: 0;
}
.back-link:hover {
  background: var(--surface-2);
  border-color: var(--line-2);
  color: var(--fg);
}
.back-link svg { width: 13px; height: 13px; stroke-width: 1.8; }

.crumb-link {
  color: inherit;
  text-decoration: none;
  transition: color 0.15s;
}
.crumb-link:hover { color: var(--fg); }

/* Scanning status pill (different from the alive .status-pill).
   Page-specific signal for workflows like network discovery. */
.stat-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 8px;
  border-radius: 100px;
  font-size: var(--text-sm);
  font-weight: 500;
  border: 1px solid;
  white-space: nowrap;
  flex-shrink: 0;
}
.stat-pill .dot {
  width: 6px; height: 6px;
  border-radius: 50%;
}
.stat-pill.scanning {
  background: var(--info-soft);
  border-color: var(--info-line);
  color: var(--info-text);
}
.stat-pill.scanning .dot {
  background: var(--info);
  animation: helios-notif-pulse 1.4s var(--ease-out-quint) infinite;
}
.stat-pill.ok {
  background: var(--ok-soft);
  border-color: var(--ok-line);
  color: var(--ok-text);
}
.stat-pill.ok .dot { background: var(--ok); }
.stat-pill.warn {
  background: var(--warn-soft);
  border-color: var(--warn-line);
  color: var(--warn-text);
}
.stat-pill.warn .dot { background: var(--warn); }

/* Segmented control (grid/list toggle, etc) */
.seg-control {
  display: inline-flex;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 6px;
  flex-shrink: 0;
}
.seg-control .seg {
  display: grid;
  place-items: center;
  width: 28px;
  height: 24px;
  border-radius: 4px;
  background: transparent;
  border: none;
  color: var(--fg-3);
  cursor: pointer;
  transition: all 0.15s;
  font-family: inherit;
}
.seg-control .seg:hover { color: var(--fg-1); }
.seg-control .seg.active {
  background: var(--surface-3);
  color: var(--fg);
}
.seg-control .seg svg { width: 13px; height: 13px; stroke-width: 1.8; }

/* Action buttons in workflow topbars */
.topbar .btn,
.topbar-v2 .btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--fg-1);
  font-size: var(--text-md);
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: all 0.15s var(--ease-out-quint);
  flex-shrink: 0;
}
.topbar .btn:hover,
.topbar-v2 .btn:hover {
  background: var(--surface-2);
  border-color: var(--line-2);
  color: var(--fg);
}
.topbar .btn:disabled,
.topbar-v2 .btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.topbar .btn svg,
.topbar-v2 .btn svg { width: 13px; height: 13px; stroke-width: 1.8; }

/* ── B.5.5d.1 — Solar arc (revived from B.5.5b, kept tooltip) ──
   Visual-only daylight indicator. Sun dot rides an SVG arc from
   east horizon (sunrise) to peak (solar noon) to west (sunset).
   At night the dot drops below the arc into a quieter grey.
   Updates every minute. Hover reveals sunrise/sunset details. */
.topbar__daylight {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  cursor: default;
  padding: 4px 6px;
  border-radius: 6px;
  transition: background 0.18s;
}
.topbar__daylight:hover {
  background: var(--surface-3);
}
.topbar__daylight .arc {
  width: 92px;
  height: 28px;
  display: block;
  overflow: visible;
}
.topbar__daylight .arc-path {
  fill: none;
  stroke: var(--line-3);
  stroke-width: 1.2;
  stroke-linecap: round;
}
.topbar__daylight .sun-dot {      /* the sun: a BRAND use of the accent */
  fill: var(--sun);
  transition: cx 1.5s ease, cy 1.5s ease, r 1s ease, fill 1.5s ease;
}
.topbar__daylight[data-mode="night"]    .sun-dot,
.topbar__daylight[data-mode="pre-dawn"] .sun-dot {
  fill: var(--sun-night);
}
.topbar__daylight[data-mode="golden-hour"] .sun-dot,
.topbar__daylight[data-mode="morning-golden"] .sun-dot,
.topbar__daylight[data-mode="dusk"] .sun-dot {
  fill: var(--sun-low);
}

/* Hover tooltip — preserved from B.5.5f */
.topbar__tip {
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%) translateY(-4px);
  min-width: 200px;
  max-width: 300px;
  padding: 8px 12px;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: 8px;
  box-shadow: var(--shadow-lg);
  font-size: var(--text-sm);
  color: var(--fg-1);
  white-space: normal;
  line-height: 1.45;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.16s, transform 0.16s;
  z-index: 100;
}
.topbar__tip .row { display: flex; justify-content: space-between; gap: 14px; }
.topbar__tip .row + .row { margin-top: 4px; }
.topbar__tip .row .k { color: var(--fg-3); }
.topbar__tip .row .v {
  color: var(--fg);
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: var(--text-xs);
}
.topbar__daylight:hover .topbar__tip {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Hide on narrow viewports */
@media (max-width: 980px) {
  .topbar__daylight { display: none; }
}

/* ── B.5.5d.1 — Workflow topbar crumb fix ──────────────────────
   Keep workflow crumbs on a single line so back-link doesn't
   push them into wrapping. */
.topbar .crumbs {
  white-space: nowrap;
  min-width: 0;
}
.topbar .crumbs .here {
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
}

/* ── B.5.5d.2 — Ensure topbar always spans its container ────── */
.topbar[data-helios-topbar] {
  width: 100%;
  box-sizing: border-box;
}

/* B.5.5d.3 — spacer must actually expand to push right cluster to edge */
.topbar[data-helios-topbar] .spacer { flex: 1 1 0; min-width: 0; }

/* ═══════════════════════════════════════════════════════════════════
 *  TOPBAR COLLAPSE LAW (2026-08-23) — the row never clips a control.
 *  Same philosophy as the tile bar's priority-collapse: when width is
 *  short, items give way IN ORDER, measured (not viewport media queries —
 *  those fired on window width while the row's CONTENT grew: the /live
 *  pager + landing + attention segments pushed the health pill off the
 *  right edge at 1440 with every media step still asleep). _topbar_js()
 *  measures the row and adds one step class at a time until it fits:
 *
 *    tb-c-search-floor  ⌘K search shrinks to its min-width floor (FIRST)
 *    tb-c-search   ⌘K search → icon only
 *    tb-c-wx       the weather chip hides (the owner, 2026-10-12: only after
 *                  the search is already an icon, before anything else)
 *    tb-c-org      breadcrumb org + separator hidden (the page name stays)
 *    tb-c-bell     the alerts bell docked on the health pill (/live,
 *                  2026-09-10) drops its count → glyph + dot only; the
 *                  count is one click away in the drawer, a folded tab
 *                  is not — so this rung sits BEFORE the tab rungs
 *    tb-c-secs     clock drops seconds + timezone     (HH:MM stays) —
 *                  moved ahead of the tab rungs 2026-09-10: with the bell
 *                  on the pill the 2560/ten-view row was 5 px from full,
 *                  and seconds + zone are cheaper than a whole tab
 *    tb-c-tabs     view-tab strip narrower → excess folds into its ⌄ menu
 *    tb-c-tabs2    …narrower still
 *    tb-c-labels   docked control captions → icon only (page-specific)
 *    tb-c-cmd      search hidden (⌘K still opens it)
 *    tb-c-tabs3    strip down to the active tab + its ⌄
 *    tb-c-tabs4    …and the permanent All Cameras tab goes icon-only, the
 *                  [+] and separator hide (the active tab stays WHOLE)
 *    tb-c-brain    second-brain button hidden (a "soon" stub)
 *    tb-c-health   health pill → COMPACT form (dot + "2 issues"): it may
 *                  shorten, it NEVER clips — a truncated health surface is
 *                  an honesty violation
 *    tb-c-clock    clock hidden — last
 *
 *  IRREDUCIBLE: site-mode pill, health pill (compact floor), theme toggle,
 *  brain, avatar. They are flex: 0 0 auto and the measure loop stops only
 *  when every item sits inside the row without overlapping its neighbour.
 * ═══════════════════════════════════════════════════════════════════ */
.topbar[data-helios-topbar] { flex-wrap: nowrap; }
.topbar[data-helios-topbar] .crumbs { flex: 0 1 auto; min-width: 48px; overflow: hidden; }
.topbar[data-helios-topbar] .crumbs > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar__sitemode, .status-pill, .topbar__iconbtn, .topbar__avatar,
.topbar__clock, .topbar__theme, .topbar__daylight { flex: 0 0 auto; }
/* .status-pill .label-short's default (hidden) lives with the pill: styles/health-pill.css */
.topbar.tb-c-search .topbar__cmd .text,
.topbar.tb-c-search .topbar__cmd .kbd { display: none; }
.topbar.tb-c-search .topbar__cmd { width: auto; min-width: 0; max-width: none; padding-left: 8px; padding-right: 8px; gap: 0; }
.topbar.tb-c-org .crumbs > span:first-child,
.topbar.tb-c-org .crumbs > .sep:first-of-type,
.topbar.tb-c-orglive .crumbs > span:first-child,
.topbar.tb-c-orglive .crumbs > .sep:first-of-type { display: none; }
.topbar.tb-c-bell #hxBell.hxBell--topbar .n { display: none; }
.topbar.tb-c-tabs  .vt-strip { max-width: 24vw; }
.topbar.tb-c-tabs2 .vt-strip { max-width: 14vw; }
.topbar.tb-c-tabs3 .vt-strip { max-width: 7vw; }
.topbar.tb-c-tabs4 .vt-strip .vt-all .vt-name,
.topbar.tb-c-tabs4 .vt-strip .vt-add,
.topbar.tb-c-tabs4 .vt-strip .vt-sep { display: none; }
.topbar.tb-c-tabs4 .vt-strip .vt-all { padding: 0 8px; }
.topbar.tb-c-brain [data-brain-trigger] { display: none; }
.topbar.tb-c-secs .topbar__clock .secs,
.topbar.tb-c-secs .topbar__clock .tz,
.topbar.tb-c-livesecs .topbar__clock .secs,
.topbar.tb-c-livesecs .topbar__clock .tz { display: none; }
.topbar.tb-c-cmd .topbar__cmd { display: none; }
.topbar.tb-c-health .status-pill .label { display: none; }
.topbar.tb-c-health .status-pill .label-short { display: inline; }
.topbar.tb-c-clock .topbar__clock { display: none; }
.topbar.tb-c-wx .topbar__wx { display: none; }

/* ─────────────────────────────────────────────────────────
   B.5.5h — Command Palette
───────────────────────────────────────────────────────── */
.pal-backdrop {
  position: fixed; inset: 0;
  background: var(--dim);
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s cubic-bezier(.22,1,.36,1);
}
.pal-backdrop.open { opacity: 1; pointer-events: auto; }

.palette {
  position: fixed;
  top: 18vh;
  left: 50%;
  transform: translateX(-50%) translateY(-8px) scale(0.985);
  width: 640px;
  max-width: calc(100vw - 32px);
  max-height: 64vh;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: 14px;
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  z-index: 1001;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity 0.22s cubic-bezier(.22,1,.36,1),
    transform 0.32s cubic-bezier(.16,1,.3,1);
}
.palette.open {
  opacity: 1;
  transform: translateX(-50%) translateY(0) scale(1);
  pointer-events: auto;
}

.pal-input-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
}
.pal-input-row svg.search-icon {
  width: 16px; height: 16px;
  color: var(--fg-3);
  flex-shrink: 0;
}
.pal-input {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--fg);
  font-family: inherit;
  font-size: 15px;
  letter-spacing: -0.01em;
  padding: 0;
}
.pal-input::placeholder { color: var(--fg-3); }
.pal-esc {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 10.5px;
  padding: 2px 7px;
  border-radius: 4px;
  background: var(--surface-3);
  border: 1px solid var(--line);
  color: var(--fg-2);
  letter-spacing: 0.04em;
}

.pal-results {
  flex: 1;
  overflow-y: auto;
  padding: 6px 6px 6px;
  scrollbar-width: thin;
}
.pal-results::-webkit-scrollbar { width: 8px; }
.pal-results::-webkit-scrollbar-thumb {
  background: var(--line-2);
  border-radius: 4px;
  border: 2px solid var(--surface);
}

.pal-section { padding: 8px 8px 2px; }
.pal-section:empty { display: none; }
.pal-section-label {
  padding: 4px 10px 6px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-3);
}

.pal-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 10px;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.14s cubic-bezier(.22,1,.36,1),
              color 0.14s cubic-bezier(.22,1,.36,1);
  position: relative;
}
.pal-item.selected { background: var(--select-soft); }   /* the keyboard cursor is selection */
.pal-item .icon {
  flex-shrink: 0;
  width: 28px; height: 28px;
  display: grid; place-items: center;
  background: var(--surface-3);
  border-radius: 7px;
  color: var(--fg-1);
  transition: all 0.14s cubic-bezier(.22,1,.36,1);
}
.pal-item.selected .icon {
  background: var(--surface);
  color: var(--fg);
  box-shadow: 0 0 0 1px var(--select) inset;
}
.pal-item .icon svg { width: 14px; height: 14px; stroke-width: 1.8; }
.pal-item .icon-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--ok-soft);
}
.pal-item .icon-dot.offline {
  background: var(--bad);
  box-shadow: 0 0 0 3px var(--bad-soft);
}

.pal-item .text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  line-height: 1.3;
}
.pal-item .label {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--fg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pal-item .meta {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 10.5px;
  color: var(--fg-3);
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pal-item .right {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
.pal-item .right .enter {
  opacity: 0;
  transform: translateX(-4px);
  transition: all 0.18s cubic-bezier(.22,1,.36,1);
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 10.5px;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--surface);
  border: 1px solid var(--line-3);
  color: var(--fg-2);
}
.pal-item.selected .right .enter {
  opacity: 1;
  transform: translateX(0);
}
.pal-item .kbd {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 10.5px;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--surface-3);
  border: 1px solid var(--line);
  color: var(--fg-2);
}
.pal-item .ai-mark {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 10px;
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--ok-soft);
  border: 1px solid var(--ok-line);
  color: var(--ok-text);
  letter-spacing: 0.04em;
}

.pal-empty {
  padding: 28px 16px;
  text-align: center;
  color: var(--fg-3);
  font-size: 13px;
}

.pal-footer {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 14px;
  border-top: 1px solid var(--line);
  background: var(--surface-2);
  font-size: 11px;
  color: var(--fg-3);
}
.pal-footer .group { display: inline-flex; align-items: center; gap: 6px; }
.pal-footer .k {
  font-family: 'Geist Mono', ui-monospace, monospace;
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--surface-3);
  border: 1px solid var(--line);
  color: var(--fg-1);
  font-size: 10.5px;
  min-width: 16px;
  text-align: center;
}
.pal-footer .spacer { flex: 1; }

/* ── INK FOLLOWS ITS SURFACE ─────────────────────────────────────────────
   `data-dark-invariant` pins a BACKGROUND dark in both themes. It said
   nothing about the foreground, so colour kept inheriting from the page —
   and in light theme that is the dark ink --fg. A camera-name pill therefore
   kept its dark fill and rendered near-black text on it: correct by the
   rule, unreadable on screen. Found 2026-08-11 photographing the
   light-theme /access lightbox, after the same pattern had already shipped
   on .dx-cam .nm.

   The theme-overlay detector cannot catch this — it audits backgrounds
   only, so a dark-on-dark foreground is invisible to it.

   --cam-* are the theme-invariant tile-chrome tokens (identical values in
   both blocks of tokens.css, and tools/check_contrast.py already gates
   --cam-fg on --cam-scrim-2). Declaring a surface invariant now carries its
   ink along, so the two cannot drift apart again. */
[data-dark-invariant] { color: var(--cam-fg); }

/* ── LIVE DETECTION BOXES ────────────────────────────────────────────────
   The overlay drawn by server/static/js/live-boxes.js, on every surface
   that carries a live <video>: /live tiles, the Command incident hero and
   its supporting tiles, the map popup's Live tab, and the lasso wall
   (which is /live).

   COLOUR LIVES HERE, NOT IN THE JS. Styling the SVG children by class is
   what makes var() legal, which deletes the read-tokens-through-
   getComputedStyle dance AND makes a light-theme leak structurally
   impossible: every colour below is a --cam-* token, and --cam-* is the
   one family tokens.css deliberately never redefines in the light block,
   because the video well stays dark in both themes.

   THREE FAMILIES, not eighty classes. person / vehicle / animal are the
   three questions an operator asks of a moving thing; the family is
   decided server-side (server/ai/live_box_classes.py) and travels in the
   payload, so this file and that one cannot disagree. */
/* CLIPPED TO THE TILE. Under object-fit: cover the media rect is LARGER
   than the container by design (media-rect.js returns negative offsets and
   a rect wider than the box, which is correct and must not be clamped) — so
   a detection in the cropped-away part of the frame maps to a box OUTSIDE
   the tile. With overflow visible those boxes painted onto whatever sat
   next to the tile: on /command a box from the top tile drew across the
   camera below it, which reads as a detection on the wrong camera. Clip at
   the tile edge: a box for something you cannot see must not be drawn, and
   it certainly must not be drawn over someone else's footage. */
.hlb-layer { position: absolute; pointer-events: none; z-index: 3;
  overflow: hidden; }
.hlb-layer svg { width: 100%; height: 100%; display: block; }

.hlb-det { transition: opacity var(--t-base) var(--ease); }
@media (prefers-reduced-motion: reduce) { .hlb-det { transition: none; } }

/* ── THE DUAL STROKE ────────────────────────────────────────────────
   A dark halo UNDER a bright edge, which is the same three-layer
   construction the coverage cones use on the map — that renderer solved
   "must read on any ground" first and this one reuses the approach.

   It is not decoration. Measured against pure white (concrete, a blown
   highlight, a headlight) the family colours are 1.50 / 1.93 / 2.44 : 1 —
   every one of them fails on its own, and the brightest colour fails
   WORST. The halo is what buys asphalt and concrete with one treatment,
   so neither stroke is optional; the pair is the guarantee. */
.hlb-det .casing {
  fill: none;
  stroke: var(--cam-evt-casing);
  stroke-width: 4.5;
  stroke-linejoin: round;
}

/* SOLID, and NO FILL. Outline only: a tinted fill sits on top of the one
   thing the operator is trying to look at. Solid because a dashed
   rectangle is the universal idiom for a SELECTION — a marquee, a crop
   handle, something the user is dragging — and a detection is a reading,
   not a selection. Dashes appear in exactly one place in this file, at the
   far end of the decay, and that is the whole point of them. */
.hlb-det .edge {
  fill: none;
  stroke: var(--cam-evt-object);
  stroke-width: 2;
  stroke-linejoin: round;
}
.hlb-det[data-fam="person"]  .edge { stroke: var(--cam-evt-person); }
.hlb-det[data-fam="vehicle"] .edge { stroke: var(--cam-evt-vehicle); }
.hlb-det[data-fam="animal"]  .edge { stroke: var(--cam-evt-animal); }

/* ── THE LABEL TAB ──────────────────────────────────────────────────
   A FILLED block of the family colour on the box's top-left corner,
   carrying dark ink — not floating text, and not a pill hovering nearby.
   Two reasons it is filled rather than scrimmed: it grounds its own text
   (§4's floor rule is satisfied by construction, on any footage), and it
   doubles the colour signal, so the family is legible even where the
   2px edge is competing with a busy background.

   The JS attaches it to the corner rather than floating it, so it reads
   as part of the box at any size. Ink is --cam-evt-ink at 12.9 / 10.0 /
   7.9 : 1 on the three families, pinned by tools/check_contrast.py. */
.hlb-det .tab   { fill: var(--cam-evt-object); }
.hlb-det[data-fam="person"]  .tab { fill: var(--cam-evt-person); }
.hlb-det[data-fam="vehicle"] .tab { fill: var(--cam-evt-vehicle); }
.hlb-det[data-fam="animal"]  .tab { fill: var(--cam-evt-animal); }
.hlb-det .tab-t {
  fill: var(--cam-evt-ink);
  font: 600 11px 'Geist', system-ui, sans-serif;
  letter-spacing: 0.01em;
}

/* ── DECAY — the honesty carrier ────────────────────────────────────
   A box that keeps drawing at full strength on a stale position is a
   lying instrument. The decay says so THREE ways: the whole group fades
   (set inline by the JS), the stroke THINS, and the tab stops naming a
   class and starts naming an age — at which point the box is a claim
   about WHEN, not about WHERE.

   THINNING, not dashing, carries the common case. The boxes run 0.75-2.5s
   behind the video on this pipeline, so a tile spends real time in decay:
   dashing there would make dashes the resting look of the feature and
   bring back the marquee read. The dash is held back for `late` — the
   last stretch before the box is dropped entirely at GONE_S — where it is
   rare enough to mean something. */
.hlb-det[data-state="stale"] .edge,
.hlb-det[data-state="late"]  .edge   { stroke-width: 1.25; }
.hlb-det[data-state="stale"] .casing,
.hlb-det[data-state="late"]  .casing { stroke-width: 3.25; }
.hlb-det[data-state="late"]  .edge   { stroke-dasharray: 3 5; }
.hlb-det[data-state="late"]  .casing { stroke-dasharray: 3 5; }

/* Density ladder: a wall cell is a glance, a one-up pane is a look. The JS
   omits the tab entirely at wall density — these rules are the belt to
   that braces, for a surface that sets the class without telling the JS. */
.hlb-layer[data-density="wall"] .hlb-det .tab,
.hlb-layer[data-density="wall"] .hlb-det .tab-t { display: none; }
.hlb-layer[data-density="wall"] .hlb-det .edge { stroke-width: 1.5; }
.hlb-layer[data-density="wall"] .hlb-det .casing { stroke-width: 3.5; }

/* UNDER THE TILE CHROME, EVERYWHERE. /live pins this to z-index 2 in its
   own stylesheet; /command's .cp-tile creates no stacking context, so the
   layer's z-index 3 competed directly with the incident badge (z-index 3)
   and the caption — a box could paint over the words naming the camera it
   belongs to. The overlay is a reading ABOUT the video; the chrome that
   says which camera this is outranks it. */
.cp-tile .hlb-layer { z-index: 1; }

/* THE TIGHTEN NOTE — said once per live surface, quietly (2026-08-19).
   The site is holding live video back so the boxes land on the subject;
   an operator watching a clock that looks slow must be able to find out
   why without asking. Quiet on purpose: it is a standing condition, not an
   alert, so it reads as a caption and never competes with the page. */
.hlb-held-note {
  display: inline-flex; align-items: center; gap: 5px;
  margin-left: 10px; padding: 2px 8px;
  border: 1px solid var(--line-2); border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--fg-3); font-size: 11px; white-space: nowrap;
}
.hlb-held-note::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--fg-3);
}

/* ── the no-footage card (.tile-nodata) — moved here from multicam_history.html 2026-09-02,
   shared with the /live solo dock (window.HeliosNoData in helios-timeline.js renders it) ── */
.tile-nodata {
  position: absolute; inset: 0; display: grid; place-items: center; z-index: 3;
  pointer-events: none;
}
.tile-nodata .nd-card {
  pointer-events: auto; text-align: center; padding: 12px 16px; border-radius: 12px;
  background: var(--surface);
  border: 1px solid var(--line-3); box-shadow: var(--shadow-lg); color: var(--fg);
  font-size: 12px; max-width: 86%;
}
.tile-nodata .nd-title { font-weight: 600; }
.tile-nodata .nd-near { color: var(--fg-2); margin-top: 4px; font: 500 12px 'Geist Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }
.tile-nodata .nd-jump {
  margin-top: 10px; height: 36px; padding: 0 14px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--accent-ink); border: 0; cursor: pointer;
  font: 600 12px 'Geist', system-ui, sans-serif;
}
.tile-nodata .nd-jump:hover { filter: brightness(1.08); }
.tile-nodata .nd-jump[disabled] { opacity: .5; cursor: default; }
.tile.cov-pending .tile-nodata .nd-title { color: var(--fg-2); }
.tile.cov-pending .tile-nodata .nd-near::after { content: ''; display: inline-block; width: 8px; height: 8px;
  margin-left: 8px; border-radius: 50%; border: 1.5px solid var(--fg-3); border-top-color: var(--fg-1);
  animation: covspin .9s linear infinite; vertical-align: -1px; }
@keyframes covspin { to { transform: rotate(360deg); } }

/* ── absence keys — SHARED by /history's .legend and the /live solo dock
   (window.HeliosGapLegend in helios-timeline.js mounts them; the paint's
   model shows a key only while its texture is on screen). The three
   swatches are the textures the canvas draws (helios-timeline.js
   tlHatchGap / tlHatchFault / tlDotsRetention): warm hatch, warm cross-
   hatch, grey dots — each over its own 2px underline. Tokens only; the
   canvas resolves the same ones at draw time. Keys, not toggles: no
   pointer, no pill (§5g). ── */
.gap-key {
  display: inline-flex; align-items: center; gap: 5px;
  cursor: default; color: var(--fg-2); font-size: 11px; white-space: nowrap;
}
/* `display` on a class beats the UA's `[hidden] {display:none}` */
.gap-key[hidden] { display: none !important; }
.gap-key .swatch-gap {
  width: 12px; height: 10px; border-radius: 2px; flex: none;
  box-shadow: inset 0 -2px 0 var(--warn);
}
.gap-key .swatch-gap-never {
  background: repeating-linear-gradient(45deg,
    color-mix(in srgb, var(--warn) 65%, transparent) 0 1.5px,
    color-mix(in srgb, var(--warn) 16%, transparent) 1.5px 4px);
}
.gap-key .swatch-gap-recorder {
  background:
    repeating-linear-gradient(45deg,
      color-mix(in srgb, var(--warn) 65%, transparent) 0 1px, transparent 1px 4px),
    repeating-linear-gradient(-45deg,
      color-mix(in srgb, var(--warn) 65%, transparent) 0 1px,
      color-mix(in srgb, var(--warn) 16%, transparent) 1px 4px);
}
.gap-key .swatch-gap-retention {
  box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--fg-3) 55%, transparent);
  background:
    radial-gradient(circle, color-mix(in srgb, var(--fg-3) 75%, transparent) 1px, transparent 1.3px) 0 0 / 4px 4px,
    color-mix(in srgb, var(--fg-3) 10%, transparent);
}
