@font-face{font-family:'Bebas Neue';font-style:normal;font-weight:400;font-display:swap;src:url('/fonts/bebas-neue.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face{font-family:'Inter';font-style:normal;font-weight:400 700;font-display:swap;src:url('/fonts/inter.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400 600;font-display:swap;src:url('/fonts/jetbrains-mono.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face{font-family:'Orbitron';font-style:normal;font-weight:500 900;font-display:swap;src:url('/fonts/orbitron.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}

/* ═══════════════════════════════════════════════════════════════════════════
   SENTINEL v4 / ParaN(oid)VR — "NIGHT PATROL"
   GTA-grounded neon design system. Presentation only.
   Fonts self-hosted (CSP: script-src 'self' blocks Google Fonts).
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── Base field ───────────────────────────────────────────────────────── */
  --bg:            #050a14;
  --bg-deep:       #03060d;

  /* GTA transparent-nav-map stacking: every plate is see-through, and the
     radar underneath is legible through all of them. Opacity climbs by tier. */
  --tier-0:        rgba(10, 18, 32, .34);   /* nav map plate  */
  --tier-1:        rgba(11, 20, 35, .52);   /* standard panel */
  --tier-2:        rgba(13, 23, 40, .68);   /* raised panel   */
  --tier-3:        rgba(7, 13, 24, .88);    /* modal / sheet  */
  --panel:         var(--tier-1);
  --panel-heavy:   var(--tier-3);

  --text:          #e9f4fb;
  --text-2:        rgba(233, 244, 251, .74);
  --muted:         rgba(150, 183, 202, .78);
  --faint:         rgba(150, 183, 202, .46);

  /* ── Neon ramp, pulled from the Night Patrol poster art ───────────────── */
  --neon-cyan:     #40e0ff;
  --neon-magenta:  #ff3ec8;
  --neon-violet:   #9d5cff;
  --neon-amber:    #ffb24d;
  --neon-lime:     #b6ff3c;

  /* ── Semantic state ramp. Every one of these is a real emitted class. ─── */
  --st-healthy:    #45e0a0;
  --st-warming:    #40e0ff;
  --st-degraded:   #ffb24d;   /* fixes the undefined --amber */
  --st-fallback:   #ff8a3c;
  --st-busy:       #c8a0ff;
  --st-offline:    #ff4d6d;
  --st-denied:     #7d8ea3;
  --amber:         var(--st-degraded);   /* legacy alias, was undefined */

  /* ── 9-camera neon tube colours, retuned into the poster family ───────── */
  --cam-1: #40e0ff;  --cam-2: #9d5cff;  --cam-3: #ff3ec8;
  --cam-4: #ffb24d;  --cam-5: #45e0a0;  --cam-6: #5b8cff;
  --cam-7: #ff5c72;  --cam-8: #33e0cf;  --cam-9: #ffe45c;

  /* ── Legacy aliases kept so nothing downstream breaks ─────────────────── */
  --cyan:  var(--neon-cyan);
  --green: var(--st-healthy);
  --red:   var(--st-offline);

  --line:        rgba(140, 200, 225, .13);
  --line-hi:     rgba(140, 200, 225, .26);
  --border:      var(--line);
  --neon-border: rgba(64, 224, 255, .28);

  /* ── Elevation ────────────────────────────────────────────────────────── */
  --sh-1: 0 2px 10px rgba(0,0,0,.28);
  --sh-2: 0 10px 28px rgba(0,0,0,.34), 0 2px 8px rgba(0,0,0,.24);
  --sh-3: 0 22px 54px rgba(0,0,0,.44), 0 3px 12px rgba(0,0,0,.30);
  --deep-shadow: var(--sh-2);
  --glow-cyan:   0 0 0 1px rgba(64,224,255,.26), 0 0 22px rgba(64,224,255,.16);
  --glass-shine: linear-gradient(168deg, rgba(255,255,255,.07) 0%, rgba(255,255,255,.015) 34%, transparent 62%);

  /* ── Radius scale (grounded, not bubbly) ──────────────────────────────── */
  --r-xs: 6px;  --r-sm: 10px;  --r-md: 14px;  --r-lg: 20px;  --r-xl: 26px;

  /* ── Type ─────────────────────────────────────────────────────────────── */
  --font-ui:      "Inter", system-ui, "Segoe UI", sans-serif;
  --font-mono:    "JetBrains Mono", ui-monospace, monospace;
  --font-display: "Orbitron", var(--font-ui);
  --font-title:   "Bebas Neue", "Oswald", var(--font-ui);

  /* ── Layout ───────────────────────────────────────────────────────────── */
  --topbar-h: 64px;
  --subnav-h: 52px;
  --max-w:    1480px;
  --page-pad: 18px;

  /* ── One clock. The radar sweep is 8s; everything breathes with it. ───── */
  --sweep:      8000ms;
  --beat:       640ms;
  --wipe:       460ms;
  --ease-out:   cubic-bezier(.16, 1, .3, 1);
  --ease-gta:   cubic-bezier(.62, .02, .12, 1);
}

/* ═══════════════════════════════════════════════════════════════════════════
   RESET
   ═══════════════════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
  min-height: 100%;
  font-family: var(--font-ui);
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
body { overflow-x: hidden; }
button, input, select, textarea { font-family: inherit; }
h1, h2, h3 { font-weight: 650; letter-spacing: -.01em; }

/* The GTA nav map. Sits under everything, read through every plate. */
/* ── Blur plane ──────────────────────────────────────────────────────────────
   A 1/8-scale mirror of #bg-canvas, stretched to the viewport. The browser's
   bilinear upscale is the blur, so panels can be translucent over a soft map
   without any live filter pass. This replaced backdrop-filter on the chrome. */
#bg-blur {
  position: fixed;
  inset: 0;
  /* Same stacking level as #bg-canvas but later in the document, so it paints
     over the sharp map. It only draws inside panel rects, so exposed map is
     untouched. Below .page-wrap (z-index 1), which holds all content. */
  z-index: 0;
  pointer-events: none;
  contain: strict;
  /* Bilinear, never nearest — the smoothing is the entire point. */
  image-rendering: auto;
}

#bg-canvas {
  position: fixed; inset: 0; width: 100%; height: 100%;
  z-index: 0; pointer-events: none;
}
/* Grounding wash so neon never floats on pure black */
body::before {
  content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(120% 80% at 50% -10%, rgba(157,92,255,.10), transparent 60%),
    radial-gradient(100% 70% at 50% 110%, rgba(64,224,255,.07), transparent 62%);
}

/* ═══════════════════════════════════════════════════════════════════════════
   SURFACE SYSTEM — transparent on transparent
   ═══════════════════════════════════════════════════════════════════════════ */
.glass {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--sh-2), inset 0 1px 0 rgba(255,255,255,.07);
  overflow: hidden;
}
.glass::before {
  content: ""; position: absolute; inset: 0;
  background: var(--glass-shine); pointer-events: none; z-index: 0;
}
.glass > * { position: relative; z-index: 1; }

/* Raised tier: anything that can raise an alarm */
.setting-card#control-card,
.setting-card#vlc-card,
.setting-card#capture-card,
.simple-restart-card,
#motion-preferences-card { background: var(--tier-2); }

/* ═══════════════════════════════════════════════════════════════════════════
   FOCUS + TOUCH — was entirely absent
   ═══════════════════════════════════════════════════════════════════════════ */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--neon-cyan);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
.cam-card:focus-visible { outline-color: var(--cc, var(--neon-cyan)); outline-offset: 3px; }
button:focus-visible, a:focus-visible, [tabindex]:focus-visible {
  box-shadow: 0 0 0 3px rgba(64,224,255,.22);
}
input:focus-visible, select:focus-visible {
  outline: none;
  border-color: rgba(64,224,255,.55) !important;
  box-shadow: 0 0 0 3px rgba(64,224,255,.14);
}

/* ═══════════════════════════════════════════════════════════════════════════
   BUTTONS
   ═══════════════════════════════════════════════════════════════════════════ */
.icon-btn {
  width: 44px; height: 44px; border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: rgba(13, 24, 40, .48);
  color: var(--text); font-size: 16px; cursor: pointer;
  display: grid; place-items: center; flex-shrink: 0;
  transition: border-color .16s, box-shadow .16s, background .16s;
}
.icon-btn:hover { border-color: rgba(64,224,255,.42); box-shadow: var(--glow-cyan); }
.icon-btn:active { transform: scale(.95); }

.primary, .pill {
  min-height: 44px;
  background: linear-gradient(135deg, rgba(64,224,255,.22), rgba(157,92,255,.20));
  border: 1px solid rgba(64,224,255,.42);
  border-radius: var(--r-sm); color: #fff;
  padding: 11px 18px; cursor: pointer;
  font-size: 13px; font-weight: 600; white-space: nowrap;
  letter-spacing: .01em;
  transition: box-shadow .16s, border-color .16s, transform .1s;
}
.primary:hover, .pill:hover {
  box-shadow: 0 0 0 1px rgba(64,224,255,.4), 0 0 26px rgba(64,224,255,.2);
}
button:active { transform: scale(.97); }

.chip {
  min-height: 34px; display: inline-flex; gap: 7px; align-items: center;
  padding: 0 13px; border: 1px solid var(--line);
  border-radius: 999px; color: var(--muted); font-size: 12px;
  background: rgba(255,255,255,.035);
  transition: border-color .16s, color .16s, background .16s;
}
button.chip { min-height: 40px; cursor: pointer; }
.chip i {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--st-denied); flex-shrink: 0;
  transition: background .3s, box-shadow .3s;
}
.chip.live { color: var(--st-healthy); border-color: rgba(69,224,160,.36); }
.chip.live i {
  background: var(--st-healthy);
  box-shadow: 0 0 9px var(--st-healthy);
  animation: neon-pulse var(--sweep) ease-in-out infinite;
}
.primary-chip {
  background: rgba(64,224,255,.16) !important;
  border-color: rgba(64,224,255,.48) !important;
  color: var(--neon-cyan) !important;
}
.danger-chip {
  background: rgba(180,40,58,.28) !important;
  border-color: rgba(255,77,109,.44) !important;
  color: #ff9aa8 !important;
}
.danger-chip:hover { background: rgba(200,50,66,.46) !important; }

kbd {
  display: inline-block; padding: 3px 7px;
  background: rgba(255,255,255,.06); border: 1px solid var(--line);
  border-radius: var(--r-xs); font-family: var(--font-mono);
  font-size: 10px; color: var(--muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   BOOT VEIL — a five-second minimum, with real bootstrap state supplied by
   app.js. It is visual only in the sense that it never blocks work.
   ═══════════════════════════════════════════════════════════════════════════ */
.boot-veil {
  position: fixed; inset: 0; z-index: 9000;
  background: var(--bg-deep);
  overflow: hidden;
  contain: strict;
}
.boot-veil.boot-complete { animation: bv-dismiss .72s var(--ease-gta) forwards; pointer-events: none; }
@keyframes bv-dismiss {
  0%        { opacity: 1; visibility: visible; }
  99.9%     { opacity: 0; visibility: visible; }
  100%      { opacity: 0; visibility: hidden; pointer-events: none; }
}

.bv-frame { position: absolute; inset: 0; overflow: hidden; animation: bv-frame-in .55s var(--ease-out) both; }
@keyframes bv-frame-in {
  0%   { opacity: 0; transform: scale(1.06); filter: blur(18px) brightness(1.6) saturate(1.4); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: scale(1);    filter: blur(0)    brightness(1)   saturate(1); }
}

/* Artwork. Source is set by /splash.js via a data-scene attribute on <html>. */
.bv-art {
  position: absolute; inset: 0;
  background-position: center; background-size: cover; background-repeat: no-repeat;
  background-color: var(--bg-deep);
  animation: bv-kenburns 3.6s linear both;
  will-change: transform;
}
@keyframes bv-kenburns {
  0%   { transform: scale(1.005) translate3d(0, 0, 0); }
  100% { transform: scale(1.075) translate3d(0, -1.6%, 0); }
}

/* Neon breathe over the sign zone of the artwork */
.bv-neon {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(42% 26% at 50% 84%, rgba(255,62,200,.30), transparent 70%),
    radial-gradient(38% 22% at 50% 88%, rgba(64,224,255,.22), transparent 72%);
  mix-blend-mode: screen;
  animation: neon-buzz 2.4s steps(1, end) infinite;
}
/* Rotating god rays */
.bv-rays {
  position: absolute; left: 50%; top: 46%;
  width: 190vmax; height: 190vmax; margin: -95vmax 0 0 -95vmax;
  pointer-events: none; opacity: .16; mix-blend-mode: screen;
  background: repeating-conic-gradient(from 0deg,
    rgba(255,62,200,.5) 0deg 3deg, transparent 3deg 13deg,
    rgba(64,224,255,.4) 13deg 15deg, transparent 15deg 27deg);
  animation: rays-spin 26s linear infinite;
}
/* CRT scan sweep */
.bv-scan {
  position: absolute; left: -10%; width: 120%; height: 14%; top: -20%;
  pointer-events: none; mix-blend-mode: screen;
  background: linear-gradient(180deg, transparent, rgba(190,240,255,.13) 46%, rgba(255,62,200,.10) 54%, transparent);
  animation: scan-run 2.1s linear infinite;
}
/* Film grain */
.bv-grain {
  position: absolute; inset: -14%; pointer-events: none; opacity: .14;
  background-image: radial-gradient(rgba(255,255,255,.7) .5px, transparent .5px);
  background-size: 3px 3px;
  animation: grain-jump .5s steps(2, end) infinite;
}
.bv-vig {
  position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 20vmax 6vmax rgba(3,6,13,.72);
}

/* Corner stamp + honest boot line */
.bv-stamp {
  position: absolute; top: max(16px, env(safe-area-inset-top)); left: 18px; z-index: 4;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .22em;
  text-transform: uppercase; color: rgba(255,255,255,.72);
  text-shadow: 0 0 14px rgba(64,224,255,.6), 0 1px 4px rgba(0,0,0,.9);
  animation: neon-breathe 2.2s ease-in-out infinite;
}
.bv-status {
  position: absolute; z-index: 5; left: clamp(18px, 6vw, 76px); bottom: clamp(42px, 12vh, 112px);
  width: min(460px, calc(100vw - 36px)); display: grid; gap: 7px;
  padding: 15px 17px; border: 1px solid rgba(130,220,255,.26); border-radius: var(--r-sm);
  background: rgba(2,8,16,.56); box-shadow: 0 16px 46px rgba(0,0,0,.36), 0 0 30px rgba(64,224,255,.10);
}
.bv-status strong { font: 400 24px/1 var(--font-title); letter-spacing: .08em; text-transform: uppercase; color: #fff; }
.bv-status > span { min-height: 15px; color: rgba(218,244,255,.76); font: 10px/1.45 var(--font-mono); letter-spacing: .08em; text-transform: uppercase; }
.bv-steps { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 5px; margin: 4px 0 0; padding: 0; list-style: none; }
.bv-steps li { min-width: 0; color: rgba(182,209,222,.46); font: 8px/1.2 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
.bv-steps li::before { content: ""; display: block; height: 3px; margin-bottom: 5px; background: rgba(181,215,232,.18); box-shadow: none; }
.bv-steps li.active, .bv-steps li.done { color: rgba(232,249,255,.92); }
.bv-steps li.active::before { background: var(--neon-cyan); box-shadow: 0 0 13px rgba(64,224,255,.75); animation: boot-step-pulse 900ms ease-in-out infinite; }
.bv-steps li.done::before { background: var(--neon-magenta); box-shadow: 0 0 10px rgba(255,62,200,.42); }
.bv-bar {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 5;
  height: 3px; background: rgba(255,255,255,.10); overflow: hidden;
}
.bv-bar::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, var(--neon-magenta), var(--neon-violet) 45%, var(--neon-cyan));
  box-shadow: 0 0 16px rgba(255,62,200,.7);
  transform-origin: left center; transform: scaleX(var(--boot-progress, .04));
  transition: transform .32s var(--ease-out);
}

/* Exit: GTA flip-wipe up + flash */
.bv-wipe {
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: linear-gradient(180deg, var(--bg-deep) 0 62%, rgba(255,62,200,.9) 68%, rgba(64,224,255,.9) 72%, transparent 78%);
  transform: translateY(101%);
}
.boot-veil.boot-complete .bv-wipe { animation: bv-wipe-up .62s var(--ease-gta) forwards; }
@keyframes bv-wipe-up { to { transform: translateY(-101%); } }
.bv-flash {
  position: absolute; inset: 0; z-index: 7; pointer-events: none;
  background: #fff; opacity: 0;
}
.boot-veil.boot-complete .bv-flash { animation: bv-flash .34s ease-out .20s forwards; }
@keyframes bv-flash { 0% { opacity: 0; } 22% { opacity: .5; } 100% { opacity: 0; } }

/* Orientation-aware artwork. Only the matching pair is ever fetched, because
   a background-image inside a non-matching media query is never requested. */
@media (orientation: landscape) {
  html[data-scene="night-patrol"]  .bv-art { background-image: url("./splash/night-patrol-landscape.webp"); }
  html[data-scene="grocery-run"]   .bv-art { background-image: url("./splash/grocery-run-landscape.webp"); }
  html[data-scene="specter-sweep"] .bv-art { background-image: url("./splash/specter-sweep-landscape.webp"); }
}
@media (orientation: portrait) {
  html[data-scene="night-patrol"]  .bv-art { background-image: url("./splash/night-patrol-portrait.webp"); }
  html[data-scene="grocery-run"]   .bv-art { background-image: url("./splash/grocery-run-portrait.webp"); }
  html[data-scene="specter-sweep"] .bv-art { background-image: url("./splash/specter-sweep-portrait.webp"); }
}

/* Legacy splash block (app.js removes #splash on load) */
.splash { position: fixed; inset: 0; z-index: 200; background: var(--bg);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px;
  transition: opacity .5s ease; }
.splash.fade-out { opacity: 0; pointer-events: none; }
.splash-icon { width: 96px; height: 96px; border-radius: var(--r-lg);
  box-shadow: 0 0 40px rgba(64,224,255,.28); object-fit: cover; }
.splash-wordmark { font-family: var(--font-title); font-size: 34px; letter-spacing: .04em; }
.splash-wordmark span { color: var(--neon-cyan); }
.splash-ring { width: 48px; height: 48px; border-radius: 50%;
  border: 3px solid var(--line); border-top-color: var(--neon-cyan); animation: spin 1s linear infinite; }
.splash-status { font-family: var(--font-mono); font-size: 12px; color: var(--muted); }

/* ═══════════════════════════════════════════════════════════════════════════
   TOPBAR
   ═══════════════════════════════════════════════════════════════════════════ */
.topbar {
  position: sticky; top: 0; z-index: 40;
  height: var(--topbar-h);
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 0 20px;
  max-width: var(--max-w); margin: 0 auto;
  background: var(--tier-2);
  border-radius: 0 0 var(--r-md) var(--r-md); border-top: none;
}
.topbar::after {
  content: ""; position: absolute; bottom: 0; left: -10%; width: 30%; height: 1px;
  background: linear-gradient(90deg, transparent, var(--neon-cyan), transparent);
  animation: shimmer 7s linear infinite;
}
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
.brand-wordmark {
  display: block;
  width: 136px;
  max-width: 100%;
  height: auto;
  object-fit: contain;
}
@media (max-width: 520px) {
  .brand-wordmark { width: 116px; }
}
@media (max-width: 380px) {
  .brand-wordmark { width: 100px; }
}
.brand-icon {
  width: 38px; height: 38px; border-radius: var(--r-sm);
  object-fit: cover; flex-shrink: 0;
  box-shadow: 0 0 0 1px rgba(64,224,255,.24), 0 0 18px rgba(64,224,255,.16);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.1; min-width: 0; }
.brand-text strong {
  font-family: var(--font-title); font-size: 21px; font-weight: 400;
  letter-spacing: .045em; text-transform: uppercase;
  background: linear-gradient(96deg, #fff 8%, var(--neon-magenta) 46%, var(--neon-cyan) 92%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.brand-text span {
  font-family: var(--font-mono); font-size: 9.5px; color: var(--faint);
  letter-spacing: .2em; text-transform: uppercase;
}
.topbar-right { display: flex; align-items: center; gap: 9px; }
.topbar-clock {
  font-family: var(--font-mono); font-size: 12px; color: var(--muted);
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
}

/* ═══════════════════════════════════════════════════════════════════════════
   SECTION NAV — GTA chapter tabs
   ═══════════════════════════════════════════════════════════════════════════ */
.section-nav {
  position: sticky; top: var(--topbar-h); z-index: 39;
  display: flex; align-items: center; gap: 4px;
  height: var(--subnav-h); padding: 0 16px;
  max-width: var(--max-w); margin: 8px auto 0;
  background: var(--tier-1);
  border-radius: var(--r-md);
  overflow-x: auto; scrollbar-width: none;
}
.section-nav::-webkit-scrollbar { display: none; }
.nav-btn {
  position: relative; flex-shrink: 0;
  min-height: 44px; padding: 10px 17px;
  border-radius: var(--r-sm); border: 1px solid transparent;
  background: none; color: var(--muted);
  font-family: var(--font-title); font-size: 16px; font-weight: 400;
  letter-spacing: .09em; text-transform: uppercase; cursor: pointer;
  transition: color .2s, background .2s;
}
.nav-btn::after {
  content: ""; position: absolute; left: 14px; right: 14px; bottom: 5px;
  height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, var(--neon-magenta), var(--neon-cyan));
  transform: scaleX(0); transform-origin: center;
  transition: transform .34s var(--ease-out);
}
.nav-btn:hover { color: var(--text-2); background: rgba(255,255,255,.04); }
.nav-btn.active { color: #fff; background: rgba(64,224,255,.08); }
.nav-btn.active::after { transform: scaleX(1); box-shadow: 0 0 12px rgba(255,62,200,.6); }
.nav-badge {
  position: absolute; top: 0; right: 0; transform: translate(40%, -40%);
  min-width: 17px; height: 17px; padding: 0 5px; border-radius: 999px;
  background: var(--st-offline); font-family: var(--font-mono);
  font-size: 9px; font-weight: 700; color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 0 12px -2px var(--st-offline);
}

/* ═══════════════════════════════════════════════════════════════════════════
   PAGE & SECTIONS — flip-wipe on arrival
   ═══════════════════════════════════════════════════════════════════════════ */
.page-wrap {
  position: relative; z-index: 1;
  max-width: var(--max-w); margin: 0 auto;
  padding: 0 var(--page-pad) 40px;
  perspective: 1400px;
}
.section {
  padding-top: 24px;
  /* preserve-3d was permanent, which forced every descendant into its own 3D
     rendering context. With ten camera cards each running non-compositable
     box-shadow/drop-shadow keyframes underneath it, Chromium promoted and
     demoted card layers continuously — that was the flicker, and each demotion
     re-composited the 48%-alpha card against the map behind it. It is now
     scoped to the frames that actually need depth. */
  transform-style: flat;
  animation: flip-wipe-in .62s var(--ease-gta) both;
}
.section.ui-glitch-enter, .section.ui-exit { transform-style: preserve-3d; }

/* A parked section keeps its <video> in the render tree on purpose, but there
   is no reason for forty neon keyframes to keep running off-screen. */
.page-wrap > .section:not(.is-active) *,
.page-wrap > .section:not(.is-active) *::before,
.page-wrap > .section:not(.is-active) *::after { animation-play-state: paused !important; }
/* Scroll-driven where supported: the flip fires as the section arrives.
   Where unsupported it degrades to a single entry animation on load. */
@supports (animation-timeline: view()) {
  .section {
    animation: flip-wipe-in linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 46%;
  }
}
@keyframes flip-wipe-in {
  0%   { opacity: 0; transform: perspective(1400px) rotateX(7deg) translateY(26px) scale(.985);
         clip-path: inset(0 0 100% 0); filter: brightness(1.5) saturate(1.3); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: perspective(1400px) rotateX(0) translateY(0) scale(1);
         clip-path: inset(0 0 0 0); filter: brightness(1) saturate(1); }
}
.section-hdr {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 16px; gap: 12px;
}
.section-hdr h2 {
  font-family: var(--font-title); font-weight: 400;
  font-size: 26px; letter-spacing: .07em; text-transform: uppercase;
  background: linear-gradient(94deg, #fff 12%, var(--neon-cyan) 88%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.section-hdr-meta {
  color: var(--faint); font-family: var(--font-mono);
  font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
}

/* ═══════════════════════════════════════════════════════════════════════════
   INSTRUMENT CLUSTER (stat bar)
   ═══════════════════════════════════════════════════════════════════════════ */
.stat-bar {
  display: flex; align-items: center; gap: 0;
  padding: 13px 20px; margin-bottom: 14px;
  border-radius: var(--r-md); overflow-x: auto; scrollbar-width: none;
  background: var(--tier-1);
}
.stat-bar::-webkit-scrollbar { display: none; }
.sb-item { display: flex; flex-direction: column; gap: 3px; padding: 0 20px; flex-shrink: 0; }
.sb-item:first-child { padding-left: 0; }
.sb-num {
  font-family: var(--font-display); font-size: 23px; font-weight: 700;
  line-height: 1; font-variant-numeric: tabular-nums; color: var(--text);
  letter-spacing: .01em;
}
.sb-num.accent { color: var(--neon-cyan); text-shadow: 0 0 22px rgba(64,224,255,.42); }
.sb-num.recording-on {
  color: var(--st-offline); text-shadow: 0 0 20px rgba(255,77,109,.4);
  animation: neon-pulse 2.4s ease-in-out infinite;
}
.sb-label {
  font-family: var(--font-mono); font-size: 9px; font-weight: 500;
  text-transform: uppercase; letter-spacing: .16em; color: var(--faint);
}
.sb-sep { width: 1px; height: 26px; background: var(--line); flex-shrink: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   WALL — constant pulsing bezel, state-driven, synced to the radar sweep
   ═══════════════════════════════════════════════════════════════════════════ */
.wall-wrap {
  position: relative; background: #01040a;
  border-radius: var(--r-md);
  border: 1px solid rgba(64,224,255,.20);
  aspect-ratio: 16/9; overflow: hidden;
  --wall-hue: var(--neon-cyan);
  /* The video wall stays isolated from the navigation-map canvas. */
  isolation: isolate; z-index: 2;
  animation: wall-breathe var(--sweep) ease-in-out infinite;
}
@keyframes wall-breathe {
  0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--wall-hue) 22%, transparent),
                         0 0 24px -4px color-mix(in srgb, var(--wall-hue) 30%, transparent),
                         inset 0 0 40px -22px color-mix(in srgb, var(--wall-hue) 60%, transparent); }
  50%      { box-shadow: 0 0 0 1px color-mix(in srgb, var(--wall-hue) 52%, transparent),
                         0 0 46px -2px color-mix(in srgb, var(--wall-hue) 52%, transparent),
                         inset 0 0 62px -18px color-mix(in srgb, var(--wall-hue) 85%, transparent); }
}
/* Corner brackets, GTA HUD framing */
.wall-wrap::after {
  content: ""; position: absolute; inset: 9px; pointer-events: none; z-index: 4;
  border: 1px solid transparent;
  background:
    linear-gradient(var(--wall-hue), var(--wall-hue)) left  top    / 22px 1px no-repeat,
    linear-gradient(var(--wall-hue), var(--wall-hue)) left  top    / 1px 22px no-repeat,
    linear-gradient(var(--wall-hue), var(--wall-hue)) right top    / 22px 1px no-repeat,
    linear-gradient(var(--wall-hue), var(--wall-hue)) right top    / 1px 22px no-repeat,
    linear-gradient(var(--wall-hue), var(--wall-hue)) left  bottom / 22px 1px no-repeat,
    linear-gradient(var(--wall-hue), var(--wall-hue)) left  bottom / 1px 22px no-repeat,
    linear-gradient(var(--wall-hue), var(--wall-hue)) right bottom / 22px 1px no-repeat,
    linear-gradient(var(--wall-hue), var(--wall-hue)) right bottom / 1px 22px no-repeat;
  opacity: .55;
}
/* Health drives the bezel colour. #sentinel-health-state is always in the DOM. */
body:has(#sentinel-health-state.sentinel-healthy)     .wall-wrap { --wall-hue: var(--st-healthy); }
body:has(#sentinel-health-state.sentinel-degraded)    .wall-wrap { --wall-hue: var(--st-degraded); }
body:has(#sentinel-health-state.sentinel-recovering)  .wall-wrap { --wall-hue: var(--st-degraded); }
body:has(#sentinel-health-state.sentinel-offline)     .wall-wrap { --wall-hue: var(--st-offline); }
/* Recovering also gets a highlight travelling the perimeter */
body:has(#sentinel-health-state.sentinel-recovering) .wall-wrap,
.wall-wrap:has(#wall-state.reconnecting) { animation-duration: 2.6s; }
.wall-wrap:has(#wall-state.reconnecting)::before {
  content: ""; position: absolute; inset: -1px; z-index: 5; pointer-events: none;
  border-radius: inherit; padding: 1px;
  background: conic-gradient(from 0turn, transparent 0 68%,
              var(--st-degraded) 82%, #fff 88%, var(--st-degraded) 94%, transparent 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  animation: perimeter-run 2.6s linear infinite;
}
@keyframes perimeter-run { to { transform: rotate(1turn); } }

.wall-wrap video { display: block; width: 100%; height: 100%; object-fit: contain; background: #01040a; }
.wall-status {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 9px;
  color: var(--muted); font-size: 13px; z-index: 3;
  background: radial-gradient(70% 70% at 50% 50%, rgba(2,5,10,.62), rgba(2,5,10,.86));
}
.wall-status strong {
  color: var(--text); font-family: var(--font-title); font-weight: 400;
  font-size: 22px; letter-spacing: .06em; text-transform: uppercase;
}
.wall-status span { max-width: 86%; text-align: center; font-family: var(--font-mono); font-size: 11px; }
.wall-status.reconnecting strong {
  color: var(--st-degraded);
  animation: reconnectPulse 1.6s ease-in-out infinite;
}
.wall-status.reconnecting {
  background-color: #03070d;
  background-image:
    repeating-linear-gradient(0deg, rgba(220,242,255,.07) 0 1px, transparent 1px 4px),
    repeating-linear-gradient(90deg, rgba(255,255,255,.045) 0 1px, transparent 1px 3px),
    radial-gradient(70% 70% at 50% 50%, rgba(32,76,98,.42), rgba(2,5,10,.92));
  animation: tv-static 170ms steps(2, end) infinite;
}
@keyframes tv-static { 0% { background-position: 0 0, 0 0, center; } 50% { background-position: 0 3px, 2px 0, center; } 100% { background-position: 0 -2px, -1px 0, center; } }
.wall-status.hidden { display: none; }
/* Standby art gets a slow drift instead of sitting there looking crashed */
.wall-wrap:has(#wall-state:not(.hidden))::after { opacity: .85; }
.sentinel-healthy    { color: var(--st-healthy)  !important; }
.sentinel-degraded,
.sentinel-recovering { color: var(--st-degraded) !important; }
.sentinel-offline    { color: var(--st-offline)  !important; }
.wall-actions { position: absolute; top: 12px; right: 12px; display: flex; gap: 8px; z-index: 6; }
#wall-frame:fullscreen video, #wall-frame:-webkit-full-screen video { object-fit: cover; }
#wall-frame:fullscreen, #wall-frame:-webkit-full-screen { animation: none; border-radius: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   CAMERA CARDS — neon tubes, one per camera, phase-offset around the sweep
   ═══════════════════════════════════════════════════════════════════════════ */
/* Cameras live inside the Wall section now. */
.wall-cameras { margin-top: 22px; }
.section-hdr-inline { margin-bottom: 12px; }

.sb-item { min-width: 0; }
.sb-num { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.cam-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }

.cam-card[data-idx="0"] { --cc: var(--cam-1); --phase: 0ms; }
.cam-card[data-idx="1"] { --cc: var(--cam-2); --phase: -889ms; }
.cam-card[data-idx="2"] { --cc: var(--cam-3); --phase: -1778ms; }
.cam-card[data-idx="3"] { --cc: var(--cam-4); --phase: -2667ms; }
.cam-card[data-idx="4"] { --cc: var(--cam-5); --phase: -3556ms; }
.cam-card[data-idx="5"] { --cc: var(--cam-6); --phase: -4444ms; }
.cam-card[data-idx="6"] { --cc: var(--cam-7); --phase: -5333ms; }
.cam-card[data-idx="7"] { --cc: var(--cam-8); --phase: -6222ms; }
.cam-card[data-idx="8"] { --cc: var(--cam-9); --phase: -7111ms; }
.cam-card[data-idx="9"] { --cc: var(--cam-10, #6ee7ff); --phase: -8000ms; }

.cam-card {
  --cc: var(--neon-cyan); --phase: 0ms;
  position: relative; aspect-ratio: 16/10;
  border-radius: var(--r-md);
  border: 1px solid color-mix(in srgb, var(--cc) 26%, transparent);
  /* Opaque. At 48% the background map read straight through any part of the
     card the thumbnail did not cover. */
  background: #070f1a;
  overflow: hidden; cursor: pointer; touch-action: manipulation;
  display: flex; flex-direction: column; justify-content: flex-end;
  transition: transform .22s var(--ease-out), border-color .22s;
  isolation: isolate;          /* children cannot escape into the page stack */
  contain: layout;             /* not `paint` — that would clip the outer bloom */
  /* Outer bloom stays on the card, where overflow:hidden cannot clip it. Only
     the inset core breathes, on ::before, as opacity. */
  box-shadow: 0 0 20px -7px color-mix(in srgb, var(--cc) 52%, transparent);
  /* The tube breath is now an opacity animation on ::before — same look,
     compositor-only, instead of animating box-shadow on the main thread. */
  animation: none;
}
@keyframes tube-breathe {
  0%, 100% { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 18%, transparent),
                         0 0 14px -6px color-mix(in srgb, var(--cc) 45%, transparent); }
  50%      { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 62%, transparent),
                         inset 0 0 12px -6px color-mix(in srgb, var(--cc) 70%, transparent),
                         0 0 26px -4px color-mix(in srgb, var(--cc) 62%, transparent); }
}
/* Gradient top-rail, the Giorgio/Pharrell signature move */
.cam-card::after {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px; z-index: 3;
  background: linear-gradient(90deg, transparent, var(--cc), transparent);
  transform: scaleX(0); transform-origin: left center;
  transition: transform .42s var(--ease-out);
}
.cam-card:focus-visible::after { transform: scaleX(1); }
@media (hover: hover) and (pointer: fine) {
  .cam-card:hover::after { transform: scaleX(1); }
}
.cam-card::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: var(--glass-shine); pointer-events: none;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 62%, transparent),
              inset 0 0 12px -6px color-mix(in srgb, var(--cc) 70%, transparent);
  animation: tube-breathe-opacity var(--sweep) ease-in-out infinite;
  animation-delay: var(--phase);
}
@keyframes tube-breathe-opacity { 0%, 100% { opacity: .34; } 50% { opacity: 1; } }
@media (hover: hover) and (pointer: fine) {
  .cam-card:hover {
    transform: translateY(-3px);
    border-color: color-mix(in srgb, var(--cc) 70%, transparent);
  }
  .cam-card:hover::before { animation: tube-strike-opacity 1.1s steps(1, end) infinite; }
}
@keyframes tube-strike-opacity {
  0%, 84% { opacity: 1; } 86% { opacity: .3; } 88% { opacity: 1; }
  92% { opacity: .45; } 94%, 100% { opacity: 1; }
}
/* Dead tube: no bloom, grey, one stutter every 12s */
.cam-card.offline {
  opacity: .46; cursor: default;
  border-color: rgba(125,142,163,.22);
  animation: tube-dead 12s steps(1, end) infinite;
}
@media (hover: hover) and (pointer: fine) {
  .cam-card.offline:hover { transform: none; animation: tube-dead 12s steps(1, end) infinite; }
}
@keyframes tube-dead {
  0%, 92%   { box-shadow: inset 0 0 0 1px rgba(125,142,163,.14); }
  93%       { box-shadow: inset 0 0 0 1px rgba(125,142,163,.5); }
  94%, 95%  { box-shadow: inset 0 0 0 1px rgba(125,142,163,.10); }
  96%       { box-shadow: inset 0 0 0 1px rgba(125,142,163,.42); }
  97%, 100% { box-shadow: inset 0 0 0 1px rgba(125,142,163,.14); }
}

.cam-thumb { position: absolute; inset: 0; z-index: 0; overflow: hidden; background: #02060e; background-size: cover; background-position: center; }
.cam-thumb-frame {
  position: absolute; inset: 0; z-index: 0;
  background-position: center; background-size: cover; background-repeat: no-repeat;
  opacity: 0; transition: opacity .46s var(--ease-out);
  pointer-events: none;
}
.cam-thumb-frame.is-active { opacity: 1; }
.cam-grid.is-scrolling .cam-thumb-frame { transition: none; }
.cam-thumb video { width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity .4s ease; display: block; }
/* Slow Ken Burns on a still frame. transform only — free on the compositor,
   and it turns ten screenshots into ten windows. */
.cam-thumb.has-frame {
  background-size: 118% auto;
  animation: thumb-drift 26s ease-in-out infinite alternate;
  animation-delay: var(--phase, 0s);
}
@keyframes thumb-drift {
  0%   { transform: scale(1)     translate3d(0, 0, 0); }
  100% { transform: scale(1.085) translate3d(-2.2%, -1.6%, 0); }
}
@media (pointer: coarse) { .cam-thumb.has-frame { animation-duration: 40s; } }
.cam-thumb video.visible { opacity: 1; }

.cam-footer {
  position: relative; z-index: 2; padding: 9px 11px 10px;
  background: linear-gradient(transparent, rgba(3, 8, 16, .90) 42%);
  display: flex; align-items: center; gap: 9px;
}
.cam-slot {
  width: 26px; height: 26px; border-radius: var(--r-xs); flex-shrink: 0;
  background: color-mix(in srgb, var(--cc) 15%, transparent);
  border: 1px solid color-mix(in srgb, var(--cc) 46%, transparent);
  color: var(--cc); font-family: var(--font-display); font-size: 11px; font-weight: 700;
  display: grid; place-items: center;
  box-shadow: 0 0 12px -4px var(--cc);
}
.cam-info { flex: 1; min-width: 0; }
.cam-info strong { display: block; font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cam-info small { color: var(--faint); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; }
.cam-dot { width: 7px; height: 7px; border-radius: 50%; background: #3a5060; flex-shrink: 0; }
.cam-card:not(.offline) .cam-dot { box-shadow: 0 0 20px var(--cc), 0 0 34px var(--cc); }
.cam-card:not(.offline) .cam-dot {
  background: var(--cc); box-shadow: 0 0 10px var(--cc);
  animation: neon-pulse var(--sweep) ease-in-out infinite; animation-delay: var(--phase);
}
.cam-kbd { position: absolute; top: 8px; right: 9px; z-index: 3; opacity: 0; transition: opacity .18s; }
@media (hover: hover) and (pointer: fine) {
  .cam-card:hover .cam-kbd { opacity: 1; }
}

/* ── Nine signal signatures ───────────────────────────────────────────────
   app.js builds the card markup and cannot be edited, so these ride on the
   two free pseudo-elements of .cam-thumb rather than a dedicated element.
   You learn a camera by its motion before you read its label. ─────────────── */
.cam-thumb::before, .cam-thumb::after { transform-origin: top left; }
.cam-thumb::before, .cam-thumb::after {
  content: ""; position: absolute; z-index: 2; pointer-events: none;
  background: var(--cc); opacity: .85;
}
.cam-card.offline .cam-thumb::before,
.cam-card.offline .cam-thumb::after { background: #7d8ea3; opacity: .22; animation: none !important; }
/* Default: no signature until an index claims one */
.cam-thumb::before, .cam-thumb::after { display: none; }

/* 1 scanline crawl */
.cam-card[data-idx="0"] .cam-thumb::before { display:block; left:10px; width:26px; height:1.5px; top:10px; animation: sig-crawl 2.6s linear infinite; }
/* 2 radial ping */
.cam-card[data-idx="1"] .cam-thumb::before { display:block; left:12px; top:12px; width:6px; height:6px; border-radius:50%; animation: sig-ping 2.2s ease-out infinite; }
/* 3 waveform ticks */
.cam-card[data-idx="2"] .cam-thumb::before { display:block; left:10px; top:14px; width:2px; height:4px; box-shadow:5px 0 var(--cc),10px 0 var(--cc),15px 0 var(--cc),20px 0 var(--cc); animation: sig-wave 1.4s ease-in-out infinite; }
/* 4 iris bloom */
.cam-card[data-idx="3"] .cam-thumb::before { display:block; left:11px; top:10px; width:13px; height:13px; border-radius:50%; background:none; border:1.5px solid var(--cc); animation: sig-iris 3.1s ease-in-out infinite; }
/* 5 twin sweep */
.cam-card[data-idx="4"] .cam-thumb::before { display:block; left:10px; top:12px; width:9px; height:1.5px; animation: sig-sweep 2.4s ease-in-out infinite; }
.cam-card[data-idx="4"] .cam-thumb::after  { display:block; left:10px; top:18px; width:9px; height:1.5px; animation: sig-sweep 2.4s ease-in-out infinite reverse; }
/* 6 stack bars */
.cam-card[data-idx="5"] .cam-thumb::before { display:block; left:10px; top:11px; width:3px; height:10px; box-shadow:6px 2px var(--cc),12px -2px var(--cc),18px 3px var(--cc); animation: sig-stack 1.9s ease-in-out infinite; }
/* 7 alarm blink */
.cam-card[data-idx="6"] .cam-thumb::before { display:block; left:11px; top:10px; width:10px; height:10px; clip-path: polygon(50% 0,100% 100%,0 100%); animation: sig-blink 1.5s steps(1,end) infinite; }
/* 8 orbit dot */
.cam-card[data-idx="7"] .cam-thumb::before { display:block; left:16px; top:10px; width:4px; height:4px; border-radius:50%; transform-origin: 2px 9px; animation: sig-orbit 3.4s linear infinite; }
/* 9 pulse grid */
.cam-card[data-idx="8"] .cam-thumb::before { display:block; left:10px; top:11px; width:3px; height:3px; box-shadow:7px 0 var(--cc),14px 0 var(--cc),0 7px var(--cc),7px 7px var(--cc),14px 7px var(--cc); animation: sig-grid 2.8s ease-in-out infinite; }

@keyframes sig-crawl { 0%,100% { transform:translateY(0);    opacity:.25 } 50% { transform:translateY(14px); opacity:1 } }
@keyframes sig-ping  { 0% { transform:scale(.4); opacity:1 } 100% { transform:scale(2.8); opacity:0 } }
@keyframes sig-wave  { 0%,100% { transform:scaleY(1) } 50% { transform:scaleY(3) } }
@keyframes sig-iris  { 0%,100% { transform:scale(.55); opacity:.5 } 50% { transform:scale(1); opacity:1 } }
@keyframes sig-sweep { 0%,100% { transform:translateX(0); opacity:.35 } 50% { transform:translateX(16px); opacity:1 } }
@keyframes sig-stack { 0%,100% { transform:scaleY(.5); opacity:.5 } 50% { transform:scaleY(1); opacity:1 } }
@keyframes sig-blink { 0%,60% { opacity:1 } 61%,100% { opacity:.18 } }
@keyframes sig-orbit { to { transform: rotate(360deg) } }
@keyframes sig-grid  { 0%,100% { opacity:.3 } 50% { opacity:1 } }

/* ═══════════════════════════════════════════════════════════════════════════
   EVENTS
   ═══════════════════════════════════════════════════════════════════════════ */
.events-list { display: flex; flex-direction: column; gap: 8px; }
.events-empty {
  min-height: 90px; display: grid; place-items: center; color: var(--faint);
  font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .1em; text-transform: uppercase;
  border: 1px dashed var(--line); border-radius: var(--r-md); padding: 20px;
}
.event-row {
  --ev-cc: var(--neon-cyan);
  display: flex; align-items: center; gap: 12px; padding: 11px 14px;
  background: rgba(9, 18, 30, .44);
  border: 1px solid var(--line); border-left: 3px solid var(--ev-cc);
  border-radius: var(--r-sm); cursor: pointer; transition: background .14s, border-color .14s;
}
.event-row:hover { background: rgba(15, 30, 47, .6); border-color: color-mix(in srgb, var(--ev-cc) 44%, transparent); }
.ev-thumb { width: 64px; height: 48px; flex-shrink: 0; border-radius: var(--r-xs); overflow: hidden; background: #02060e; }
.ev-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ev-info { flex: 1; min-width: 0; }
.ev-cam { display: block; font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ev-time { display: block; font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-top: 2px; }
.ev-play { flex-shrink: 0; padding: 5px 11px; font-size: 12px; }

/* ═══════════════════════════════════════════════════════════════════════════
   WAYPOINTS
   ═══════════════════════════════════════════════════════════════════════════ */
.waypoint-card { padding: 18px; margin-bottom: 12px; }
.waypoint-toolbar { display: grid; grid-template-columns: minmax(160px,1fr) minmax(180px,1.4fr) auto auto; gap: 10px; align-items: end; }
.waypoint-toolbar label { display: flex; flex-direction: column; gap: 6px; color: var(--faint);
  font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .14em; }
.waypoint-toolbar select, .waypoint-toolbar input, .waypoint-row input {
  width: 100%; min-height: 42px; padding: 9px 11px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: rgba(255,255,255,.055);
  color: var(--text); font-family: var(--font-ui); font-size: 13px;
}
.waypoint-list { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.waypoint-row {
  display: grid; grid-template-columns: 44px minmax(130px,1.2fr) minmax(92px,.8fr) repeat(3, minmax(64px,.55fr)) auto;
  gap: 8px; align-items: center; padding: 10px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: rgba(9,18,30,.44);
}
.waypoint-row .waypoint-token { font-family: var(--font-mono); font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.waypoint-actions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
.waypoint-actions button { padding: 6px 10px; min-height: 36px; }
.schedule-toolbar { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); align-items: end; }
.schedule-list { display: grid; gap: 8px; margin-top: 10px; }
.schedule-row {
  display: grid; grid-template-columns: 1.2fr 1fr 1fr .7fr auto; gap: 8px; align-items: center;
  padding: 11px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: rgba(255,255,255,.03); cursor: pointer; font-size: 12px; transition: border-color .16s, background .16s;
}
.schedule-row:hover { background: rgba(255,255,255,.055); }
.schedule-row.selected { border-color: var(--neon-cyan); background: rgba(64,224,255,.10); box-shadow: inset 0 0 22px -12px var(--neon-cyan); }
.schedule-row .schedule-state { font-family: var(--font-mono); color: var(--neon-cyan); }
.schedule-status { color: var(--muted); font-family: var(--font-mono); font-size: 11px; margin-top: 10px; }
.waypoint-coming-soon h3 { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: .14em; color: var(--neon-cyan); margin-bottom: 8px; }
.waypoint-coming-soon p { color: var(--muted); font-size: 13px; }
.orientation-guard {
  position: fixed; inset: 0; z-index: 20000; display: grid; place-content: center; gap: 10px;
  text-align: center; color: #d9f6ff; background: var(--bg-deep);
}
.orientation-guard strong { font-family: var(--font-title); font-weight: 400; font-size: 34px; letter-spacing: .07em; text-transform: uppercase; }
.orientation-guard span { color: var(--muted); font-family: var(--font-mono); font-size: 12px; }
.orientation-guard.hidden { display: none; }
@media (max-width: 820px) {
  .waypoint-toolbar, .waypoint-row { grid-template-columns: 1fr; }
  .waypoint-actions { justify-content: flex-start; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   SYSTEM
   ═══════════════════════════════════════════════════════════════════════════ */
/* System sub-views. Grouping beats a fifteen-card scroll, and keeps the top
   nav at two buttons on a phone. */
.sys-pills { display: flex; gap: 8px; margin: 0 0 16px; flex-wrap: wrap; }
.sys-pill {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; padding: 9px 16px; border-radius: var(--r-sm);
  background: var(--tier-2); border: 1px solid rgba(255,255,255,.14);
  color: var(--faint); cursor: pointer; transition: color .18s, border-color .18s;
}
.sys-pill:hover { color: #fff; border-color: rgba(64,224,255,.4); }
.sys-pill.active {
  color: #fff; border-color: color-mix(in srgb, var(--neon-magenta) 62%, transparent);
  box-shadow: 0 0 16px -6px var(--neon-magenta);
}
.system-grid[data-sys-view="status"]   .setting-card:not([data-sys="status"]),
.system-grid[data-sys-view="control"]  .setting-card:not([data-sys="control"]),
.system-grid[data-sys-view="settings"] .setting-card:not([data-sys="settings"]) { display: none; }

.system-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(272px, 1fr)); gap: 12px; }
/* The map used to read straight through these. Section labels in particular
   were close to illegible against the road network. */
.setting-card, .waypoint-card {
  /* 0.90 still let a bright freeway read through. These panels carry body
     text over a road network; the backdrop-filter does the glass work. */
  background: rgba(8, 15, 26, .975) !important;
}
/* Section labels were the least legible thing on these two tabs. */
.setting-card h3, .waypoint-card h3,
.waypoint-card > .waypoint-toolbar > label { color: #a8c0d4; }
.setting-card .setting-note, .waypoint-card .events-empty { color: #8ba2b6; }
.setting-card { padding: 19px 21px; min-width: 0; overflow: visible; }

.copyable {
  font: inherit; color: inherit; background: none; border: 0; padding: 0;
  cursor: pointer; text-align: right; max-width: 60%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  border-bottom: 1px dashed rgba(255,255,255,.26);
}
.copyable:hover { color: var(--neon-cyan); border-bottom-color: var(--neon-cyan); }

.chip[data-state="ok"]   { color: #73f4bc; }
.chip[data-state="down"] { color: #ff8a3c; }
.setting-card h3 {
  font-family: var(--font-mono); font-size: 10px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .18em; color: var(--faint); margin-bottom: 14px;
}
.setting-row { padding: 10px 0; font-size: 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; border-top: 1px solid var(--line); }
.setting-row:first-of-type { border-top: none; }
.setting-row > span:first-child { color: var(--muted); }
.setting-row strong { font-family: var(--font-mono); font-size: 11px; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
.setting-row input[type="number"], .setting-row select {
  width: 132px; max-width: 46vw; min-height: 40px; padding: 7px 10px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: rgba(255,255,255,.055);
  color: var(--text); font-family: var(--font-mono); font-size: 12px;
}
.setting-toggle { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 13px; cursor: pointer; width: 100%; min-height: 40px; }
.setting-toggle input { accent-color: var(--neon-cyan); width: 17px; height: 17px; }
.setting-note { color: var(--faint); font-size: 12px; line-height: 1.5; margin: 6px 0 10px; }
.motion-subscription-row {
  display: grid; grid-template-columns: minmax(7rem,1fr) repeat(3, auto); gap: .5rem;
  align-items: center; padding: .5rem 0; border-top: 1px solid rgba(112,220,236,.10); font-size: .78rem;
}
.motion-subscription-row label { white-space: nowrap; }
.kbd-row { display: flex; align-items: center; justify-content: space-between; font-size: 12px; padding: 7px 0; border-top: 1px solid var(--line); }
.kbd-row:first-of-type { border-top: none; }
.kbd-row span { color: var(--muted); }
.simple-restart-card { border-color: rgba(255,77,109,.30); }
.simple-restart-button {
  width: 100%; min-height: 76px; border-radius: var(--r-lg);
  border: 1px solid rgba(255,77,109,.55);
  background: linear-gradient(135deg, rgba(140,28,42,.60), rgba(90,20,48,.55));
  color: #fff; font-family: var(--font-title); font-weight: 400; font-size: 24px;
  letter-spacing: .08em; text-transform: uppercase; cursor: pointer;
  box-shadow: 0 0 0 1px rgba(255,77,109,.22), 0 12px 34px rgba(0,0,0,.34);
}
.simple-restart-button:disabled { opacity: .55; cursor: wait; }
.display-opt { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-top: 1px solid var(--line); font-size: 13px; cursor: pointer; width: 100%; min-height: 42px; }
.display-opt:first-of-type { border-top: none; }
.display-opt input[type="radio"] { accent-color: var(--neon-cyan); width: 17px; height: 17px; flex-shrink: 0; }
.vlc-worker-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 9px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
.vlc-worker-row:last-child { border-bottom: none; }
.vlc-worker-name { flex: 1; font-weight: 500; }
.vlc-worker-meta { font-family: var(--font-mono); font-size: 11px; color: var(--muted); flex: 1; }
.vlc-worker-actions, .vlc-card-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.chip.alive { background: rgba(69,224,160,.14); border-color: rgba(69,224,160,.5); color: var(--st-healthy); }
.chip.dead  { background: rgba(255,77,109,.14); border-color: rgba(255,77,109,.5); color: #ff9aa8; }
.setting-row > .chip, .vlc-worker-actions .chip, .vlc-card-actions .chip { flex-shrink: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   USERS
   ═══════════════════════════════════════════════════════════════════════════ */
.users-card-hdr { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.users-card-hdr h3 { margin-bottom: 0; }
.sessions-hdr { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }
.user-row, .session-row { display: flex; align-items: center; gap: 9px; padding: 10px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
.user-row:last-child, .session-row:last-child { border-bottom: none; }
.user-main, .session-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.user-main small, .session-main small, .session-main em {
  color: var(--faint); font-family: var(--font-mono); font-size: 10.5px; font-style: normal;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.session-main span { font-weight: 600; }
.user-name { font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-role { font-family: var(--font-mono); font-size: 10px; padding: 3px 9px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); flex-shrink: 0; text-transform: uppercase; letter-spacing: .1em; }
.user-actions { display: flex; gap: 5px; flex-shrink: 0; }
.user-actions button {
  min-width: 44px; min-height: 38px; padding: 0 10px; border-radius: var(--r-sm); font-size: 12px;
  background: rgba(255,255,255,.06); border: 1px solid var(--line); color: var(--text);
  cursor: pointer; display: grid; place-items: center;
}
.user-actions button:hover { background: rgba(255,255,255,.13); }
.user-actions .del-btn:hover { background: rgba(200,50,45,.42); border-color: rgba(255,77,109,.5); }
.session-revoke { margin-left: 8px; flex: 0 0 auto; }
.session-seen { flex-shrink: 0; color: var(--neon-cyan); font-family: var(--font-mono); font-size: 11px; }
.user-form { display: flex; flex-direction: column; gap: 10px; padding: 13px 0; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
.user-form label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--faint); }
.user-form input[type="text"], .user-form input[type="password"], .user-form select {
  display: block; width: 100%; margin-top: 6px; min-height: 42px; padding: 9px 11px;
  border-radius: var(--r-sm); background: rgba(255,255,255,.055); border: 1px solid var(--line);
  color: var(--text); font-size: 13px; font-family: var(--font-ui);
}
.user-form select option { background: #0a1520; }
.perm-grid { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.perm-toggle {
  display: flex; align-items: center; gap: 6px; min-height: 36px;
  font-size: 12px; cursor: pointer; color: var(--text);
  background: rgba(255,255,255,.045); border: 1px solid var(--line);
  border-radius: var(--r-xs); padding: 5px 10px;
}
.perm-toggle input { accent-color: var(--neon-cyan); width: 14px; height: 14px; }
.form-actions { display: flex; gap: 7px; }
.form-actions button { padding: 9px 16px; min-height: 42px; border-radius: var(--r-sm); font-size: 13px; cursor: pointer; }
.form-actions .primary-btn { background: rgba(64,224,255,.18); border: 1px solid rgba(64,224,255,.46); color: var(--neon-cyan); }
.form-actions .cancel-btn { background: rgba(255,255,255,.06); border: 1px solid var(--line); color: var(--text); }

/* ═══════════════════════════════════════════════════════════════════════════
   LOGIN
   ═══════════════════════════════════════════════════════════════════════════ */
.sheet-backdrop {
  position: fixed; z-index: 60; inset: 0; display: grid; place-items: center; padding: 16px;
  background: rgba(2,5,10,.72);
  animation: fade-up .3s var(--ease-out) both;
}
.login-card { width: min(392px, calc(100vw - 28px)); padding: 34px 32px; background: var(--tier-3); border-radius: var(--r-lg); }
.login-brand { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
.login-card h2 {
  font-family: var(--font-title); font-weight: 400; font-size: 34px;
  letter-spacing: .06em; text-transform: uppercase; margin: 18px 0 4px;
  background: linear-gradient(94deg, #fff 10%, var(--neon-magenta) 55%, var(--neon-cyan) 96%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.login-card label { display: block; font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .16em; color: var(--faint); margin: 18px 0 0; }
.login-card input {
  display: block; width: 100%; margin-top: 8px; min-height: 48px;
  padding: 13px 15px; border-radius: var(--r-sm); border: 1px solid var(--line);
  background: rgba(5,11,19,.82); color: var(--text); font-size: 15px; font-family: var(--font-ui);
}
.login-card .primary { width: 100%; margin-top: 22px; padding: 15px; font-size: 14px; border-radius: var(--r-sm); }
.error { min-height: 16px; color: var(--st-offline); font-size: 13px; margin-top: 10px; }

/* ═══════════════════════════════════════════════════════════════════════════
   TOAST
   ═══════════════════════════════════════════════════════════════════════════ */
.toast {
  position: fixed; z-index: 70; left: 50%; bottom: 16px; transform: translateX(-50%);
  padding: 12px 18px; max-width: calc(100vw - 32px);
  background: var(--tier-3); border: 1px solid var(--neon-border);
  border-radius: var(--r-sm); box-shadow: var(--glow-cyan), var(--sh-3);
  font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  animation: toast-in .34s var(--ease-out) both;
}
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 14px); } to { opacity: 1; transform: translate(-50%, 0); } }
.hidden { display: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   DRILL-IN — flip-wipe entry, video layer transitions
   ═══════════════════════════════════════════════════════════════════════════ */
.drillin {
  /* #drillin is reparented into #wall-frame at startup.  It is therefore
     exactly the wall's current visible area, including native wall fullscreen. */
  position: absolute; inset: 0; z-index: 45;
  padding: 0; background: #000;
  display: grid; grid-template-rows: 1fr;
  border-radius: inherit;
}
.drillin:not(.hidden) { animation: drill-open .52s var(--ease-gta); }
@keyframes drill-open {
  0%   { opacity: 0; transform: perspective(1600px) rotateX(8deg) translate3d(0,18px,0) scale(.91); clip-path: inset(48% 0 48% 0); filter: brightness(2.2) saturate(1.85) hue-rotate(18deg); }
  18%  { opacity: .82; transform: perspective(1600px) rotateX(3deg) translate3d(-4px,4px,0) scale(1.03); clip-path: inset(13% 0 20% 0); }
  36%  { transform: perspective(1600px) rotateX(-2deg) translate3d(3px,-2px,0) scale(.99); clip-path: inset(4% 0 7% 0); }
  60%  { opacity: 1; }
  100% { opacity: 1; transform: perspective(1600px) rotateX(0) scale(1); clip-path: inset(0 0 0 0); filter: brightness(1) saturate(1); }
}
#wall-frame:fullscreen .drillin, #wall-frame:-webkit-full-screen .drillin { border-radius: 0; }

.detail-video { position: absolute; inset: 0; background: #000; overflow: hidden; }
.detail-video video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
.drillin, .detail-video, .detail-video video, #crop-canvas {
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}
.detail-video video:focus, #crop-canvas:focus { outline: none; }
.wall-warm-video { width: 2px !important; height: 2px !important; opacity: .001; pointer-events: none; z-index: -1; }
.detail-video video.crop-preview { object-fit: fill; transform-origin: 0 0; }
#crop-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; z-index: 3; object-fit: contain; background: #000; pointer-events: none; }
.detail-video video.direct-preview { z-index: 1; }
/* The real crop-to-direct handoff. Class removal is the trigger; commit is a flip. */
.stream-reveal-pending {
  /* Keep the decoder in the compositor while the crop owns the visible frame.
     A practically transparent surface avoids Chromium deprioritising it. */
  opacity: .001 !important;
  pointer-events: none !important;
}
.detail-video video.direct-preview:not(.stream-reveal-pending) { animation: none; }
@keyframes stream-commit {
  0%   { opacity: 0; transform: scale(1.025); clip-path: inset(0 0 100% 0); filter: brightness(2.0) saturate(1.7) hue-rotate(15deg); }
  36%  { opacity: .9; transform: scale(1.005); clip-path: inset(0 0 38% 0); }
  100% { opacity: 1; transform: scale(1); clip-path: inset(0 0 0 0); filter: brightness(1) saturate(1); }
}
#detail-state {
  position: absolute; z-index: 2; left: 14px; bottom: 14px;
  background: var(--tier-3); padding: 8px 13px; border-radius: var(--r-sm);
  border: 1px solid var(--line);
  color: var(--muted); font-family: var(--font-mono); font-size: 11px;
}

/* SOUTHGATE rendered on top of BACK and above LIVE because the identity
   block, the playback back button and both window controls were four
   independent absolutes claiming the same corner. One rail, real layout. */
.fs-topbar {
  position: absolute; top: 0; left: 0; right: 0; z-index: 22;
  display: flex; align-items: center; gap: 10px;
  padding: max(10px, env(safe-area-inset-top)) 12px 12px;
  background: linear-gradient(180deg, rgba(3,7,14,.88) 12%, rgba(3,7,14,0));
  pointer-events: none;
}
.fs-topbar > * { pointer-events: auto; }
.fs-topbar .fs-identity,
.fs-topbar .fs-exit,
.fs-topbar .fs-camera-fullscreen,
.fs-topbar .pb-back-top,
.fs-topbar .pb-times-toggle,
.fs-topbar .pb-close { position: static !important; top: auto; left: auto; right: auto; }
.fs-topbar .fs-identity { flex: 1 1 auto; min-width: 0; }
.fs-topbar .fs-exit { margin-left: auto; }

.fs-identity { z-index: 20; display: flex; flex-direction: column; gap: 3px; pointer-events: none; min-width: 0; }
.fs-cam-name {
  font-family: var(--font-title); font-weight: 400; font-size: 26px;
  letter-spacing: .05em; text-transform: uppercase; color: #fff;
  text-shadow: 0 1px 8px rgba(0,0,0,.9), 0 0 24px rgba(64,224,255,.34);
}
.fs-cam-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fs-cam-state { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase; color: rgba(255,255,255,.6); }
/* The identity block used to keep whatever the live stream last wrote, so it
   said LIVE for the whole of a playback session. */
.fs-cam-state[data-mode="live"]       { color: var(--neon-cyan); }
.fs-cam-state[data-mode="playback"]   { color: #f0b64a; }
.fs-cam-state[data-mode="connecting"] { color: rgba(255,255,255,.55); }

.fs-exit {
  position: absolute; top: 12px; right: 12px; z-index: 20;
  width: 52px; height: 52px; border-radius: var(--r-md);
  background: var(--tier-3);  border: 1px solid rgba(255,255,255,.22); color: #fff; cursor: pointer;
  display: grid; place-items: center; transition: border-color .16s, box-shadow .16s;
}
.fs-exit:active { transform: scale(.94); }
.fs-exit:hover { box-shadow: var(--glow-cyan); border-color: rgba(64,224,255,.46); }

.fs-btn {
  position: absolute; top: 72px; left: 12px; z-index: 20;
  width: 46px; height: 46px; border-radius: var(--r-sm);
  background: var(--tier-2);  border: 1px solid rgba(255,255,255,.18); color: #fff; font-size: 18px; cursor: pointer;
  display: grid; place-items: center; transition: border-color .16s, box-shadow .16s, color .16s;
}
.fs-btn:active { transform: scale(.94); }
.fs-btn:hover { border-color: rgba(64,224,255,.46); }
#fs-audio[aria-pressed="true"] { color: var(--neon-cyan); box-shadow: var(--glow-cyan); border-color: rgba(64,224,255,.5); }
#fs-talk { top: 128px; }
#fs-talk[aria-pressed="true"] {
  color: var(--st-offline);
  box-shadow: 0 0 0 1px rgba(255,77,109,.4), 0 0 22px rgba(255,77,109,.28);
  animation: mic-live 1.05s ease-in-out infinite;
}
#fs-talk.talk-unverified { border-color: rgba(255,178,77,.48); color: var(--st-degraded); }
#fs-snapshot { top: 184px; }
#fs-playback { top: 240px; }
#fs-pip      { top: 296px; }
#fs-waypoints-toggle { top: 352px; font-family: var(--font-mono); font-size: 12px; font-weight: 700; }
.fs-btn-right { left: auto; right: 12px; top: 72px; }
#fs-camera-fullscreen { top: 72px; }
#fs-orientation-lock { left: auto; right: 12px; top: 128px; font-family: var(--font-mono); font-weight: 700; font-size: 15px; }
#fs-hud-toggle { top: 184px; }

/* The in-wall drill-in deliberately keeps only close, fullscreen, and the
   four directional PTZ arrows. True camera fullscreen restores the toolset. */
.drillin:not(.camera-native-fullscreen) .fs-identity,
.drillin:not(.camera-native-fullscreen) #fs-audio,
.drillin:not(.camera-native-fullscreen) #fs-talk,
.drillin:not(.camera-native-fullscreen) #fs-snapshot,
.drillin:not(.camera-native-fullscreen) #fs-playback,
.drillin:not(.camera-native-fullscreen) #fs-pip,
.drillin:not(.camera-native-fullscreen) #fs-waypoints-toggle,
.drillin:not(.camera-native-fullscreen) #fs-hud-toggle,
.drillin:not(.camera-native-fullscreen) #fs-orientation-lock,
.drillin:not(.camera-native-fullscreen) #stream-hud,
.drillin:not(.camera-native-fullscreen) .fs-zoom,
.drillin:not(.camera-native-fullscreen) .ptz-zin,
.drillin:not(.camera-native-fullscreen) .ptz-zout,
.drillin:not(.camera-native-fullscreen) .ptz-duration,
.drillin:not(.camera-native-fullscreen) .preset-strip,
.drillin:not(.camera-native-fullscreen) .waypoint-save-sheet { display: none !important; }

.fs-zoom {
  position: absolute; bottom: 16px; left: 16px; z-index: 20;
  font-family: var(--font-display); font-size: 14px; font-weight: 700;
  color: var(--neon-cyan); background: var(--tier-3);
  padding: 6px 12px; border-radius: var(--r-xs); border: 1px solid rgba(64,224,255,.26);
  display: none; pointer-events: none; letter-spacing: .04em;
}
.fs-hud {
  position: absolute; top: 16px; right: 16px; z-index: 20;
  background: var(--tier-3); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 10px 14px;  font-family: var(--font-mono); font-size: 11px; color: var(--text);
  display: flex; flex-direction: column; gap: 5px; min-width: 148px; pointer-events: none;
}
.fs-hud-row { display: flex; justify-content: space-between; gap: 12px; }
.fs-hud-label { color: var(--faint); }
.fs-hud-val { color: var(--neon-cyan); font-weight: 600; }
.fs-controls, .preset-strip { padding-bottom: max(8px, env(safe-area-inset-bottom)); }

/* ═══════════════════════════════════════════════════════════════════════════
   PTZ
   ═══════════════════════════════════════════════════════════════════════════ */
.ptz-overlay { position: absolute; inset: 0; z-index: 10; pointer-events: none; }
.ptz-overlay.hidden { display: block; }
.ptz-btn {
  position: absolute; pointer-events: all;
  background: var(--tier-1);  border: 1px solid rgba(255,255,255,.22); color: #fff; border-radius: 50%; cursor: pointer;
  font-size: 28px; font-weight: 700; width: 80px; height: 80px;
  display: grid; place-items: center;
  transition: background .12s, opacity .24s var(--ease-out), box-shadow .16s;
  box-shadow: 0 4px 20px rgba(0,0,0,.36);
}
.ptz-btn:hover { background: var(--tier-2); border-color: rgba(64,224,255,.5); box-shadow: var(--glow-cyan); }
.ptz-btn:active { background: rgba(64,224,255,.3); box-shadow: 0 0 0 2px rgba(64,224,255,.5), 0 0 34px rgba(64,224,255,.4); transform: scale(.93); }
/* Fullscreen PTZ is banded to the picture via custom properties. If JS never
   runs, or measures nothing, both default to 0px and the pad behaves exactly
   as it did before — an override that can only ever narrow, never break. */
.drillin.camera-native-fullscreen .ptz-overlay {
  top: var(--ptz-top, 0px);
  bottom: var(--ptz-bottom, 0px);
  left: 0; right: 0;
}
.ptz-up { top: 5%; left: 50%; transform: translateX(-50%); }
.ptz-down { bottom: 5%; left: 50%; transform: translateX(-50%); }
.ptz-left { left: 5%; top: 50%; transform: translateY(-50%); }
.ptz-right { right: 5%; top: 50%; transform: translateY(-50%); }
.ptz-up:active { top: 5%; left: 50%; transform: translateX(-50%) scale(.93); }
.ptz-down:active { bottom: 5%; left: 50%; transform: translateX(-50%) scale(.93); }
.ptz-left:active { left: 5%; top: 50%; transform: translateY(-50%) scale(.93); }
.ptz-right:active { right: 5%; top: 50%; transform: translateY(-50%) scale(.93); }
.ptz-zin { bottom: 14%; right: 12%; width: 62px; height: 62px; font-size: 26px; border-radius: 50%; }
.ptz-zout { bottom: 6%; right: 12%; width: 62px; height: 62px; font-size: 26px; border-radius: 50%; }
.ptz-overlay.hidden .ptz-btn { pointer-events: none; opacity: 0; }
.ptz-overlay:not(.hidden) .ptz-btn { opacity: 1; }
.ptz-duration {
  position: absolute; bottom: 24%; left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 5px; z-index: 15;
  background: var(--tier-2); border: 1px solid rgba(255,255,255,.18);
  border-radius: var(--r-md); padding: 9px 16px;
  pointer-events: all; touch-action: none; overscroll-behavior: contain;
}
.ptz-duration-scale { display: flex; align-items: center; gap: 9px; }
.ptz-dur-bound { font-family: var(--font-display); font-size: 13px; font-weight: 700; color: var(--neon-cyan); min-width: 30px; line-height: 1; }
.ptz-dur-bound:first-child { text-align: right; }
.ptz-dur-bound:last-child { text-align: left; }
.ptz-dur-unit { font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--faint); }
#ptz-dur-slider {
  -webkit-appearance: none; appearance: none; width: 140px; height: 4px; border-radius: 2px;
  background: rgba(255,255,255,.18); outline: none; cursor: pointer; accent-color: var(--neon-cyan);
}
#ptz-dur-slider::-webkit-slider-thumb {
  -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%;
  background: var(--neon-cyan); box-shadow: 0 0 10px rgba(64,224,255,.6); cursor: pointer;
}
#ptz-dur-slider::-moz-range-thumb { width: 20px; height: 20px; border: none; border-radius: 50%; background: var(--neon-cyan); cursor: pointer; }
.ptz-overlay.hidden .ptz-duration { opacity: 0; pointer-events: none; }

.preset-strip {
  position: absolute; top: 72px; left: 68px; display: flex; flex-direction: column; gap: 8px;
  z-index: 15; max-width: min(190px, 32vw); max-height: calc(100vh - 240px); overflow-y: auto; pointer-events: all;
}
.preset-btn {
  background: var(--tier-2);  border: 1px solid rgba(255,255,255,.2); color: #fff; border-radius: var(--r-sm);
  min-height: 40px; padding: 9px 15px; font-size: 12px; font-weight: 500; cursor: pointer;
  transition: background .12s, border-color .12s;
}
.preset-btn:hover { background: rgba(64,224,255,.14); border-color: rgba(64,224,255,.5); }
.preset-btn:active { background: rgba(64,224,255,.28); }
.waypoint-save-sheet {
  position: absolute; top: 72px; left: 68px; z-index: 18;
  width: min(268px, calc(100vw - 92px)); padding: 14px; border-radius: var(--r-md);
  background: var(--tier-3); border: 1px solid rgba(64,224,255,.38);
  box-shadow: var(--sh-3);}
.waypoint-save-sheet label { display: block; margin-bottom: 8px; color: var(--faint); font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .14em; }
.waypoint-save-sheet input {
  width: 100%; min-height: 44px; padding: 10px 12px; border-radius: var(--r-sm);
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.18);
  color: var(--text); font: 14px var(--font-ui);
}
.waypoint-save-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }

/* ═══════════════════════════════════════════════════════════════════════════
   PLAYBACK
   ═══════════════════════════════════════════════════════════════════════════ */
.playback-panel {
  position: absolute; inset: 0; z-index: 9; overflow: hidden;
  background: linear-gradient(180deg, rgba(0,0,0,.44), rgba(0,0,0,.08) 42%, rgba(0,0,0,.32));
}
.playback-panel:not(.hidden) { animation: drill-open .52s var(--ease-gta) both; }
.pb-header { position: absolute; top: 72px; left: 68px; right: 76px; z-index: 12; display: flex; flex-direction: column; gap: 3px; pointer-events: none; }
.pb-cam-name { font-family: var(--font-title); font-weight: 400; font-size: 24px; letter-spacing: .05em; text-transform: uppercase; color: #fff; text-shadow: 0 1px 8px rgba(0,0,0,.9); }
.pb-segment-info { font-family: var(--font-mono); font-size: 11px; color: var(--muted); text-shadow: 0 1px 6px rgba(0,0,0,.9); }
.pb-close { width: 46px; height: 46px; border-radius: var(--r-sm); background: var(--tier-2); border: 1px solid rgba(255,255,255,.18); color: #fff; cursor: pointer; font-size: 16px; display: grid; place-items: center; }
.pb-back-top, .pb-times-toggle {
  position: absolute; top: 12px; z-index: 16; height: 46px; padding: 0 17px; border-radius: var(--r-md);
  background: var(--tier-2); border: 1px solid rgba(255,255,255,.22); color: #fff; cursor: pointer;
  font-family: var(--font-mono); font-size: 12px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
}
.pb-back-top { left: 12px; }
.pb-times-toggle { left: 112px; }
.pb-browser {
  position: absolute; left: 68px; right: 76px; top: 126px; z-index: 12;
  display: flex; flex-direction: column; gap: 9px; max-height: 34vh; overflow-y: auto;
  padding: 12px; border-radius: var(--r-md);
  background: var(--tier-2); border: 1px solid var(--line);
}
.pb-calendar { display: flex; gap: 8px; flex-wrap: wrap; padding-bottom: 2px; }
.pb-day-chip {
  background: rgba(255,255,255,.06); border: 1px solid var(--line); border-radius: var(--r-sm);
  min-height: 42px; padding: 9px 15px; font-size: 13px; cursor: pointer; color: var(--text); transition: background .12s, border-color .12s;
}
.pb-day-chip:hover { background: rgba(255,255,255,.12); }
.pb-day-chip.active { background: rgba(64,224,255,.18); border-color: rgba(64,224,255,.5); color: var(--neon-cyan); }
.pb-timeline-wrap { flex-shrink: 0; }
.pb-timeline-label { font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--faint); margin-bottom: 9px; }
.pb-timeline { display: grid; grid-template-columns: repeat(auto-fill, minmax(62px, 1fr)); gap: 7px; background: none; border: none; height: auto; overflow: visible; }
.ui-glitch-enter { animation: ui-glitch-expand .48s var(--ease-gta) both; }
@keyframes ui-glitch-expand {
  0% { opacity: 0; transform: translate3d(0,14px,0) scale(.93); clip-path: inset(44% 0 44% 0); filter: brightness(2.1) saturate(1.8) hue-rotate(16deg); }
  16% { opacity: .9; transform: translate3d(-4px,2px,0) scale(1.025); clip-path: inset(14% 0 22% 0); }
  33% { transform: translate3d(3px,-2px,0) scale(.992); clip-path: inset(5% 0 7% 0); filter: brightness(1.35) saturate(1.25) hue-rotate(-8deg); }
  100% { opacity: 1; transform: none; clip-path: inset(0); filter: none; }
}
.pb-seg {
  min-height: 46px; background: rgba(64,224,255,.14); border-radius: var(--r-sm); cursor: pointer;
  border: 1px solid rgba(64,224,255,.32); transition: background .14s, border-color .14s;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap;
}
.pb-seg:hover { background: rgba(64,224,255,.3); border-color: rgba(64,224,255,.6); }
.pb-seg.active { background: rgba(64,224,255,.55); border-color: var(--neon-cyan); color: #fff; box-shadow: 0 0 22px -6px var(--neon-cyan); }
.pb-seg-wrap { display: flex; flex-direction: column; align-items: stretch; gap: 3px; }
.pb-seg-dl { display: block; text-align: center; text-decoration: none; font-family: var(--font-mono); font-size: 10px; color: var(--faint); line-height: 1; padding: 3px 0; transition: color .12s; }
.pb-seg-dl:hover { color: var(--neon-cyan); }
.pb-audit-link { grid-column: 1/-1; color: var(--neon-cyan); font: 600 11px var(--font-mono); text-decoration: none; padding: 6px 0; }
.pb-audit-link:hover { text-decoration: underline; }
.pb-empty { color: var(--faint); font-family: var(--font-mono); font-size: 12px; text-align: center; padding: 24px 0; }
#pb-video { position: absolute; inset: 0; width: 100%; height: 100%; background: #000; object-fit: contain; }
.pb-player { position: absolute; inset: 0; z-index: 13; pointer-events: none; }
.pb-player-actions { display: contents; }
.pb-ctrl {
  position: absolute; pointer-events: all;
  background: var(--tier-1);  border: 1px solid rgba(255,255,255,.22); color: #fff; border-radius: 50%; cursor: pointer;
  font-size: 22px; font-weight: 700; width: 80px; height: 80px;
  display: grid; place-items: center; box-shadow: 0 4px 20px rgba(0,0,0,.36);
}
.pb-ctrl:hover { background: var(--tier-2); border-color: rgba(64,224,255,.5); box-shadow: var(--glow-cyan); }
.pb-ctrl:active { background: rgba(64,224,255,.3); transform: scale(.93); }
.pb-ctrl:disabled { opacity: .32; pointer-events: none; }
.pb-prev { left: 5%; top: 50%; transform: translateY(-50%); }
.pb-next { right: 5%; top: 50%; transform: translateY(-50%); }
.pb-prev:active { transform: translateY(-50%) scale(.93); }
.pb-next:active { transform: translateY(-50%) scale(.93); }
.pb-toggle { left: 50%; bottom: 5%; transform: translateX(-50%); width: 84px; height: 84px; color: var(--neon-cyan); }
.pb-toggle:active { transform: translateX(-50%) scale(.93); }
.pb-seek-back { left: calc(50% - 132px); bottom: 7%; width: 62px; height: 62px; font-size: 16px; }
.pb-seek-forward { left: calc(50% + 72px); bottom: 7%; width: 62px; height: 62px; font-size: 16px; }
#pb-progress { position: absolute; left: 50%; bottom: 24%; transform: translateX(-50%); width: min(520px, 54vw); z-index: 14; pointer-events: all; accent-color: var(--neon-cyan); }
.pb-big-play {
  position: absolute; left: 50%; top: 50%; z-index: 15; transform: translate(-50%,-50%);
  min-width: 140px; height: 76px; border-radius: var(--r-lg);
  background: var(--tier-3); border: 1px solid rgba(64,224,255,.55);
  color: var(--neon-cyan); font-family: var(--font-title); font-weight: 400; font-size: 30px;
  letter-spacing: .1em; text-transform: uppercase; box-shadow: var(--glow-cyan); cursor: pointer;
}
.drillin.playback-mode #fs-audio, .drillin.playback-mode #fs-talk,
.drillin.playback-mode #fs-snapshot, .drillin.playback-mode #fs-playback,
.drillin.playback-mode #fs-pip, .drillin.playback-mode #fs-hud-toggle,
.drillin.playback-mode #fs-orientation-lock, .drillin.playback-mode #fs-waypoints-toggle,
.drillin.playback-mode #stream-hud, .drillin.playback-mode #fs-zoom { display: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   FIRESTORM — page-wide alert mode.
   Hook: app.js already maintains aria-pressed on #btn-firestorm. Zero JS.
   Every tube changes gas; nine identities collapse into one alarm state.
   ═══════════════════════════════════════════════════════════════════════════ */
#btn-firestorm[aria-pressed="true"] {
  color: #fff; border-color: rgba(255,77,109,.6);
  background: rgba(150,26,42,.5);
  box-shadow: 0 0 0 1px rgba(255,77,109,.42), 0 0 26px rgba(255,77,109,.34);
  animation: neon-pulse 1.4s ease-in-out infinite;
}
body:has(#btn-firestorm[aria-pressed="true"]) {
  --neon-cyan:    #ff5a3c;
  --neon-magenta: #ff2d55;
  --neon-violet:  #ff7a35;
  --cam-1: #ff5a3c; --cam-2: #ff7a35; --cam-3: #ff2d55;
  --cam-4: #ffa03c; --cam-5: #ff6b4a; --cam-6: #ff4d6d;
  --cam-7: #ff2d3c; --cam-8: #ff8552; --cam-9: #ffc23c;
  --line:    rgba(255,120,90,.16);
  --line-hi: rgba(255,120,90,.32);
  --sweep:   2800ms;   /* matches canvas-bg alert period */
}
body:has(#btn-firestorm[aria-pressed="true"])::before {
  background:
    radial-gradient(120% 80% at 50% -10%, rgba(255,60,40,.16), transparent 60%),
    radial-gradient(100% 70% at 50% 110%, rgba(255,45,85,.10), transparent 62%);
  animation: firestorm-throb 2.8s ease-in-out infinite;
}
@keyframes firestorm-throb { 0%,100% { opacity: .7 } 50% { opacity: 1 } }
/* Alert scanlines over the whole page */
body:has(#btn-firestorm[aria-pressed="true"])::after {
  content: ""; position: fixed; inset: 0; z-index: 2; pointer-events: none;
  background: repeating-linear-gradient(180deg, rgba(255,80,60,.045) 0 1px, transparent 1px 4px);
  mix-blend-mode: screen;
}
body:has(#btn-firestorm[aria-pressed="true"]) .wall-wrap { --wall-hue: #ff3c2d; animation-duration: 1.4s; }
body:has(#btn-firestorm[aria-pressed="true"]) .topbar,
body:has(#btn-firestorm[aria-pressed="true"]) .section-nav { background: rgba(30,10,12,.60); }

/* ═══════════════════════════════════════════════════════════════════════════
   KEYFRAMES
   ═══════════════════════════════════════════════════════════════════════════ */
@keyframes spin        { to { transform: rotate(360deg); } }
@keyframes shimmer     { from { transform: translateX(0); } to { transform: translateX(420%); } }
@keyframes neon-pulse  { 50% { opacity: .48; } }
@keyframes neon-breathe{ 0%,100% { opacity: .58 } 50% { opacity: 1 } }
@keyframes neon-buzz   { 0%,3% { opacity:.28 } 4% { opacity:1 } 5% { opacity:.34 } 6%,9% { opacity:1 }
                         10% { opacity:.42 } 11%,74% { opacity:1 } 75% { opacity:.55 } 76%,100% { opacity:1 } }
@keyframes rays-spin   { to { transform: rotate(360deg); } }
@keyframes scan-run    { to { top: 116%; } }
@keyframes grain-jump  { 0% { transform: translate(0,0) } 25% { transform: translate(-3%,2%) }
                          50% { transform: translate(2%,-3%) } 75% { transform: translate(-2%,-2%) } }
@keyframes boot-step-pulse { 50% { opacity: .62; } }
@keyframes fade-up     { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes mic-live    { 50% { transform: scale(1.06); } }
@keyframes reconnectPulse { 0%,100% { opacity:.5; text-shadow:0 0 0 rgba(255,178,77,0) }
                            50% { opacity:1; text-shadow:0 0 20px rgba(255,178,77,.7) } }
@keyframes statPulse   { 0%,100% { opacity:.72; transform:translateY(0) } 50% { opacity:1; transform:translateY(-1px) } }
/* Neon tube striking on hover, borrowed from the Pharrell splash */
@keyframes tube-strike {
  0%, 4%   { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 24%, transparent); }
  5%       { box-shadow: inset 0 0 0 1px var(--cc), inset 0 0 16px -5px var(--cc), 0 0 30px -4px var(--cc); }
  6%       { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 30%, transparent); }
  7%, 100% { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 85%, transparent),
                         inset 0 0 16px -5px color-mix(in srgb, var(--cc) 80%, transparent),
                         0 0 34px -4px color-mix(in srgb, var(--cc) 72%, transparent); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 960px) {
  .cam-grid { grid-template-columns: repeat(2, 1fr); }
  .topbar, .section-nav { padding: 0 14px; }
  .topbar .status-chip { display: none; }
  .ptz-btn { width: 64px; height: 64px; font-size: 22px; }
  .ptz-zin, .ptz-zout { width: 52px; height: 52px; font-size: 20px; }
  .pb-ctrl { width: 64px; height: 64px; font-size: 20px; }
  .pb-seek-back, .pb-seek-forward { width: 52px; height: 52px; font-size: 14px; }
  .pb-browser { left: 64px; right: 64px; }
  .bv-grain, .bv-rays { display: none; }  /* low-power boot path */
}
@media (max-width: 520px) {
  :root { --page-pad: 11px; }
  .sb-num { font-size: 18px; }
  .sb-item { padding: 0 13px; }
  .cam-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
  .system-grid { grid-template-columns: 1fr; }
  .topbar-clock { display: none; }
  .brand-text strong { font-size: 18px; }
  .section-hdr h2 { font-size: 22px; }
  .ptz-btn { width: 58px; height: 58px; font-size: 20px; }
  .ptz-zin, .ptz-zout { width: 46px; height: 46px; font-size: 18px; }
  .pb-ctrl { width: 58px; height: 58px; font-size: 18px; }
  .pb-seek-back { left: calc(50% - 104px); }
  .pb-seek-forward { left: calc(50% + 48px); }
  .pb-browser { left: 58px; right: 58px; max-height: 32vh; }
  .fs-exit { width: 46px; height: 46px; }
  .fs-cam-name { font-size: 21px; }
}
@media (orientation: portrait) and (max-width: 720px) {
  .fs-identity { left: 12px; right: 70px; }
  .fs-btn { width: 42px; height: 42px; }
  #fs-talk { top: 122px; }
  #fs-snapshot { top: 170px; }
  #fs-playback { top: 218px; }
  #fs-pip { top: 266px; }
  #fs-waypoints-toggle { top: 314px; }
  .preset-strip { top: 120px; left: 60px; max-width: calc(100vw - 132px); max-height: 34vh; }
  .ptz-up { top: 12%; } .ptz-down { bottom: 12%; }
  .ptz-left { left: 4%; } .ptz-right { right: 4%; }
  .ptz-zin { bottom: 22%; right: 8%; } .ptz-zout { bottom: 14%; right: 8%; }
  .ptz-duration { bottom: 7%; left: 50%; max-width: calc(100vw - 120px); }
  #ptz-dur-slider { width: min(130px, 34vw); }
  .pb-browser { left: 12px; right: 12px; top: 72px; max-height: 38vh; }
  .pb-header { top: 118px; left: 18px; right: 18px; }
  #pb-progress { bottom: 18%; width: 76vw; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   REDUCED MOTION — ambient motion off, state feedback survives at low amplitude
   Honours both the OS setting and the in-app #reduce-motion toggle, which
   previously only paused the canvas and left every CSS animation running.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  /* Keep the five-second bootstrap information available as a static screen. */
  .boot-veil { animation: none !important; }
  .boot-veil.boot-complete { opacity: 0; visibility: hidden; pointer-events: none; }
  .cam-card, .wall-wrap { animation: none !important; }
  .cam-card { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 55%, transparent); }
  .cam-card.offline { box-shadow: inset 0 0 0 1px rgba(125,142,163,.2); }
  .wall-wrap { box-shadow: 0 0 0 1px color-mix(in srgb, var(--wall-hue) 55%, transparent); }
  .cam-thumb::before, .cam-thumb::after, .bv-grain, .bv-rays, .bv-scan, .topbar::after { display: none !important; }
  .section { clip-path: none !important; transform: none !important; filter: none !important; opacity: 1 !important; }
}
body:has(#reduce-motion:checked) *,
body:has(#reduce-motion:checked) *::before,
body:has(#reduce-motion:checked) *::after {
  animation-duration: .001ms !important;
  animation-iteration-count: 1 !important;
  animation-delay: 0ms !important;
  transition-duration: .001ms !important;
}
body:has(#reduce-motion:checked) .cam-card,
body:has(#reduce-motion:checked) .wall-wrap { animation: none !important; }
body:has(#reduce-motion:checked) .cam-card { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 55%, transparent); }
body:has(#reduce-motion:checked) .wall-wrap { box-shadow: 0 0 0 1px color-mix(in srgb, var(--wall-hue) 55%, transparent); }
body:has(#reduce-motion:checked) .cam-thumb::before,
body:has(#reduce-motion:checked) .cam-thumb::after { display: none !important; }
body:has(#reduce-motion:checked) .section { clip-path: none !important; transform: none !important; filter: none !important; opacity: 1 !important; }


/* ╔═══════════════════════════════════════════════════════════════════════════╗
   ║  v13 — TABBED SECTIONS + GLITCH FLIP + AMPLIFIED MOTION                   ║
   ║  Presentation-only override layer. Appended so the local additions above  ║
   ║  (bv-status, tv-static, drill-in reparenting) stay intact and this pass    ║
   ║  reads as a clean diff.                                                   ║
   ╚═══════════════════════════════════════════════════════════════════════════╝ */

/* ═══════════════════════════════════════════════════════════════════════════
   1. TABBED SECTIONS
   app.js already maintains .active on exactly one .nav-btn[data-target] and
   index.html ships Wall pre-marked. No JS needed.

   Inactive sections are parked at top:-300vh with visibility:hidden rather
   than display:none, deliberately:
     · the <video> elements stay in the render tree, so currentTime keeps
       advancing and the five-second stale detector cannot false-fire while
       you are on another tab;
     · they sit outside the viewport, so the scroll-spy IntersectionObserver
       never fires for them and cannot fight the CSS for which tab is active;
     · overflow above the origin is not scrollable, so a hidden tab adds no
       scroll height;
     · no filter/transform is used, so #drillin (now reparented inside
       #wall-frame) keeps a real fixed-position containing block.
   app.js calls scrollIntoView on the target before adding .active; against a
   parked section that clamps to scroll-top, which is what a tab switch wants.
   ═══════════════════════════════════════════════════════════════════════════ */
.page-wrap { position: relative; }

.page-wrap > .section {
  position: absolute;
  top: -300vh; left: 0; right: 0;
  visibility: hidden;
  pointer-events: none;
}
body:has(.nav-btn[data-target="section-wall"].active)      #section-wall,
body:has(.nav-btn[data-target="section-events"].active)    #section-events,
body:has(.nav-btn[data-target="section-waypoints"].active) #section-waypoints,
body:has(.nav-btn[data-target="section-system"].active)    #section-system,
body:not(:has(.nav-btn.active))                            #section-wall {
  position: relative;
  top: 0;
  visibility: visible;
  pointer-events: auto;
}
/* .hidden stays authoritative — permission gating must still win. */
.page-wrap > .section.hidden { display: none !important; }

/* #drillin is reparented into #wall-frame and sized to the wall's visible area
   by design, so it cannot be shown while the wall tab is parked. Opening it
   from any tab therefore promotes the wall tab to visible and parks the rest.
   Closing it falls straight back to whichever .nav-btn is still .active, so
   drill-in from Cameras returns you to Cameras. */
body:has(#drillin:not(.hidden)) .page-wrap > .section {
  position: absolute; top: -300vh; visibility: hidden; pointer-events: none;
}
body:has(#drillin:not(.hidden)) #section-wall {
  position: relative !important;
  top: 0 !important;
  visibility: visible !important;
  pointer-events: auto !important;
}

/* ═══════════════════════════════════════════════════════════════════════════
   2. GLITCH FLIP
   app.js: playUiTransition() adds .ui-glitch-enter to the target section on
   every nav click and strips it after 560ms. Existing hook, no JS change.
   ═══════════════════════════════════════════════════════════════════════════ */
.ui-glitch-enter {
  animation: glitch-flip .54s var(--ease-gta) both !important;
  transform-origin: 50% 40%;
  will-change: transform, filter, clip-path;
}
@keyframes glitch-flip {
  0% {
    opacity: 0;
    transform: perspective(1200px) rotateX(-46deg) scale(.94);
    clip-path: inset(47% 0 47% 0);
    filter: brightness(2.6) saturate(2.2) hue-rotate(28deg) blur(3px);
  }
  9% {
    opacity: 1;
    transform: perspective(1200px) rotateX(-14deg) translate3d(-16px, 0, 0) scale(1.015);
    clip-path: inset(28% 0 34% 0);
    filter: brightness(1.9) saturate(1.9) hue-rotate(-24deg) blur(0);
  }
  17% {
    transform: perspective(1200px) rotateX(9deg) translate3d(14px, -3px, 0) scale(.996);
    clip-path: inset(4% 0 52% 0);
    filter: brightness(1.5) saturate(1.6) hue-rotate(20deg);
  }
  25% {
    transform: perspective(1200px) rotateX(-5deg) translate3d(-9px, 2px, 0);
    clip-path: inset(46% 0 6% 0);
    filter: brightness(1.7) saturate(1.4) hue-rotate(-14deg);
  }
  34% {
    transform: perspective(1200px) rotateX(3deg) translate3d(7px, 0, 0) scale(1.006);
    clip-path: inset(0 0 0 0);
    filter: brightness(1.3) saturate(1.25);
  }
  46% { transform: translate3d(-4px, 0, 0); filter: brightness(1.16) saturate(1.12); }
  58% { transform: translate3d(3px, 0, 0);  filter: brightness(1.08); }
  70% { transform: translate3d(-1px, 0, 0); }
  100% {
    opacity: 1; transform: none; clip-path: inset(0 0 0 0); filter: none;
  }
}

/* RGB channel split + scanline tear, layered over the flipping section */
.ui-glitch-enter::before {
  content: ""; position: absolute; inset: 0; z-index: 60; pointer-events: none;
  background:
    linear-gradient(90deg, rgba(255,62,200,.28), transparent 22%),
    linear-gradient(270deg, rgba(64,224,255,.28), transparent 22%);
  mix-blend-mode: screen;
  animation: glitch-rgb .54s steps(1, end) both;
}
@keyframes glitch-rgb {
  0%,8%   { opacity: 1;  transform: translate3d(-11px, 0, 0); }
  9%,16%  { opacity: .9; transform: translate3d(9px, 0, 0); }
  17%,24% { opacity: 1;  transform: translate3d(-6px, 2px, 0); }
  25%,33% { opacity: .7; transform: translate3d(5px, -2px, 0); }
  34%,52% { opacity: .38; transform: translate3d(-2px, 0, 0); }
  53%,100%{ opacity: 0;  transform: none; }
}
.ui-glitch-enter::after {
  content: ""; position: absolute; inset: 0; z-index: 61; pointer-events: none;
  background:
    repeating-linear-gradient(180deg, rgba(255,255,255,.10) 0 2px, transparent 2px 5px);
  mix-blend-mode: overlay;
  animation: glitch-tear .54s steps(1, end) both;
}
@keyframes glitch-tear {
  0%,10%  { opacity: .9; clip-path: inset(0 0 62% 0); }
  11%,21% { opacity: .8; clip-path: inset(58% 0 12% 0); }
  22%,32% { opacity: .7; clip-path: inset(24% 0 44% 0); }
  33%,44% { opacity: .45; clip-path: inset(72% 0 0 0); }
  45%,100%{ opacity: 0;  clip-path: inset(0 0 100% 0); }
}
.section { position: relative; }   /* anchor for the tear pseudo-elements */

/* Nav button fires its own glitch so the tap feels connected to the flip */
.nav-btn.active { animation: nav-strike .5s steps(1, end); }
@keyframes nav-strike {
  0%,6%   { color: var(--neon-magenta); text-shadow: -3px 0 var(--neon-cyan), 3px 0 var(--neon-magenta); }
  7%,13%  { color: #fff; text-shadow: 3px 0 var(--neon-cyan), -3px 0 var(--neon-magenta); }
  14%,22% { color: var(--neon-cyan); text-shadow: -2px 0 var(--neon-magenta); }
  23%,100%{ color: #fff; text-shadow: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   3. AMPLIFIED MOTION
   The 8000ms sweep was correct as a concept and invisible as a pulse. Motion
   now runs on 2667ms, exactly one third of a sweep revolution, so it stays
   phase-locked to the radar behind it while reading as an actual pulse.
   Amplitudes roughly tripled.
   ═══════════════════════════════════════════════════════════════════════════ */
:root { --pulse: 2667ms; }
body:has(#btn-firestorm[aria-pressed="true"]) { --pulse: 933ms; }

/* ── Camera neon tubes: hard pulse, visible bloom ─────────────────────────── */
.cam-card {
  animation: tube-breathe var(--pulse) ease-in-out infinite !important;
  transition: transform .2s var(--ease-out), border-color .2s !important;
}
@keyframes tube-breathe {
  0%, 100% {
    border-color: color-mix(in srgb, var(--cc) 34%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cc) 26%, transparent),
                inset 0 0 24px -14px var(--cc),
                0 0 16px -6px color-mix(in srgb, var(--cc) 55%, transparent);
  }
  50% {
    border-color: var(--cc);
    box-shadow: inset 0 0 0 1.5px var(--cc),
                inset 0 0 34px -10px var(--cc),
                0 0 42px -6px var(--cc),
                0 0 78px -18px color-mix(in srgb, var(--cc) 85%, transparent);
  }
}
@media (hover: hover) and (pointer: fine) {
  .cam-card:hover {
    transform: translateY(-6px) scale(1.018) !important;
    z-index: 5;
  }
}
/* Top rail is always faintly lit, then wipes hot on hover */
.cam-card::after {
  height: 3px !important;
  transform: scaleX(.18) !important;
  transform-origin: center !important;
  box-shadow: 0 0 14px var(--cc);
  transition: transform .38s var(--ease-out) !important;
}
.cam-card:focus-visible::after { transform: scaleX(1) !important; }
@media (hover: hover) and (pointer: fine) {
  .cam-card:hover::after { transform: scaleX(1) !important; }
}
.cam-card.offline::after { display: none; }

/* Scanline crawl across every live tile, so the grid is never still */
.cam-card:not(.offline) .cam-thumb {
  background-image: repeating-linear-gradient(180deg,
    color-mix(in srgb, var(--cc) 7%, transparent) 0 1px, transparent 1px 4px);
}
.cam-card:not(.offline) .cam-footer::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px; z-index: 3;
  background: linear-gradient(90deg, transparent, var(--cc), transparent);
  animation: rail-run var(--pulse) linear infinite;
  animation-delay: var(--phase);
}
@keyframes rail-run { 0% { transform: translateX(-100%) } 100% { transform: translateX(100%) } }

/* Slot badge and dot both hammer */
.cam-slot { /* glow lives on ::after now — see the slot-glow block below */ }
/* Was animating box-shadow + four border colours on ten elements at 60fps.
   The glow is now a static overlay whose opacity pulses — same read, no paint. */
.cam-slot { position: relative; }
.cam-slot::after {
  content: ""; position: absolute; inset: -1px; border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 22px -2px var(--cc), inset 0 0 12px -6px var(--cc);
  border: 1px solid var(--cc);
  animation: slot-glow 2.4s ease-in-out infinite;
  animation-delay: var(--phase, 0s);
}
@keyframes slot-glow { 0%,100% { opacity: .30; } 50% { opacity: 1; } }
.cam-card:not(.offline) .cam-dot {
  animation: dot-blink var(--pulse) ease-in-out infinite !important;
  animation-delay: var(--phase) !important;
}
/* box-shadow removed from the keyframes; it is now static on .cam-dot. */
@keyframes dot-blink {
  0%,100% { opacity: .35; transform: scale(.82); }
  50%     { opacity: 1;   transform: scale(1.25); }
}

/* Nine signatures: bigger, brighter, unmistakable */
.cam-thumb::before, .cam-thumb::after {
  opacity: 1 !important;
  filter: drop-shadow(0 0 7px var(--cc)) drop-shadow(0 0 14px var(--cc));
}
.cam-card[data-idx="0"] .cam-thumb::before { height: 2.5px !important; width: 40px !important; }
.cam-card[data-idx="1"] .cam-thumb::before { width: 9px !important; height: 9px !important; }
.cam-card[data-idx="2"] .cam-thumb::before { width: 3px !important; box-shadow: 7px 0 var(--cc),14px 0 var(--cc),21px 0 var(--cc),28px 0 var(--cc) !important; }
.cam-card[data-idx="3"] .cam-thumb::before { width: 18px !important; height: 18px !important; border-width: 2.5px !important; }
.cam-card[data-idx="4"] .cam-thumb::before,
.cam-card[data-idx="4"] .cam-thumb::after  { width: 15px !important; height: 2.5px !important; }
.cam-card[data-idx="5"] .cam-thumb::before { width: 4px !important; height: 14px !important; box-shadow: 8px 3px var(--cc),16px -3px var(--cc),24px 4px var(--cc) !important; }
.cam-card[data-idx="6"] .cam-thumb::before { width: 15px !important; height: 14px !important; }
.cam-card[data-idx="7"] .cam-thumb::before { width: 6px !important; height: 6px !important; transform-origin: 3px 13px !important; }
.cam-card[data-idx="8"] .cam-thumb::before { width: 4px !important; height: 4px !important; box-shadow: 9px 0 var(--cc),18px 0 var(--cc),0 9px var(--cc),9px 9px var(--cc),18px 9px var(--cc) !important; }

@keyframes sig-crawl { 0%,100% { transform:translateY(0);    opacity:.3 } 50% { transform:translateY(36px); opacity:1 } }
@keyframes sig-ping  { 0% { transform:scale(.35); opacity:1 } 100% { transform:scale(4.2); opacity:0 } }
@keyframes sig-wave  { 0%,100% { transform:scaleY(1) } 50% { transform:scaleY(5) } }
@keyframes sig-iris  { 0%,100% { transform:scale(.4);  opacity:.45 } 50% { transform:scale(1.15); opacity:1 } }
@keyframes sig-sweep { 0%,100% { transform:translateX(0);    opacity:.3 } 50% { transform:translateX(26px); opacity:1 } }
@keyframes sig-stack { 0%,100% { transform:scaleY(.35); opacity:.45 } 50% { transform:scaleY(1); opacity:1 } }
@keyframes sig-blink { 0%,55% { opacity:1; transform:scale(1) } 56%,100% { opacity:.12; transform:scale(.85) } }
@keyframes sig-grid  { 0%,100% { opacity:.22; transform:scale(.85) } 50% { opacity:1; transform:scale(1.1) } }

/* ── Wall bezel: constant, unmissable, with a light always running the rim ── */
.wall-wrap {
  animation: wall-breathe var(--pulse) ease-in-out infinite !important;
  isolation: isolate;
}
@keyframes wall-breathe {
  0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--wall-hue) 34%, transparent),
                         0 0 30px -8px color-mix(in srgb, var(--wall-hue) 50%, transparent),
                         inset 0 0 60px -34px var(--wall-hue); }
  50%      { box-shadow: 0 0 0 2px var(--wall-hue),
                         0 0 60px -6px var(--wall-hue),
                         0 0 120px -22px color-mix(in srgb, var(--wall-hue) 80%, transparent),
                         inset 0 0 90px -26px var(--wall-hue); }
}
.wall-wrap::after { opacity: 1 !important; animation: bracket-flare var(--pulse) ease-in-out infinite; }
@keyframes bracket-flare {
  0%,100% { filter: drop-shadow(0 0 3px var(--wall-hue)); opacity: .6; }
  50%     { filter: drop-shadow(0 0 12px var(--wall-hue)) drop-shadow(0 0 22px var(--wall-hue)); opacity: 1; }
}
/* Perimeter light now runs on every state, not just recovering */
.wall-wrap::before {
  content: ""; position: absolute; inset: -1px; z-index: 5; pointer-events: none;
  border-radius: inherit; padding: 2px;
  background: conic-gradient(from 0turn, transparent 0 62%,
              color-mix(in srgb, var(--wall-hue) 60%, transparent) 78%,
              #fff 86%,
              color-mix(in srgb, var(--wall-hue) 60%, transparent) 92%, transparent 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  animation: perimeter-run 3.4s linear infinite;
}
body:has(#sentinel-health-state.sentinel-recovering) .wall-wrap::before,
body:has(#btn-firestorm[aria-pressed="true"]) .wall-wrap::before { animation-duration: 1.2s; }

/* ── Instrument cluster: numbers that visibly tick ────────────────────────── */
.sb-num { font-size: 26px; letter-spacing: .02em; }
.sb-num.accent {
  animation: num-throb var(--pulse) ease-in-out infinite;
}
@keyframes num-throb {
  0%,100% { text-shadow: 0 0 14px color-mix(in srgb, var(--neon-cyan) 45%, transparent); }
  50%     { text-shadow: 0 0 26px var(--neon-cyan), 0 0 52px color-mix(in srgb, var(--neon-cyan) 70%, transparent); }
}
.sb-num.recording-on { animation: rec-throb 1.1s ease-in-out infinite !important; }
@keyframes rec-throb {
  0%,100% { opacity: .55; text-shadow: 0 0 10px rgba(255,77,109,.4); }
  50%     { opacity: 1;   text-shadow: 0 0 24px var(--st-offline), 0 0 48px rgba(255,77,109,.6); }
}
.stat-bar { position: relative; overflow: hidden; }
.stat-bar::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 2px; width: 34%;
  background: linear-gradient(90deg, transparent, var(--neon-cyan), var(--neon-magenta), transparent);
  animation: rail-run 4.2s linear infinite;
}
.chip.live i { animation: dot-blink var(--pulse) ease-in-out infinite !important; --cc: var(--st-healthy); }

/* ── Section headers get a running gradient ───────────────────────────────── */
.section-hdr h2 {
  background: linear-gradient(94deg, #fff 0%, var(--neon-cyan) 30%, var(--neon-magenta) 52%, var(--neon-cyan) 74%, #fff 100%);
  background-size: 280% 100%;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  animation: hdr-sweep 5.4s linear infinite;
}
@keyframes hdr-sweep { 0% { background-position: 180% 0 } 100% { background-position: -80% 0 } }

/* ── Global CRT layer. It intentionally remains over the live picture. ───── */
body::after {
  content: ""; position: fixed; inset: 0; z-index: 75; pointer-events: none;
  background: repeating-linear-gradient(180deg, rgba(180,230,255,.030) 0 1px, transparent 1px 3px);
  mix-blend-mode: overlay;
}
.page-wrap::after {
  content: ""; position: fixed; left: 0; right: 0; height: 34vh; top: -34vh;
  z-index: 76; pointer-events: none; mix-blend-mode: screen;
  background: linear-gradient(180deg, transparent, rgba(64,224,255,.045) 60%, rgba(255,62,200,.05) 82%, transparent);
  animation: crt-roll 9s linear infinite;
}
@keyframes crt-roll { 0% { top: -34vh } 100% { top: 100vh } }

/* ── Topbar wordmark flickers like a real sign ────────────────────────────── */
.brand-text strong { animation: neon-buzz 5.2s steps(1, end) infinite; }
.topbar::after { animation: shimmer 4s linear infinite !important; }

/* ── Firestorm hits harder ────────────────────────────────────────────────── */
body:has(#btn-firestorm[aria-pressed="true"]) .cam-card { animation-duration: var(--pulse) !important; }
body:has(#btn-firestorm[aria-pressed="true"])::after {
  background: repeating-linear-gradient(180deg, rgba(255,80,60,.085) 0 1px, transparent 1px 3px);
  animation: firestorm-throb .9s ease-in-out infinite;
}

/* ═══════════════════════════════════════════════════════════════════════════
   4. REDUCED MOTION — everything above goes away
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .ui-glitch-enter, .ui-glitch-enter::before, .ui-glitch-enter::after,
  .cam-card, .cam-slot, .cam-dot, .wall-wrap, .wall-wrap::before, .wall-wrap::after,
  .sb-num, .stat-bar::after, .section-hdr h2, .brand-text strong,
  .cam-card:not(.offline) .cam-footer::before, .page-wrap::after, .nav-btn.active,
  /* These carry !important, so a bare .cam-dot loses the specificity tiebreak
     and nine dots kept blinking under reduced motion. Match them exactly. */
  .cam-card:not(.offline) .cam-dot,
  .cam-slot::after, .cam-tube, .cam-gas, .cam-gas::before, .cam-gas::after,
  .cam-thumb.has-frame, .cam-thumb::before, .cam-thumb::after,
  .wall-tube::before, .wall-tube::after, .chip.live i {
    animation: none !important;
  }
  .ui-glitch-enter { filter: none !important; transform: none !important; clip-path: none !important; opacity: 1 !important; }
  .ui-glitch-enter::before, .ui-glitch-enter::after { display: none !important; }
  body::after, .page-wrap::after { display: none !important; }
  .section-hdr h2 { -webkit-text-fill-color: var(--text); color: var(--text); background: none; }
  .cam-card { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--cc) 70%, transparent); }
  .wall-wrap { box-shadow: 0 0 0 2px var(--wall-hue); }
}
body:has(#reduce-motion:checked) .ui-glitch-enter,
body:has(#reduce-motion:checked) .cam-card,
body:has(#reduce-motion:checked) .cam-slot,
body:has(#reduce-motion:checked) .cam-dot,
body:has(#reduce-motion:checked) .wall-wrap,
body:has(#reduce-motion:checked) .wall-wrap::before,
body:has(#reduce-motion:checked) .wall-wrap::after,
body:has(#reduce-motion:checked) .sb-num,
body:has(#reduce-motion:checked) .stat-bar::after,
body:has(#reduce-motion:checked) .section-hdr h2,
body:has(#reduce-motion:checked) .brand-text strong,
body:has(#reduce-motion:checked) .cam-card:not(.offline) .cam-footer::before { animation: none !important; }
body:has(#reduce-motion:checked) .ui-glitch-enter::before,
body:has(#reduce-motion:checked) .ui-glitch-enter::after,
body:has(#reduce-motion:checked) body::after,
body:has(#reduce-motion:checked) .page-wrap::after { display: none !important; }
body:has(#reduce-motion:checked) .section-hdr h2 { -webkit-text-fill-color: var(--text); color: var(--text); background: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   5. BRAND LOCKUP
   The topbar wordmark becomes the real ParaN(oid)VR logotype instead of a
   Bebas Neue approximation. Image replacement, not markup change: the text
   node stays in the accessibility tree and is pushed off-box visually, so
   screen readers and the document title still read "ParaN(oid)VR".
   ═══════════════════════════════════════════════════════════════════════════ */
.brand-text strong {
  display: block;
  width: 172px; height: 31px;
  /* cancel the gradient-clipped-to-text treatment this replaces */
  background: url("/brand/wordmark.webp") left center / contain no-repeat;
  -webkit-background-clip: border-box; background-clip: border-box;
  -webkit-text-fill-color: transparent; color: transparent;
  /* keep the text for assistive tech, hide it visually */
  text-indent: -200vw; overflow: hidden; white-space: nowrap;
  filter: drop-shadow(0 0 9px rgba(255,62,200,.34)) drop-shadow(0 0 18px rgba(64,224,255,.22));
  animation: neon-buzz 5.2s steps(1, end) infinite;
}
.brand-text span { margin-top: 1px; }
.login-brand .brand-icon { border-radius: var(--r-md); }

/* Firestorm heats the logotype with the rest of the page */
body:has(#btn-firestorm[aria-pressed="true"]) .brand-text strong {
  filter: drop-shadow(0 0 10px rgba(255,90,60,.55)) drop-shadow(0 0 22px rgba(255,45,85,.34))
          hue-rotate(-42deg) saturate(1.35);
}

@media (max-width: 520px) {
  .brand-text strong { width: 138px; height: 25px; }
}
@media (max-width: 380px) {
  .brand-text strong { width: 118px; height: 21px; }
  .brand-icon { width: 32px; height: 32px; }
}
@media (prefers-reduced-motion: reduce) {
  .brand-text strong { animation: none !important; }
}
body:has(#reduce-motion:checked) .brand-text strong { animation: none !important; }


/* ╔═══════════════════════════════════════════════════════════════════════════╗
   ║  v14d — NEON GAS TUBES · DIRECTIONAL TRANSITIONS · ONE CIRCUIT            ║
   ║                                                                           ║
   ║  Appended as an override layer so v13 above stays readable as its own     ║
   ║  diff. Three things happen here:                                          ║
   ║    1. .cam-card stops faking a neon tube with box-shadow and gets a real   ║
   ║       masked glass ring with gas moving inside it.                        ║
   ║    2. Section switching gains an EXIT animation, a direction, a shutter    ║
   ║       wipe and staggered children. Previously the outgoing section simply  ║
   ║       ceased to exist, which is why the transition read as a cut.          ║
   ║    3. Every neon element stops running its own private loop and reacts to  ║
   ║       one sweep angle published by canvas-bg.js.                           ║
   ╚═══════════════════════════════════════════════════════════════════════════╝ */

/* ═══════════════════════════════════════════════════════════════════════════
   0. REGISTERED PROPERTIES
   --sweep-deg is written by canvas-bg.js roughly ten times a second and is
   registered here so the browser can interpolate it. Without @property it is
   an unanimatable token and the circuit band would visibly step. The value is
   monotonic and never wraps, so the band never spins backwards.
   ═══════════════════════════════════════════════════════════════════════════ */
@property --sweep-deg { syntax: "<angle>";      inherits: true;  initial-value: 0deg; }
@property --gas-a     { syntax: "<percentage>"; inherits: false; initial-value: 40%;  }
@property --gas-turn  { syntax: "<angle>";      inherits: false; initial-value: 0deg; }

:root {
  --sweep-deg: 0deg;
  transition: --sweep-deg 140ms linear;
  --tube-w: 7px;          /* glass wall to glass wall */
  --tube-r: var(--r-md);
}

/* The scroll-driven variant never fires now that sections are parked off
   screen by the v13 tab system, so it is retired rather than left to rot. */
@supports (animation-timeline: view()) {
  .section { animation-timeline: auto; animation-range: normal; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   1. THE TUBE
   Anatomy, outside in:
     .cam-tube   bloom carrier. Unmasked, so its drop-shadow can escape.
     .cam-gas      the ring itself, clipped to a hollow rounded rectangle by a
                   two-layer mask composited with exclude. Everything inside it
                   is confined to the glass.
       ::before    the gas: a conic hot band, blurred, rotating.
       ::after     the glass: outer specular edge plus a raking sheen.
     .cam-tube::after   electrode caps where the tube enters the frame.
     .cam-haze   the interior. Contained gas glow filling the cell, plus the
                   inner glass line.

   .cam-card loses overflow:hidden so the bloom is not amputated at the border.
   Clipping moves to .cam-thumb, which is the only child that needs it.
   ═══════════════════════════════════════════════════════════════════════════ */
.cam-card {
  overflow: visible;
  border-color: transparent !important;
  /* The v13 box-shadow tube is replaced wholesale. A single soft ambient
     shadow stays so cards still sit on the page rather than float. */
  animation: none !important;
  box-shadow: 0 10px 26px -14px rgba(0,0,0,.75) !important;
  transition: transform .24s var(--ease-out) !important;
}
.cam-card::before { border-radius: inherit; }
.cam-thumb { border-radius: inherit; overflow: hidden; }
.cam-footer { border-radius: 0 0 var(--tube-r) var(--tube-r); }
.cam-card::after {
  left: calc(var(--tube-w) + 2px) !important;
  right: calc(var(--tube-w) + 2px) !important;
  top: var(--tube-w) !important;
  border-radius: 2px;
}

.cam-tube {
  position: absolute; inset: 0; z-index: 4;
  border-radius: inherit; pointer-events: none;
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--cc) 60%, transparent))
          drop-shadow(0 0 17px color-mix(in srgb, var(--cc) 34%, transparent));
  /* drop-shadow is a main-thread filter; animating it ten times over was the
     single most expensive thing on this tab. The filter is now static and the
     layer's opacity breathes instead. */
  animation: tube-bloom-opacity var(--pulse) ease-in-out infinite;
  animation-delay: var(--phase);
}
@keyframes tube-bloom-opacity { 0%, 100% { opacity: .58; } 50% { opacity: 1; } }
@keyframes tube-bloom {
  0%, 100% { filter: drop-shadow(0 0 4px  color-mix(in srgb, var(--cc) 45%, transparent))
                     drop-shadow(0 0 13px color-mix(in srgb, var(--cc) 22%, transparent)); }
  50%      { filter: drop-shadow(0 0 9px  color-mix(in srgb, var(--cc) 92%, transparent))
                     drop-shadow(0 0 30px color-mix(in srgb, var(--cc) 58%, transparent))
                     drop-shadow(0 0 58px color-mix(in srgb, var(--cc) 30%, transparent)); }
}

/* Electrodes: the two hot nubs where a real tube enters its housing. */
.cam-tube::after {
  content: ""; position: absolute; z-index: 2;
  left: 0; right: 0; top: 0;
  height: calc(var(--tube-w) * 1.15);
  pointer-events: none;
  background:
    radial-gradient(closest-side circle at 50% 50%, #fff 8%,
      color-mix(in srgb, var(--cc) 92%, #fff) 42%, transparent 78%),
    radial-gradient(closest-side circle at 50% 50%, #fff 8%,
      color-mix(in srgb, var(--cc) 92%, #fff) 42%, transparent 78%);
  background-size: calc(var(--tube-w) * 3.4) 100%, calc(var(--tube-w) * 3.4) 100%;
  background-position: 21% 50%, 79% 50%;
  background-repeat: no-repeat;
  opacity: .95;
}

.cam-gas {
  position: absolute; inset: 0;
  border-radius: inherit;
  padding: var(--tube-w);
  overflow: hidden;
  /* Hollow-ring mask: full box minus content box. */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--cc) 62%, transparent),
      color-mix(in srgb, var(--cc) 34%, transparent) 45%,
      color-mix(in srgb, var(--cc) 74%, transparent));
}
/* The gas. A hot band sweeping the circumference, blurred so it reads as
   luminous vapour rather than a gradient border. */
.cam-gas::before {
  content: ""; position: absolute;
  left: -50%; top: -50%; width: 200%; height: 200%;
  background: conic-gradient(from 0deg,
    transparent 0deg,
    color-mix(in srgb, var(--cc) 22%, transparent) 20deg,
    color-mix(in srgb, var(--cc) 90%, #fff) 44deg,
    #fff 56deg,
    color-mix(in srgb, var(--cc) 88%, #fff) 70deg,
    color-mix(in srgb, var(--cc) 30%, transparent) 110deg,
    transparent 158deg,
    color-mix(in srgb, var(--cc) 45%, transparent) 226deg,
    color-mix(in srgb, var(--cc) 95%, #fff) 258deg,
    #fff 268deg,
    color-mix(in srgb, var(--cc) 20%, transparent) 306deg,
    transparent 360deg);
  filter: blur(3.4px) saturate(1.6) contrast(1.15);
  animation: gas-swirl 7.5s linear infinite;
  animation-delay: var(--phase);
  will-change: transform;
}
@keyframes gas-swirl { to { transform: rotate(360deg); } }

/* The glass. Outer specular edge plus a raking sheen — this is what separates
   "tube with gas in it" from "thick glowing border". */
.cam-gas::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: inset 0 0 0 1.3px rgba(255,255,255,.46),
              inset 0 0 6px -2px rgba(255,255,255,.30);
  background: linear-gradient(148deg,
    rgba(255,255,255,.42) 0%, rgba(255,255,255,.10) 24%,
    transparent 46%, rgba(255,255,255,.16) 90%);
  mix-blend-mode: screen;
}

/* The interior. Gas escapes the tube and fills the cell, which is the half of
   this that a border alone can never do. Screen blend keeps the thumbnail
   readable underneath instead of washing it out. */
.cam-haze {
  position: absolute; inset: var(--tube-w); z-index: 2;
  border-radius: calc(var(--tube-r) - var(--tube-w));
  pointer-events: none;
  mix-blend-mode: screen;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.09);
  background:
    radial-gradient(78% 62% at 22% 8%,  color-mix(in srgb, var(--cc) 26%, transparent), transparent 72%),
    radial-gradient(66% 74% at 88% 96%, color-mix(in srgb, var(--cc) 20%, transparent), transparent 70%),
    radial-gradient(120% 100% at 50% 50%, transparent 42%, color-mix(in srgb, var(--cc) 14%, transparent));
  animation: haze-drift 11s ease-in-out infinite alternate;
  animation-delay: var(--phase);
}
@keyframes haze-drift {
  0%   { background-position: 0% 0%, 0% 0%, 0 0;   opacity: .78; }
  100% { background-position: 14% 9%, -12% -8%, 0 0; opacity: 1; }
}

/* Strike. A real tube does not fade up, it stutters and catches. */
.cam-card:focus-visible .cam-tube {
  animation: tube-strike 1.05s steps(1, end) 1 forwards, tube-bloom var(--pulse) ease-in-out infinite 1.05s;
}
@media (hover: hover) and (pointer: fine) {
  .cam-card:hover { transform: translateY(-6px) scale(1.02) !important; }
  .cam-card:hover .cam-tube {
    animation: tube-strike 1.05s steps(1, end) 1 forwards, tube-bloom var(--pulse) ease-in-out infinite 1.05s;
  }
  .cam-card:hover .cam-gas::before { animation-duration: 2.6s; }
  .cam-card:hover .cam-haze { opacity: 1; }
}
@keyframes tube-strike {
  0%, 4%   { filter: drop-shadow(0 0 2px color-mix(in srgb, var(--cc) 20%, transparent)); }
  5%       { filter: drop-shadow(0 0 14px #fff) drop-shadow(0 0 40px var(--cc)); }
  6%, 8%   { filter: drop-shadow(0 0 2px color-mix(in srgb, var(--cc) 16%, transparent)); }
  9%       { filter: drop-shadow(0 0 16px #fff) drop-shadow(0 0 48px var(--cc)); }
  10%, 12% { filter: drop-shadow(0 0 3px color-mix(in srgb, var(--cc) 30%, transparent)); }
  13%,100% { filter: drop-shadow(0 0 10px color-mix(in srgb, var(--cc) 95%, transparent))
                     drop-shadow(0 0 34px color-mix(in srgb, var(--cc) 70%, transparent))
                     drop-shadow(0 0 70px color-mix(in srgb, var(--cc) 38%, transparent)); }
}
/* ═══════════════════════════════════════════════════════════════════════════
   2. GAS CARRIES HEALTH
   Status stops being a word in a footer and becomes the state of the gas.
   You read the room before you read a label.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Offline: the gas drains out and leaves dead glass. */
.cam-card.offline { opacity: 1; }
.cam-card.offline .cam-tube {
  animation: none !important;
  filter: drop-shadow(0 0 2px rgba(125,142,163,.25)) !important;
}
.cam-card.offline .cam-gas {
  background: linear-gradient(180deg, rgba(90,104,120,.30), rgba(58,70,84,.22));
}
.cam-card.offline .cam-gas::before {
  background: linear-gradient(180deg, color-mix(in srgb, var(--cc) 70%, transparent), transparent 70%);
  filter: blur(6px);
  animation: gas-drain 1.25s cubic-bezier(.5,0,.75,0) both;
}
@keyframes gas-drain {
  0%   { transform: translateY(0);    opacity: .9; }
  70%  { transform: translateY(62%);  opacity: .35; }
  100% { transform: translateY(112%); opacity: 0; }
}
.cam-card.offline .cam-gas::after { box-shadow: inset 0 0 0 1.1px rgba(255,255,255,.16); }
.cam-card.offline .cam-haze { opacity: .10; animation: none; }
/* One dead-tube stutter on a long cycle, so the grid is never quite still. */
.cam-card.offline .cam-gas { animation: dead-glass 13s steps(1, end) infinite; }
@keyframes dead-glass {
  0%, 93%   { opacity: .55; }
  94%       { opacity: .95; }
  95%, 96%  { opacity: .40; }
  97%       { opacity: .88; }
  98%, 100% { opacity: .55; }
}

/* Degraded: amber striations drift through otherwise healthy gas. */
body:has(#sentinel-health-state.sentinel-degraded) .cam-card:not(.offline) .cam-gas,
body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas {
  background-image:
    linear-gradient(180deg,
      color-mix(in srgb, var(--cc) 62%, transparent),
      color-mix(in srgb, var(--cc) 34%, transparent) 45%,
      color-mix(in srgb, var(--cc) 74%, transparent)),
    repeating-linear-gradient(118deg,
      color-mix(in srgb, var(--st-degraded) 55%, transparent) 0 3px,
      transparent 3px 11px);
  background-size: 100% 100%, 240% 240%;
  animation: gas-striate 3.2s linear infinite;
}
@keyframes gas-striate {
  0%   { background-position: 0 0, 0 0; }
  100% { background-position: 0 0, 240% 0; }
}

/* Reconnecting: the gas surges electrode to electrode, trying to strike. */
body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas::before,
/* .wall-wrap gained a .wall-tube wrapper, so its old general-sibling
   selector matched nothing — the tube is the sibling of the camera grid now. */
.wall-tube:has(#wall-state.reconnecting) ~ * .cam-gas::before {
  animation: gas-swirl 1.6s linear infinite, gas-surge 1.1s ease-in-out infinite;
}
@keyframes gas-surge { 0%,100% { opacity: .35 } 45% { opacity: 1 } }

/* ── Phone budget ───────────────────────────────────────────────────────────
   Ten cards x seven infinite animations is not a phone budget. The tube, the
   swirl and the rail carry the identity; the second gas layer and the offline
   glass shimmer are decoration and come off on a coarse pointer. */
@media (pointer: coarse) {
  /* Reconnecting drives a 1.6s swirl *plus* a 1.1s surge across ten masked
     conic gradients at once — the single most expensive state on this tab,
     and it fires exactly when the device is already busy re-establishing a
     stream. The alarm reads fine at half the rate. */
  body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas::before,
  .wall-tube:has(#wall-state.reconnecting) ~ * .cam-gas::before {
    animation: gas-swirl 3.4s linear infinite, gas-surge 2.2s ease-in-out infinite;
  }
  .cam-card.offline .cam-thumb { animation: none !important; }
  .cam-slot::after { animation-duration: 3.6s; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   3. THE WALL GETS THE SAME TUBE, LARGER
   ═══════════════════════════════════════════════════════════════════════════ */
.wall-wrap {
  --tube-w: 11px;
  border-color: transparent !important;
  box-shadow: 0 18px 44px -22px rgba(0,0,0,.8) !important;
  animation: wall-bloom var(--pulse) ease-in-out infinite !important;
}
@keyframes wall-bloom {
  0%,100% { box-shadow: 0 18px 44px -22px rgba(0,0,0,.8),
                        0 0 30px -10px color-mix(in srgb, var(--wall-hue) 50%, transparent); }
  50%     { box-shadow: 0 18px 44px -22px rgba(0,0,0,.8),
                        0 0 66px -8px color-mix(in srgb, var(--wall-hue) 78%, transparent),
                        0 0 130px -30px color-mix(in srgb, var(--wall-hue) 55%, transparent); }
}
/* ::before is the perimeter runner from v13 and stays. ::after becomes the
   glass ring so the wall bezel matches the cell tubes exactly. */
.wall-wrap::after {
  content: ""; position: absolute; inset: 0; z-index: 4;
  border-radius: inherit; pointer-events: none;
  padding: var(--tube-w);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--wall-hue) 58%, transparent),
      color-mix(in srgb, var(--wall-hue) 28%, transparent) 48%,
      color-mix(in srgb, var(--wall-hue) 70%, transparent)),
    radial-gradient(60% 140% at 20% 0%, rgba(255,255,255,.30), transparent 60%);
  box-shadow: inset 0 0 0 1.2px rgba(255,255,255,.30);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 1 !important;
  animation: wall-gas var(--pulse) ease-in-out infinite !important;
  filter: none;
}
@keyframes wall-gas {
  0%,100% { background-position: 0 0, 12% 0; }
  50%     { background-position: 0 0, 88% 0; }
}

/* Live-video contract: the wall itself has no decorative pixel layer.  The
   v14 tube mask is not reliable over Chromium's hardware-decoded video plane
   and can paint a green wedge across the picture.  CRT scanlines and roll
   remain global presentation effects; map/radar/cone/circuit/tube effects are
   prohibited inside the live wall. */
#wall-frame.wall-wrap {
  /* The bezel is .wall-tube's padding ring now, so the frame keeps only a
     hairline seat. Two borders read as a double frame. */
  border: 1px solid rgba(0,0,0,.55) !important;
  box-shadow: inset 0 0 22px -10px rgba(0,0,0,.9) !important;
  animation: none !important;
}
#wall-frame.wall-wrap::before,
#wall-frame.wall-wrap::after {
  content: none !important;
  display: none !important;
  background: none !important;
  animation: none !important;
  filter: none !important;
}

/* ── Motion flare ───────────────────────────────────────────────────────────
   The cards are already colour-coded per camera. A motion event drives that
   card's tube to hot amber and back, so the colour carries information rather
   than only identity. opacity + transform only. */
.cam-card.cam-flare { --cc: #ffb03c; }
.cam-card.cam-flare::before { animation: flare-strike 4.2s ease-out both; }
.cam-card.cam-flare .cam-tube { animation: flare-bloom 4.2s ease-out both; }
@keyframes flare-strike {
  0%   { opacity: 1; }
  6%   { opacity: .2; }
  12%  { opacity: 1; }
  22%  { opacity: .45; }
  30%  { opacity: 1; }
  100% { opacity: .34; }
}
@keyframes flare-bloom {
  0%   { opacity: 1; transform: scale(1.015); }
  60%  { opacity: 1; transform: scale(1); }
  100% { opacity: .58; transform: scale(1); }
}
body:has(#reduce-motion:checked) .cam-card.cam-flare::before,
body:has(#reduce-motion:checked) .cam-card.cam-flare .cam-tube { animation: none !important; }

/* ── Wall gas tube ──────────────────────────────────────────────────────────
   One ring around the whole wall, not ten around the cells.

   §3 above documents why the wall gets no decorative pixel layer: a masked
   tube over Chromium's hardware-decoded video plane can paint a green wedge
   across the picture. That contract is intact — nothing here is layered over
   #wall-frame. The gas is the *padding box* of a wrapper, with the opaque
   .wall-wrap sitting on top of the centre, so the decoration cannot reach the
   video even if compositing misbehaves. */
.wall-tube {
  --wall-tube-w: 10px;
  position: relative;
  padding: var(--wall-tube-w);
  border-radius: calc(var(--r-md) + var(--wall-tube-w));
  overflow: hidden;
  isolation: isolate;
  z-index: 2;
  background: rgba(3, 8, 16, .9);
  box-shadow: 0 18px 44px -22px rgba(0,0,0,.82),
              0 0 34px -14px color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 46%, transparent);
}
/* The gas: a slow conic sweep, clipped to the wrapper, hidden behind the
   wall in the centre and visible only in the ring. */
.wall-tube::before {
  content: ""; position: absolute; z-index: 0;
  left: 50%; top: 50%; width: 190%; aspect-ratio: 1; translate: -50% -50%;
  background: conic-gradient(from 0deg,
    transparent 0deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 18%, transparent) 26deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 88%, #fff) 50deg,
    #fff 62deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 86%, #fff) 76deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 20%, transparent) 104deg,
    transparent 150deg,
    color-mix(in srgb, var(--neon-magenta) 26%, transparent) 236deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 44%, transparent) 292deg,
    transparent 340deg);
  animation: wall-tube-swirl 11s linear infinite;
}
/* Hot inner electrode edge, so the ring reads as glass rather than a gradient. */
.wall-tube::after {
  content: ""; position: absolute; z-index: 1;
  /* Flush with the frame, never 1.5px inside it — an inward offset put the
     highlight's spread onto the frame's rounded corner, which showed up as a
     single antialiased pixel inside the picture. The shadow spreads outward
     into the padding only. */
  inset: var(--wall-tube-w);
  border-radius: var(--r-md);
  pointer-events: none;
  box-shadow: 0 0 0 1.5px rgba(255,255,255,.34),
              0 0 18px -2px color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 70%, transparent);
  animation: wall-tube-breathe var(--sweep) ease-in-out infinite;
}
.wall-tube > .wall-wrap {
  position: relative; z-index: 2;   /* covers the centre of the conic */
}
@keyframes wall-tube-swirl  { to { rotate: 360deg; } }
@keyframes wall-tube-breathe { 0%,100% { opacity: .48; } 50% { opacity: 1; } }

/* Reconnecting drives the whole bezel amber and speeds the gas — the wall's
   own version of the card alarm. */
body:has(#wall-state.reconnecting) .wall-tube,
body:has(#sentinel-health-state.sentinel-recovering) .wall-tube {
  --wall-hue: #ff9b3c;
}
body:has(#wall-state.reconnecting) .wall-tube::before {
  animation-duration: 4.2s;
}
@media (pointer: coarse) {
  .wall-tube { --wall-tube-w: 8px; }
  .wall-tube::before { animation-duration: 15s; }
}

/* ── Viewer control dock ────────────────────────────────────────────────────
   Lives in the page flow between #wall-frame and #wall-cameras, so it pushes
   the camera cells down rather than covering the picture. Overlaying the video
   was worse than hiding the controls, which is what the previous pass did. */
.fs-dock { display: none; }
.fs-dock-head {
  display: flex; align-items: center; gap: 10px; margin-bottom: 10px;
}
.fs-dock-title {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--faint);
}
.fs-dock-head::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--neon-cyan) 40%, transparent), transparent);
}
.fs-dock-body {
  display: grid; gap: 10px; justify-items: center;
  grid-template-columns: repeat(auto-fit, 46px);
  justify-content: center;
}

@media (max-width: 820px) and (pointer: coarse) {
  body:has(#drillin:not(.hidden)) #fs-dock:not(.hidden) {
    display: block;
    margin: 14px 0 0;
    padding: 13px 13px 15px;
    border-radius: var(--r-md);
    background: rgba(8, 15, 26, .96);
    border: 1px solid color-mix(in srgb, var(--neon-cyan) 20%, transparent);
    box-shadow: 0 0 26px -10px var(--neon-cyan), var(--sh-2);
  }
  /* Buttons the windowed viewer rules hide are re-shown here, re-homed. */
  #fs-dock .fs-btn {
    display: grid !important;
    position: static !important;
    top: auto !important; left: auto !important; right: auto !important;
    bottom: auto !important; opacity: 1 !important;
    width: 46px; height: 46px;
  }
  #fs-dock .fs-btn.hidden { display: none !important; }
  #fs-dock .fs-btn.is-on { border-color: var(--neon-cyan); color: var(--neon-cyan); }

  /* A real D-pad cross plus a zoom stack, rather than six buttons in a row.
     Thumb geometry matters more here than it does in fullscreen. */
  #fs-dock .ptz-overlay.ptz-docked {
    display: grid !important;
    position: static !important; inset: auto !important;
    grid-column: 1 / -1; justify-self: center;
    grid-template-columns: repeat(3, 52px) 20px 52px;
    grid-template-rows: repeat(3, 52px);
    gap: 6px; place-items: center;
    width: auto; pointer-events: auto !important;
    margin-top: 8px;
  }
  #fs-dock .ptz-overlay.ptz-docked .ptz-up    { grid-area: 1 / 2; }
  #fs-dock .ptz-overlay.ptz-docked .ptz-left  { grid-area: 2 / 1; }
  #fs-dock .ptz-overlay.ptz-docked .ptz-right { grid-area: 2 / 3; }
  #fs-dock .ptz-overlay.ptz-docked .ptz-down  { grid-area: 3 / 2; }
  #fs-dock .ptz-overlay.ptz-docked .ptz-zin   { grid-area: 1 / 5; }
  #fs-dock .ptz-overlay.ptz-docked .ptz-zout  { grid-area: 2 / 5; }
  /* Centre of the cross reads as a rest position, not a dead cell. */
  #fs-dock .ptz-overlay.ptz-docked::before {
    content: ""; grid-area: 2 / 2; width: 12px; height: 12px; border-radius: 50%;
    border: 1px solid color-mix(in srgb, var(--neon-cyan) 34%, transparent);
  }
  /* .hidden is !important but lower specificity than the docked display
     rules, so without these two lines the D-pad stayed on screen during
     playback. State classes must always win over layout classes. */
  #fs-dock .ptz-overlay.hidden,
  #fs-dock .preset-strip.hidden { display: none !important; }
  #fs-dock .ptz-overlay.ptz-docked .ptz-btn {
    display: grid !important; position: static !important;
    transform: none !important; inset: auto !important;
    width: 46px; height: 46px; font-size: 19px; opacity: 1 !important;
  }
  #fs-dock .ptz-overlay.ptz-docked .ptz-duration {
    display: flex !important; position: static !important;
    transform: none !important; inset: auto !important;
    grid-column: 1 / -1; grid-row: 4; opacity: 1 !important;
    padding: 7px 13px; width: 100%; margin-top: 6px;
  }
  #fs-dock .preset-strip {
    position: static !important; transform: none !important;
    grid-column: 1 / -1; width: 100%; margin-top: 4px;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   4. SECTION TRANSITIONS
   The v13 tab system parks inactive sections at top:-300vh, so the outgoing
   section had no chance to animate — it was simply gone on the same frame.
   .ui-exit lifts it back out of the park, absolutely positioned over the page
   so it cannot push layout, long enough to play a real exit.

   Direction comes from --nav-dir (-1 back, 1 forward), set by app.js from the
   tab index delta, so Wall→System throws left and System→Wall throws right.
   ═══════════════════════════════════════════════════════════════════════════ */
:root { --nav-dir: 1; }
/* scrollIntoView honours scroll-padding on the *scrolling element*, which is
   <html>, not .page-wrap — putting it there is why the sticky nav still ate
   the first row of every tab. */
html { scroll-padding-top: 118px; }
@media (max-width: 520px) { html { scroll-padding-top: 132px; } }

.page-wrap > .section.ui-exit {
  position: absolute !important;
  top: 0 !important; left: 0; right: 0;
  visibility: visible !important;
  pointer-events: none !important;
  z-index: 9;
  animation: sec-exit .52s var(--ease-gta) both !important;
}
@keyframes sec-exit {
  0%   { opacity: 1; transform: none; filter: none; clip-path: inset(0 0 0 0); }
  36%  { opacity: 1; filter: brightness(1.7) saturate(1.6);
         clip-path: inset(0 0 0 0);
         transform: perspective(1200px) translate3d(calc(var(--nav-dir) * -2vw), 0, 0); }
  100% { opacity: 0;
         transform: perspective(1200px) rotateX(22deg)
                    translate3d(calc(var(--nav-dir) * -30vw), -34px, 0) scale(.88);
         filter: brightness(2.9) saturate(2.2) blur(9px);
         clip-path: inset(50% 0 50% 0); }
}

/* Enter, now directional. Overrides the v13 glitch-flip for sections only;
   the shared .ui-glitch-enter hook still serves the waypoint preset strip. */
.page-wrap > .section.ui-glitch-enter {
  animation: sec-enter .78s var(--ease-gta) both !important;
}
@keyframes sec-enter {
  0%   { opacity: 0;
         transform: perspective(1100px) rotateX(-58deg)
                    translate3d(calc(var(--nav-dir) * 34vw), 0, 0) scale(.90);
         clip-path: inset(50% 0 50% 0);
         filter: brightness(3.2) saturate(2.4) blur(7px); }
  12%  { opacity: 1;
         transform: perspective(1100px) rotateX(-19deg)
                    translate3d(calc(var(--nav-dir) * 11vw), 0, 0) scale(1.03);
         clip-path: inset(24% 0 28% 0);
         filter: brightness(2.1) saturate(2) blur(0); }
  22%  { transform: perspective(1100px) rotateX(12deg)
                    translate3d(calc(var(--nav-dir) * -6vw), -6px, 0);
         clip-path: inset(2% 0 52% 0);
         filter: brightness(1.6) saturate(1.6) hue-rotate(24deg); }
  32%  { transform: perspective(1200px) translate3d(calc(var(--nav-dir) * 1.4vw), 2px, 0);
         clip-path: inset(0 0 0 0);
         filter: brightness(1.3) saturate(1.25) hue-rotate(-12deg); }
  48%  { transform: translate3d(calc(var(--nav-dir) * -.5vw), 0, 0); filter: brightness(1.12); }
  66%  { transform: translate3d(calc(var(--nav-dir) * .2vw), 0, 0); }
  100% { opacity: 1; transform: none; clip-path: inset(0 0 0 0); filter: none; }
}

/* Chromatic aberration on the entrance. Two offset copies of the section,
   screened, for the front third of the flip only. */
.page-wrap > .section.ui-glitch-enter::before,
.page-wrap > .section.ui-glitch-enter::after {
  content: ""; position: absolute; inset: 0; z-index: 3;
  pointer-events: none; mix-blend-mode: screen;
  animation: rgb-split .34s var(--ease-gta) both;
}
.page-wrap > .section.ui-glitch-enter::before {
  background: linear-gradient(90deg, rgba(255,0,90,.30), transparent 42%);
}
.page-wrap > .section.ui-glitch-enter::after {
  background: linear-gradient(270deg, rgba(0,220,255,.30), transparent 42%);
  animation-direction: reverse;
}
@keyframes rgb-split {
  0%   { opacity: .95; transform: translate3d(calc(var(--nav-dir) * 14px), 0, 0); }
  38%  { opacity: .7;  transform: translate3d(calc(var(--nav-dir) * -7px), 0, 0); }
  70%  { opacity: .3;  transform: translate3d(calc(var(--nav-dir) * 3px), 0, 0); }
  100% { opacity: 0;   transform: none; }
}

/* Children arrive in sequence rather than as one slab. #wall-frame is excluded
   deliberately: #drillin is reparented inside it and needs a real fixed-
   position containing block, which a transformed ancestor would destroy. */
.page-wrap > .section.ui-glitch-enter > *:not(#wall-frame) {
  animation: child-in .44s var(--ease-out) both;
  animation-delay: calc(120ms + var(--i, 0) * 52ms);
}
@keyframes child-in {
  from { opacity: 0; filter: brightness(1.8);
         transform: translate3d(calc(var(--nav-dir) * 3.5vw), 16px, 0) scale(.98); }
  to   { opacity: 1; filter: none; transform: none; }
}
/* Camera tiles cascade individually — the grid is the payoff of that tab. */
#section-cameras.ui-glitch-enter .cam-card {
  animation: tile-in .5s var(--ease-out) both;
  animation-delay: calc(190ms + var(--idx, 0) * 58ms);
}
@keyframes tile-in {
  from { opacity: 0; transform: translate3d(0, 22px, 0) scale(.9) rotateX(-18deg);
         filter: brightness(2.4) blur(3px); }
  to   { opacity: 1; transform: none; filter: none; }
}

/* ── Shutter: the channel change ──────────────────────────────────────────── */
/* ── Playback preparation ───────────────────────────────────────────────────
   Eighteen seconds of unchanging black is indistinguishable from a crash. */
.pb-prepare { display: none; }
.playback-panel.pb-preparing .pb-prepare {
  display: grid; justify-items: center; align-content: center; gap: 12px;
  position: absolute; inset: 0; z-index: 26;
  background: rgba(3, 7, 14, .82);
  padding: 24px; text-align: center;
}
.pb-prepare strong {
  font-family: var(--font-title); font-weight: 400; font-size: 21px;
  letter-spacing: .06em; text-transform: uppercase; color: #fff;
}
.pb-prepare span {
  font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .1em;
  color: rgba(255,255,255,.7);
}
.pb-prepare-ring {
  width: 46px; height: 46px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--neon-cyan) 22%, transparent);
  border-top-color: var(--neon-cyan);
  animation: pb-spin 1s linear infinite;
}
@keyframes pb-spin { to { transform: rotate(360deg); } }
.pb-prepare-bar {
  --pct: 0%;
  position: relative; width: min(320px, 74vw); height: 4px; border-radius: 3px;
  background: rgba(255,255,255,.13); overflow: hidden;
}
.pb-prepare-bar > i {
  position: absolute; inset: 0 auto 0 0; width: var(--pct);
  background: linear-gradient(90deg, var(--neon-cyan), var(--neon-magenta));
  transition: width .3s var(--ease-out);
}
.pb-prepare-bar.indeterminate > i {
  width: 38%; animation: pb-indet 1.25s var(--ease-out) infinite;
}
@keyframes pb-indet {
  from { transform: translateX(-110%); } to { transform: translateX(300%); }
}
/* A thumb pinned hard left during preparation reads as broken, not busy. */
.pb-progress-idle { opacity: .35; pointer-events: none; }

.shutter {
  position: fixed; inset: 0; z-index: 92;
  pointer-events: none; opacity: 0;
  contain: strict;
}
.shutter.run { opacity: 1; }
.sh-bar {
  position: absolute; top: -5vh; bottom: -5vh; width: 58vw; left: -64vw;
  background: linear-gradient(90deg,
    transparent 0%,
    color-mix(in srgb, var(--neon-cyan) 22%, transparent) 34%,
    color-mix(in srgb, var(--neon-magenta) 44%, transparent) 74%,
    #fff 95%, rgba(255,255,255,0) 100%);
  box-shadow: 0 0 90px 10px color-mix(in srgb, var(--neon-cyan) 35%, transparent);
  transform: translateX(0) skewX(-9deg);
  mix-blend-mode: screen;
}
.shutter.run .sh-bar { animation: shutter-run .66s var(--ease-gta) both; }
@keyframes shutter-run {
  from { transform: translateX(0) skewX(-9deg); }
  to   { transform: translateX(210vw) skewX(-9deg); }
}
html[data-nav-dir="back"] .sh-bar { left: auto; right: -64vw; transform: translateX(0) skewX(9deg); }
html[data-nav-dir="back"] .shutter.run .sh-bar { animation-name: shutter-run-back; }
@keyframes shutter-run-back {
  from { transform: translateX(0) skewX(9deg); }
  to   { transform: translateX(-210vw) skewX(9deg); }
}
/* VHS head-switching noise at the bottom of the frame. A tape machine artifact,
   which is exactly the vocabulary this product should be speaking. */
.sh-tear {
  position: absolute; left: 0; right: 0; bottom: 0; height: 54px;
  opacity: 0; mix-blend-mode: screen;
  background:
    repeating-linear-gradient(90deg, rgba(255,255,255,.55) 0 2px, transparent 2px 6px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--neon-cyan) 60%, transparent) 0 1px, transparent 1px 9px);
}
.shutter.run .sh-tear { animation: head-switch .44s steps(1, end) both; }
/* A second, faster bar the other way. One bar is a wipe; two is a switch. */
.sh-bar2 {
  position: absolute; top: -5vh; bottom: -5vh; width: 40vw; right: -46vw;
  background: linear-gradient(270deg, transparent 0%,
    color-mix(in srgb, var(--neon-magenta) 26%, transparent) 40%, #fff 92%,
    rgba(255,255,255,0) 100%);
  transform: translateX(0) skewX(7deg);
  mix-blend-mode: screen; opacity: .8;
}
.shutter.run .sh-bar2 { animation: shutter-run-back .46s var(--ease-gta) .06s both; }
html[data-nav-dir="back"] .sh-bar2 { right: auto; left: -46vw; transform: translateX(0) skewX(-7deg); }
html[data-nav-dir="back"] .shutter.run .sh-bar2 { animation-name: shutter-run; }
@keyframes head-switch {
  0%   { opacity: .85; height: 30px; transform: translateX(0); }
  20%  { opacity: .6;  height: 14px; transform: translateX(-9px); }
  40%  { opacity: .9;  height: 42px; transform: translateX(7px); }
  60%  { opacity: .45; height: 20px; transform: translateX(-4px); }
  80%  { opacity: .7;  height: 9px;  transform: translateX(3px); }
  100% { opacity: 0;   height: 0; }
}

/* Nav tabs: one indicator that travels, instead of four that grow and shrink */
.section-nav { position: sticky; }
.nav-btn.active::after { box-shadow: 0 0 16px var(--neon-magenta), 0 0 34px color-mix(in srgb, var(--neon-cyan) 60%, transparent); }

/* ═══════════════════════════════════════════════════════════════════════════
   6. WALL LOCK
   The tube that flies from a camera tile to the wall bezel on drill-in.
   Positioned and animated by app.js; this is only its skin.
   ═══════════════════════════════════════════════════════════════════════════ */
.tube-flier {
  position: fixed; z-index: 91; pointer-events: none;
  border-radius: var(--tube-r);
  box-shadow:
    inset 0 0 0 var(--tube-w, 7px) color-mix(in srgb, var(--cc) 85%, transparent),
    inset 0 0 0 calc(var(--tube-w, 7px) + 1px) rgba(255,255,255,.35),
    0 0 34px color-mix(in srgb, var(--cc) 70%, transparent),
    0 0 90px color-mix(in srgb, var(--cc) 40%, transparent);
  will-change: transform, opacity;
}

/* ═══════════════════════════════════════════════════════════════════════════
   7. LOWER-POWER PATH
   Animated drop-shadow is a paint-bound effect. On phones the multi-layer
   bloom collapses to one static shadow and only transform-driven motion — the
   gas swirl — survives, which is the part that composites for free.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 700px) {
  :root { --tube-w: 5px; }
  .wall-wrap { --tube-w: 8px; }
  .cam-tube {
    animation: none !important;
    filter: drop-shadow(0 0 5px color-mix(in srgb, var(--cc) 70%, transparent))
            drop-shadow(0 0 16px color-mix(in srgb, var(--cc) 35%, transparent)) !important;
  }
  .cam-gas::before { filter: blur(4px); }
  .cam-haze { animation: none; }
}
@media (max-width: 520px) {
  .sh-bar { width: 74vw; }
  .sh-bar2 { width: 52vw; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   8. REDUCED MOTION
   The tube stays — it is state, not decoration. The gas stops moving.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .cam-tube, .cam-gas, .cam-gas::before, .cam-haze,
  .wall-wrap, .wall-wrap::after, .sh-bar, .sh-bar2, .sh-tear,
  .pb-prepare-ring, .pb-prepare-bar > i, .cam-card::before, .cam-tube,
  .page-wrap > .section.ui-glitch-enter::before,
  .page-wrap > .section.ui-glitch-enter::after,
  .page-wrap > .section.ui-exit,
  .page-wrap > .section.ui-glitch-enter,
  .page-wrap > .section.ui-glitch-enter > *,
  #section-cameras.ui-glitch-enter .cam-card { animation: none !important; }
  .cam-tube { filter: drop-shadow(0 0 4px color-mix(in srgb, var(--cc) 70%, transparent)) !important; }
  .cam-gas::before { transform: none; opacity: .5; }
  .page-wrap > .section.ui-exit { display: none !important; }
  .page-wrap > .section.ui-glitch-enter,
  .page-wrap > .section.ui-glitch-enter > * {
    opacity: 1 !important; transform: none !important;
    filter: none !important; clip-path: none !important;
  }
  .shutter { display: none !important; }
  .tube-flier { display: none !important; }
}
body:has(#reduce-motion:checked) .cam-tube,
body:has(#reduce-motion:checked) .cam-gas,
body:has(#reduce-motion:checked) .cam-gas::before,
body:has(#reduce-motion:checked) .cam-haze,
body:has(#reduce-motion:checked) .wall-wrap,
body:has(#reduce-motion:checked) .wall-wrap::after,
body:has(#reduce-motion:checked) .sh-bar,
body:has(#reduce-motion:checked) .sh-bar2,
body:has(#reduce-motion:checked) .pb-prepare-ring,
body:has(#reduce-motion:checked) .pb-prepare-bar > i,
body:has(#reduce-motion:checked) .cam-card::before,
body:has(#reduce-motion:checked) .cam-tube,
body:has(#reduce-motion:checked) .page-wrap > .section.ui-glitch-enter::before,
body:has(#reduce-motion:checked) .page-wrap > .section.ui-glitch-enter::after,
body:has(#reduce-motion:checked) .sh-tear { animation: none !important; }
body:has(#reduce-motion:checked) .page-wrap > .section.ui-glitch-enter::before,
body:has(#reduce-motion:checked) .page-wrap > .section.ui-glitch-enter::after { display: none !important; }
body:has(#reduce-motion:checked) .cam-tube {
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--cc) 70%, transparent)) !important;
}
body:has(#reduce-motion:checked) .shutter { display: none !important; }
body:has(#reduce-motion:checked) .page-wrap > .section.ui-exit { display: none !important; }

/* Sequential viewer, viewport fit, and reversible transition contract. */
html, body { width: 100%; min-height: 100dvh; max-width: 100%; overflow-x: clip; touch-action: manipulation; }
body { min-height: var(--app-vh, 100dvh); }
.app, .page-wrap { width: min(100%, var(--max-w)); max-width: 100%; }
#wall-frame:fullscreen video, #wall-frame:-webkit-full-screen video { object-fit: contain !important; }
#wall-frame:fullscreen, #wall-frame:-webkit-full-screen { width: 100vw; height: 100dvh; max-width: none; border-radius: 0; }
#wall-frame:fullscreen .drillin, #wall-frame:-webkit-full-screen .drillin { width: 100%; height: 100%; }
.drillin.camera-native-fullscreen .detail-video { touch-action: none; }

.ui-transition-open { animation: sentinel-layer-open .5s var(--ease-gta) both !important; }
.ui-transition-close { animation: sentinel-layer-close .5s var(--ease-gta) both !important; }
@keyframes sentinel-layer-open {
  0% { opacity: 0; transform: translate3d(0,-18px,0) scale(.94); clip-path: inset(0 0 92% 0); filter: brightness(2) saturate(1.6) hue-rotate(18deg); }
  35% { opacity: .9; transform: translate3d(-4px,3px,0) scale(1.018); clip-path: inset(0 0 24% 0); }
  100% { opacity: 1; transform: translate3d(0,0,0) scale(1); clip-path: inset(0); filter: none; }
}
@keyframes sentinel-layer-close {
  0% { opacity: 1; transform: translate3d(0,0,0) scale(1); clip-path: inset(0); filter: none; }
  65% { opacity: .9; transform: translate3d(-4px,3px,0) scale(1.018); clip-path: inset(0 0 24% 0); }
  100% { opacity: 0; transform: translate3d(0,-18px,0) scale(.94); clip-path: inset(0 0 92% 0); filter: brightness(2) saturate(1.6) hue-rotate(18deg); }
}

.motion-notification {
  position: fixed; top: max(8px, env(safe-area-inset-top)); left: 50%; z-index: 500;
  width: min(560px, calc(100vw - 24px)); transform: translateX(-50%);
  display: grid; grid-template-columns: 1fr 44px; overflow: hidden;
  border: 1px solid rgba(64,224,255,.65); border-radius: 10px;
  background: rgba(3,11,22,.96); box-shadow: 0 0 30px rgba(64,224,255,.28);
}
.motion-notification-main { min-width: 0; padding: 12px 15px; display: grid; gap: 5px; text-align: left; background: transparent; border: 0; }
.motion-notification-main strong { font-family: var(--font-display); color: var(--neon-cyan); font-size: clamp(16px,3vw,22px); }
.motion-notification-main span { font-family: var(--font-mono); color: var(--text); font-size: clamp(11px,2.2vw,14px); }
.motion-notification-close { border: 0; border-left: 1px solid rgba(64,224,255,.24); background: rgba(255,255,255,.03); color: var(--text); }

@media (max-width: 700px) {
  .topbar, .nav, .page-wrap { width: 100%; max-width: 100%; }
  .page-wrap { padding-left: max(8px, env(safe-area-inset-left)); padding-right: max(8px, env(safe-area-inset-right)); }
  .wall-wrap { width: 100%; max-height: calc(var(--app-vh, 100dvh) - 190px); }
  .stats-bar { overflow-x: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .ui-transition-open, .ui-transition-close { animation-duration: .12s !important; transform: none !important; clip-path: none !important; filter: none !important; }
}
body:has(#reduce-motion:checked) .ui-transition-open,
body:has(#reduce-motion:checked) .ui-transition-close { animation-duration: .12s !important; transform: none !important; clip-path: none !important; filter: none !important; }


/* ══ GAS-TUBE NUMERALS ═══════════════════════════════════════════════════════
   Bespoke numeric display. Each figure is treated as a real sealed glass tube:
   an unlit glass body always present, a lit gas core inside it, electrode caps
   at both ends, and a ballast strike when the value changes. Nothing here is a
   font effect — the glass is a box, the gas is an inset shadow, the strike is
   real ignition behaviour. Extends the wall tube language into typography. */

.tube-num {
  --gas: var(--accent, #67d6ff);
  --gas-dim: color-mix(in oklab, var(--gas) 55%, #06121d);
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: .04em;
  padding: .10em .34em;
  border-radius: .30em;
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: .01em;
  isolation: isolate;
  /* The unlit glass body: cool, slightly reflective, always visible. */
  background:
    linear-gradient(180deg, rgba(190,225,255,.09), rgba(120,170,210,.03) 42%, rgba(8,16,26,.28));
  box-shadow:
    inset 0 1px 0 rgba(210,235,255,.22),
    inset 0 -1px 0 rgba(0,0,0,.45),
    inset 0 0 .5em rgba(120,190,255,.10);
}

/* Electrode caps — the metal end seals every real tube has. */
.tube-num::before,
.tube-num::after {
  content: "";
  position: absolute;
  top: .18em;
  bottom: .18em;
  width: .11em;
  border-radius: .06em;
  background: linear-gradient(180deg, #9fb6c6, #4a5b68 45%, #2b3742);
  box-shadow: 0 0 .18em rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.35);
  z-index: 2;
}
.tube-num::before { left: .07em; }
.tube-num::after  { right: .07em; }

/* The gas itself. Colour is inherited so a single --gas swap re-tints the
   whole figure, cap glow included. */
.tube-num .gas {
  position: relative;
  z-index: 1;
  color: color-mix(in oklab, var(--gas) 88%, white);
  text-shadow:
    0 0 .10em color-mix(in oklab, var(--gas) 90%, white),
    0 0 .32em var(--gas),
    0 0 .85em color-mix(in oklab, var(--gas) 70%, transparent),
    0 0 1.6em color-mix(in oklab, var(--gas) 45%, transparent);
}

/* Gas fill glow pooling inside the glass, behind the figures. */
.tube-num .fill {
  position: absolute;
  inset: .12em .20em;
  border-radius: .22em;
  background: radial-gradient(120% 140% at 50% 120%,
              color-mix(in oklab, var(--gas) 26%, transparent),
              transparent 70%);
  z-index: 0;
  pointer-events: none;
}

/* Semantic gas colours — the tube tells you the reading's meaning. */
.tube-num.is-warn { --gas: #ffb03a; }
.tube-num.is-bad  { --gas: #ff5570; }
.tube-num.is-good { --gas: #58ffc4; }

/* Ballast strike: a real tube does not fade up, it stutters then catches.
   Fires only when the value actually changes. */
.tube-num.strike .gas { animation: tube-strike .42s steps(1, end) 1; }
.tube-num.strike .fill { animation: tube-fill .42s ease-out 1; }
@keyframes tube-strike {
  0%   { opacity: .25; }
  12%  { opacity: 1; }
  20%  { opacity: .30; }
  33%  { opacity: 1; }
  41%  { opacity: .55; }
  55%  { opacity: 1; }
  100% { opacity: 1; }
}
@keyframes tube-fill {
  0%   { opacity: .1; transform: scaleY(.6); }
  55%  { opacity: 1;  transform: scaleY(1.04); }
  100% { opacity: 1;  transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .tube-num.strike .gas,
  .tube-num.strike .fill { animation: none !important; }
}

/* ══ MOTION RIBBON ═══════════════════════════════════════════════════════════
   24 hours of activity as one density strip. Bright = busy. */
.ribbon-wrap { margin: 0 0 12px; padding: 10px 12px 8px; border-radius: 14px;
  background: rgba(9,17,28,.72); border: 1px solid rgba(103,214,255,.18); }
.ribbon-head { display: flex; justify-content: space-between; align-items: baseline;
  gap: 10px; margin-bottom: 7px; }
.ribbon-title { font-size: .72rem; letter-spacing: .14em; text-transform: uppercase;
  color: rgba(190,225,250,.72); }
.ribbon-read { font-size: .74rem; color: var(--accent, #67d6ff); font-variant-numeric: tabular-nums; }
.ribbon { display: block; width: 100%; height: 64px; border-radius: 8px;
  background: linear-gradient(180deg, rgba(6,12,20,.9), rgba(8,16,26,.75));
  cursor: crosshair; touch-action: none; }
.ribbon:focus-visible { outline: 2px solid var(--accent, #67d6ff); outline-offset: 2px; }
.ribbon-axis { display: flex; justify-content: space-between; margin-top: 5px;
  font-size: .64rem; letter-spacing: .08em; color: rgba(170,205,235,.5); }

/* ══ EVIDENCE SHEET ══════════════════════════════════════════════════════════ */
.eve-sheet { position: fixed; inset: 0; z-index: 90; display: grid; place-items: center;
  padding: 18px; background: rgba(3,7,13,.82); }
.eve-sheet.hidden { display: none; }
.eve-card { width: min(560px, 100%); border-radius: 16px; overflow: hidden;
  background: rgba(10,18,30,.97); border: 1px solid rgba(103,214,255,.26);
  box-shadow: 0 26px 70px rgba(0,0,0,.6); }
.eve-card img { display: block; width: 100%; height: auto; background: #05090f; }
.eve-meta { padding: 11px 14px; font-size: .78rem; line-height: 1.5;
  color: rgba(198,228,250,.86); font-variant-numeric: tabular-nums;
  border-top: 1px solid rgba(103,214,255,.14); }
.eve-actions { display: flex; gap: 8px; padding: 0 14px 14px; flex-wrap: wrap; }


/* ╔═══════════════════════════════════════════════════════════════════════════╗
   ║  v15 — LOCKED CHROME · FOCUS DIM · COMPOSITOR-ONLY NEON · STEP BROWSER    ║
   ║                                                                           ║
   ║  Appended as an override layer, same as v14d, so the earlier passes stay  ║
   ║  readable as their own diffs.                                             ║
   ║                                                                           ║
   ║    1. CHROME LOCK  the topbar and section nav were `position: sticky`     ║
   ║                    inside a scrolling body. On mobile the URL bar          ║
   ║                    collapse changes the containing block mid-scroll and    ║
   ║                    a sticky element visibly detaches and re-seats. Both    ║
   ║                    are now genuinely fixed.                                ║
   ║    2. FOCUS DIM    a camera open inside the wall is a modal state. Only    ║
   ║                    the wall and its controls stay at full luminance.       ║
   ║    3. NEON         the tube stops being a blurred rotating conic and       ║
   ║                    becomes an @property-animated gradient angle. No        ║
   ║                    filter, no oversized child, no blend mode.              ║
   ║    4. BROWSER      one stepped pane for day → AM/PM → hour → block.        ║
   ║    5. BUDGET       a coarse pointer runs a fraction of the animation       ║
   ║                    count, chosen so the identity survives intact.          ║
   ╚═══════════════════════════════════════════════════════════════════════════╝ */

/* ═══════════════════════════════════════════════════════════════════════════
   1. CHROME LOCK
   The two bars are one fixed stack. .page-wrap reserves their height so the
   first card is never underneath them, and scroll-padding keeps anchor jumps
   honest. Both read env(safe-area-inset-top) because the viewport is
   `viewport-fit=cover`.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --chrome-h: calc(var(--topbar-h) + var(--subnav-h) + 8px + var(--safe-top));
}

.topbar {
  position: fixed !important;
  top: 0; left: 0; right: 0;
  z-index: 60;
  width: min(100%, var(--max-w));
  margin: 0 auto;
  padding-top: var(--safe-top);
  height: calc(var(--topbar-h) + var(--safe-top));
  /* Opaque enough to read against a moving map without a live filter pass. */
  background: rgba(9, 16, 28, .93);
  border-bottom: 1px solid var(--line);
  /* Its own layer, so scrolling content underneath never repaints it. */
  will-change: transform;
  transform: translateZ(0);
}

.section-nav {
  position: fixed !important;
  top: calc(var(--topbar-h) + var(--safe-top)); left: 0; right: 0;
  z-index: 59;
  width: min(100%, var(--max-w));
  margin: 0 auto;
  border-radius: 0 0 var(--r-md) var(--r-md);
  background: rgba(9, 16, 28, .93);
  border-bottom: 1px solid var(--line);
  transform: translateZ(0);
}

/* The bars are out of flow now, so the page has to reserve their height. */
.page-wrap { padding-top: var(--chrome-h); }
.section { padding-top: 16px; }
html { scroll-padding-top: calc(var(--chrome-h) + 12px); }

/* The shimmer sweep on the topbar underline ran forever at 7s and repainted a
   fixed layer. It survives on desktop only, where it costs nothing. */
@media (pointer: coarse) {
  .topbar::after { animation: none; opacity: .5; }
}

/* Fullscreen surfaces own the whole screen; chrome must not float over them. */
body:has(#wall-frame:fullscreen) .topbar,
body:has(#wall-frame:fullscreen) .section-nav,
body:has(.drillin.camera-native-fullscreen:not(.hidden)) .topbar,
body:has(.drillin.camera-native-fullscreen:not(.hidden)) .section-nav {
  display: none !important;
}

/* ═══════════════════════════════════════════════════════════════════════════
   2. FOCUS DIM
   `body.cam-focus` is set by syncViewerControlHome() whenever a camera is open
   inside the wall and NOT in native fullscreen.
   Everything recedes except .wall-tube (the wall and its bezel) and #fs-dock
   (the controls that drive it). Implemented as opacity + a saturation drop on
   a small number of top-level regions — never a page-wide `filter`, which
   would create one enormous non-compositable layer and force the map canvas
   through it too.
   ═══════════════════════════════════════════════════════════════════════════ */
:root { --dim-o: .26; --dim-t: 420ms; }

body.cam-focus .topbar,
body.cam-focus .section-nav,
body.cam-focus .stat-bar,
body.cam-focus #section-system,
body.cam-focus #section-waypoints,
body.cam-focus .section-hdr {
  opacity: var(--dim-o);
  filter: saturate(.35);
  transition: opacity var(--dim-t) var(--ease-out), filter var(--dim-t) var(--ease-out);
  pointer-events: none;
}
/* Not display:none — the camera grid must keep its <video> elements alive and
   its layout height stable, or minimising the viewer would jump the scroll. */

body.cam-focus .wall-tube,
body.cam-focus #fs-dock {
  opacity: 1;
  filter: none;
  pointer-events: auto;
}

/* The wall gets a touch more presence while it is the only thing in play. */
body.cam-focus .wall-tube {
  --wall-tube-w: 12px;
  box-shadow:
    0 24px 60px -26px rgba(0, 0, 0, .9),
    0 0 46px -12px color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 62%, transparent);
  transition: box-shadow var(--dim-t) var(--ease-out);
}

/* The first cut of this used a fixed full-page scrim at z-index 1 to stop the
   dimmed page bleeding through the bezel. That put an extra always-on layer
   over the whole document AND, because #fs-dock lives in the same stacking
   context, it washed the dock out too — the one surface besides the wall that
   has to stay lit.

   Dimming the wall section's siblings directly is both cheaper (no extra
   layer) and correct: .wall-tube is already opaque, so nothing bleeds. */
body.cam-focus .page-wrap > .section > *:not(.wall-tube):not(#fs-dock) {
  opacity: var(--dim-o);
  filter: saturate(.35);
  transition: opacity var(--dim-t) var(--ease-out), filter var(--dim-t) var(--ease-out);
  pointer-events: none;
}

/* The dock is the other live surface. Explicitly lit, above everything in the
   page, and opaque so the dimmed grid behind it cannot show through. */
body.cam-focus #fs-dock {
  opacity: 1 !important;
  filter: none !important;
  pointer-events: auto !important;
  position: relative;
  z-index: 5;
  background: rgba(9, 17, 30, .98) !important;
  border-color: color-mix(in srgb, var(--neon-cyan) 34%, transparent) !important;
  box-shadow: 0 0 38px -10px var(--neon-cyan), 0 18px 44px -20px rgba(0,0,0,.9) !important;
}
body.cam-focus #fs-dock .fs-btn { opacity: 1 !important; }

@media (prefers-reduced-motion: reduce) {
  body.cam-focus .topbar, body.cam-focus .section-nav, body.cam-focus .stat-bar,
  body.cam-focus .wall-cameras, body.cam-focus #section-system,
  body.cam-focus #section-waypoints, body.cam-focus .section-hdr {
    transition: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   3. NEON — the flicker fix
   ───────────────────────────────────────────────────────────────────────────
   What was there: .cam-gas::before was a conic-gradient painted onto a
   200%x200% element, carrying `filter: blur(3.4px) saturate() contrast()`,
   rotated by an infinite transform animation, inside a two-layer
   mask-composite:exclude, ten times over. blur() is not compositable, so every
   frame Chromium re-rasterised ten oversized gradients on the main thread and
   then re-masked them. That is the flicker, the heat and the jank.

   What replaces it: the same look, produced by animating the conic gradient's
   own `from` angle via a registered custom property. --gas-turn is already
   registered above, so it interpolates. The softness that blur() was faking
   now comes from the gradient stops themselves, which is both cheaper and
   sharper — the falloff is exact instead of post-processed.

   Layer count per card drops from five (.cam-tube, ::after, .cam-gas, ::before,
   ::after, .cam-haze) to two.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The ring itself. One element, one background, one animated angle. */
.cam-gas {
  background:
    conic-gradient(from var(--gas-turn),
      color-mix(in srgb, var(--cc) 14%, transparent)   0deg,
      color-mix(in srgb, var(--cc) 34%, transparent)  26deg,
      color-mix(in srgb, var(--cc) 88%, #fff)         52deg,
      color-mix(in srgb, #fff        88%, var(--cc))  64deg,
      color-mix(in srgb, var(--cc) 88%, #fff)         76deg,
      color-mix(in srgb, var(--cc) 40%, transparent) 108deg,
      color-mix(in srgb, var(--cc) 16%, transparent) 152deg,
      color-mix(in srgb, var(--cc) 30%, transparent) 214deg,
      color-mix(in srgb, var(--cc) 78%, #fff)        252deg,
      color-mix(in srgb, #fff        70%, var(--cc)) 262deg,
      color-mix(in srgb, var(--cc) 26%, transparent) 302deg,
      color-mix(in srgb, var(--cc) 14%, transparent) 360deg);
  animation: gas-turn 9s linear infinite;
  animation-delay: var(--phase);
}
@keyframes gas-turn { to { --gas-turn: 360deg; } }

/* The oversized blurred child is gone. ::before now carries only the inner
   glass line — a single static box-shadow, no animation. */
.cam-gas::before {
  content: "";
  position: absolute; inset: 0;
  left: 0; top: 0; width: 100%; height: 100%;
  border-radius: inherit;
  background: none !important;
  filter: none !important;
  animation: none !important;
  will-change: auto;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .13);
}

/* The glass highlight. mix-blend-mode forced the card into its own blending
   group and disabled compositor fast paths on every one of the ten cards; a
   plain low-alpha gradient is indistinguishable at this size. */
.cam-gas::after {
  mix-blend-mode: normal;
  background: linear-gradient(148deg,
    rgba(255, 255, 255, .30) 0%,
    rgba(255, 255, 255, .07) 24%,
    transparent 48%,
    rgba(255, 255, 255, .11) 92%);
  box-shadow: inset 0 0 0 1.2px rgba(255, 255, 255, .38);
}

/* The bloom. drop-shadow() is a main-thread filter; it was static already but
   still forced a filter pass per card per frame. A box-shadow on a sibling
   layer whose opacity breathes is compositor-only and reads the same. */
.cam-tube {
  filter: none !important;
  border-radius: inherit;
  box-shadow:
    0 0 6px -1px color-mix(in srgb, var(--cc) 66%, transparent),
    0 0 20px -3px color-mix(in srgb, var(--cc) 40%, transparent),
    0 0 44px -14px color-mix(in srgb, var(--cc) 30%, transparent);
  animation: tube-bloom-opacity var(--pulse) ease-in-out infinite;
  animation-delay: var(--phase);
}

/* haze-drift animated background-position, which repaints the element every
   frame. The same wander is now a transform on a slightly oversized layer —
   free on the compositor. */
.cam-haze {
  mix-blend-mode: normal;
  opacity: .8;
  background:
    radial-gradient(78% 62% at 22% 8%,  color-mix(in srgb, var(--cc) 20%, transparent), transparent 72%),
    radial-gradient(66% 74% at 88% 96%, color-mix(in srgb, var(--cc) 15%, transparent), transparent 70%);
  animation: haze-float 14s ease-in-out infinite alternate;
  animation-delay: var(--phase);
}
@keyframes haze-float {
  from { transform: translate3d(0, 0, 0) scale(1); opacity: .62; }
  to   { transform: translate3d(1.6%, -1.2%, 0) scale(1.04); opacity: .92; }
}

/* Strike. Was a fourteen-stop drop-shadow keyframe; now opacity steps on a
   layer that already carries the shadow. Identical read, no filter. */
.cam-card:focus-visible .cam-tube,
.cam-card.cam-flare .cam-tube {
  animation: tube-strike-o .95s steps(1, end) 1 both,
             tube-bloom-opacity var(--pulse) ease-in-out infinite .95s;
}
@keyframes tube-strike-o {
  0%, 4%   { opacity: .12; }
  5%       { opacity: 1; }
  6%, 8%   { opacity: .10; }
  9%       { opacity: 1; }
  10%, 12% { opacity: .18; }
  13%, 100%{ opacity: 1; }
}
@media (hover: hover) and (pointer: fine) {
  .cam-card:hover .cam-tube {
    animation: tube-strike-o .95s steps(1, end) 1 both,
               tube-bloom-opacity var(--pulse) ease-in-out infinite .95s;
  }
  .cam-card:hover .cam-gas { animation-duration: 3.2s; }
}

/* Offline: gas drained, glass dead. Static, one long stutter. */
.cam-card.offline .cam-gas {
  background: linear-gradient(180deg, rgba(90, 104, 120, .28), rgba(58, 70, 84, .20)) !important;
  animation: dead-glass 13s steps(1, end) infinite !important;
}
.cam-card.offline .cam-tube {
  box-shadow: 0 0 4px -1px rgba(125, 142, 163, .3) !important;
  animation: none !important;
}
.cam-card.offline .cam-gas::before { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06); }

/* Degraded striations animated background-position on a 240% layer — repaint.
   Swap for an overlay whose opacity pulses. */
body:has(#sentinel-health-state.sentinel-degraded) .cam-card:not(.offline) .cam-gas,
body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas {
  animation: gas-turn 9s linear infinite, gas-fault 2.6s ease-in-out infinite !important;
  background-size: auto !important;
}
@keyframes gas-fault {
  0%, 100% { filter: none; }
  46%      { filter: hue-rotate(-28deg) saturate(1.5); }
}
/* Even that filter is too much on a phone; drop to opacity there. */
@media (pointer: coarse) {
  body:has(#sentinel-health-state.sentinel-degraded) .cam-card:not(.offline) .cam-gas,
  body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas {
    animation: gas-turn 14s linear infinite !important;
    filter: none !important;
  }
}

/* Reconnecting surge: opacity only, no second conic. */
body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas::before,
.wall-tube:has(#wall-state.reconnecting) ~ * .cam-gas::before {
  animation: gas-surge 1.4s ease-in-out infinite !important;
}

/* ── The wall's own ring: same treatment ────────────────────────────────── */
.wall-tube::before {
  width: 100%; height: 100%; aspect-ratio: auto;
  left: 0; top: 0; translate: none;
  border-radius: inherit;
  background: conic-gradient(from var(--gas-turn),
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 12%, transparent)   0deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 30%, transparent)  26deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 88%, #fff)         50deg,
    color-mix(in srgb, #fff 86%, var(--wall-hue, var(--neon-cyan)))         62deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 86%, #fff)         76deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 24%, transparent) 104deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 12%, transparent) 150deg,
    color-mix(in srgb, var(--neon-magenta) 24%, transparent)               236deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 44%, transparent) 292deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 12%, transparent) 340deg);
  animation: gas-turn 13s linear infinite !important;
}

/* wall-bloom animated a three-layer box-shadow. Static shadow, breathing
   opacity on the ring highlight instead. */
.wall-wrap {
  animation: none !important;
  box-shadow: 0 18px 44px -22px rgba(0, 0, 0, .8) !important;
}
.wall-wrap::after { animation: wall-gas-o var(--pulse) ease-in-out infinite !important; }
@keyframes wall-gas-o { 0%, 100% { opacity: .72; } 50% { opacity: 1; } }

/* ── Phone budget ──────────────────────────────────────────────────────────
   Ten simultaneous swirls are not perceptible as ten. Only the card you are
   touching, hovering or focusing animates its gas; the rest hold a static
   ring and breathe. This is the difference between ~30 running animations
   and ~11 on the camera tab. */
@media (pointer: coarse) {
  .cam-gas { animation-duration: 16s; }
  .cam-haze { animation: none; opacity: .6; }
  .cam-tube { animation-duration: calc(var(--pulse) * 1.6); }
  /* Ken Burns on ten thumbnails is a transform on ten promoted layers. One
     is charming, ten is a battery complaint. */
  .cam-thumb.has-frame { animation: none !important; background-size: cover; }
  /* The per-camera signal glyphs are nine more infinite loops for decoration
     that is 6px tall on a phone. */
  .cam-thumb::before, .cam-thumb::after { animation: none !important; opacity: .5; }
  .cam-card:active .cam-gas { animation-duration: 3.4s; }
  .cam-card:active { transform: scale(.985); }
}

/* Cards that are not on screen genuinely stop, rather than merely being
   invisible while their keyframes keep the compositor awake. */
.cam-grid > .cam-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 190px;
}

/* Dead custom-property transition on :root. Nothing has ever written
   --sweep-deg, and a transition on an *inherited* custom property at document
   root is a whole-document style-recalc trigger waiting to be armed. */
:root { transition: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   4. SECTION TRANSITIONS — keep the glitch, drop the blur on touch
   The flip is the best thing in this UI and it stays. But sec-enter and
   sec-exit both run `filter: blur()` across an entire section, which on a
   phone is a full-viewport non-compositable raster twice per tab change.
   Touch devices get the same choreography built from transform, opacity and
   clip-path only — all compositor properties.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (pointer: coarse) {
  @keyframes sec-enter {
    0%   { opacity: 0;
           transform: perspective(1100px) rotateX(-42deg)
                      translate3d(calc(var(--nav-dir) * 26vw), 0, 0) scale(.92);
           clip-path: inset(50% 0 50% 0); }
    14%  { opacity: 1;
           transform: perspective(1100px) rotateX(-14deg)
                      translate3d(calc(var(--nav-dir) * 8vw), 0, 0) scale(1.02);
           clip-path: inset(22% 0 26% 0); }
    26%  { transform: perspective(1100px) rotateX(9deg)
                      translate3d(calc(var(--nav-dir) * -5vw), -5px, 0);
           clip-path: inset(2% 0 46% 0); }
    38%  { transform: perspective(1200px) translate3d(calc(var(--nav-dir) * 1.2vw), 2px, 0);
           clip-path: inset(0 0 0 0); }
    56%  { transform: translate3d(calc(var(--nav-dir) * -.4vw), 0, 0); }
    100% { opacity: 1; transform: none; clip-path: inset(0 0 0 0); }
  }
  @keyframes sec-exit {
    0%   { opacity: 1; transform: none; clip-path: inset(0 0 0 0); }
    36%  { opacity: 1; clip-path: inset(0 0 0 0);
           transform: perspective(1200px) translate3d(calc(var(--nav-dir) * -2vw), 0, 0); }
    100% { opacity: 0;
           transform: perspective(1200px) rotateX(20deg)
                      translate3d(calc(var(--nav-dir) * -28vw), -30px, 0) scale(.9);
           clip-path: inset(50% 0 50% 0); }
  }
  @keyframes child-in {
    from { opacity: 0; transform: translate3d(calc(var(--nav-dir) * 3vw), 14px, 0) scale(.98); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes tile-in {
    from { opacity: 0; transform: translate3d(0, 18px, 0) scale(.92); }
    to   { opacity: 1; transform: none; }
  }
  /* Two screened pseudo-copies of the whole section for a 340ms aberration is
     the most expensive third of a second in the app. Desktop keeps it. */
  .page-wrap > .section.ui-glitch-enter::before,
  .page-wrap > .section.ui-glitch-enter::after { display: none; }
  /* The drill-in and stream-commit flips also carry brightness/saturate. */
  @keyframes drill-open {
    0%   { opacity: 0; transform: perspective(1600px) rotateX(8deg) translate3d(0,18px,0) scale(.91);
           clip-path: inset(48% 0 48% 0); }
    55%  { opacity: 1; }
    100% { opacity: 1; transform: perspective(1600px) rotateX(0) scale(1); clip-path: inset(0 0 0 0); }
  }
  @keyframes ui-glitch-expand {
    0%   { opacity: 0; transform: translate3d(0,14px,0) scale(.93); clip-path: inset(44% 0 44% 0); }
    33%  { transform: translate3d(3px,-2px,0) scale(.992); clip-path: inset(5% 0 7% 0); }
    100% { opacity: 1; transform: none; clip-path: inset(0); }
  }
  .shutter .sh-bar, .shutter .sh-bar2 { mix-blend-mode: normal; opacity: .8; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   5. PLAYBACK — the stepped browser
   The activity ribbon is gone. One pane, one breadcrumb, four steps.
   ═══════════════════════════════════════════════════════════════════════════ */
.ribbon-wrap, .ribbon, .ribbon-head, .ribbon-axis { display: none !important; }

.pb-browser {
  display: flex; flex-direction: column; gap: 10px;
}

.pb-nav {
  display: flex; align-items: center; gap: 8px;
  min-height: 34px;
}
.pb-step-back {
  flex-shrink: 0;
  width: 34px; height: 34px; border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--neon-cyan) 30%, transparent);
  background: rgba(10, 20, 34, .8);
  color: var(--neon-cyan); font-size: 16px; line-height: 1;
  cursor: pointer;
  display: grid; place-items: center;
  transition: background .14s, border-color .14s;
}
.pb-step-back:hover { background: rgba(64, 224, 255, .14); }
.pb-step-back.hidden { display: none !important; }

.pb-crumbs {
  display: flex; align-items: center; gap: 4px;
  flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
  min-width: 0;
}
.pb-crumbs::-webkit-scrollbar { display: none; }
.pb-crumb {
  flex-shrink: 0;
  border: none; background: none; cursor: pointer;
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--faint);
  padding: 5px 7px; border-radius: var(--r-xs);
  transition: color .14s, background .14s;
}
.pb-crumb:hover { color: var(--neon-cyan); background: rgba(64, 224, 255, .08); }
.pb-crumb.active {
  color: #fff; cursor: default;
  background: rgba(64, 224, 255, .13);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--neon-cyan) 28%, transparent);
}
.pb-crumb-sep { color: rgba(150, 183, 202, .3); font-size: 11px; flex-shrink: 0; }

.pb-timeline-wrap { display: block !important; }
.pb-timeline-label {
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--faint); margin-bottom: 9px;
}

/* One grid, retargeted per step. Days are dense chips; AM/PM are two big
   targets; hours and blocks are two-line tiles with a count or a size. */
.pb-timeline {
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  align-content: start;
}
.pb-timeline[data-step="period"] { grid-template-columns: repeat(2, 1fr); gap: 12px; }
.pb-timeline[data-step="hour"]   { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); }
.pb-timeline[data-step="block"]  { grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); }
.pb-timeline .pb-empty { grid-column: 1 / -1; }

.pb-day-chip, .pb-seg {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px;
  min-height: 48px; padding: 9px 10px;
  border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--neon-cyan) 20%, transparent);
  background: rgba(10, 19, 32, .78);
  color: var(--text); cursor: pointer;
  font-family: var(--font-mono); font-size: 12px;
  text-align: center; line-height: 1.2;
  transition: background .14s, border-color .14s, transform .12s;
}
.pb-day-chip strong, .pb-seg strong {
  font-family: var(--font-display); font-size: 13px; font-weight: 700;
  letter-spacing: .02em; color: #fff;
}
.pb-day-chip small, .pb-seg small {
  font-size: 9px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--faint);
}
.pb-day-chip:hover, .pb-seg:hover {
  background: rgba(64, 224, 255, .12);
  border-color: color-mix(in srgb, var(--neon-cyan) 46%, transparent);
}
.pb-day-chip:active, .pb-seg:active { transform: scale(.97); }
.pb-day-chip.active, .pb-seg.active {
  background: rgba(64, 224, 255, .17);
  border-color: var(--neon-cyan);
  box-shadow: 0 0 18px -6px var(--neon-cyan);
}

/* AM / PM want to be unmistakable, not another chip in a row of chips. */
.pb-period-chip { min-height: 76px; }
.pb-period-chip strong {
  font-family: var(--font-title); font-weight: 400;
  font-size: 30px; letter-spacing: .1em;
  background: linear-gradient(96deg, #fff 10%, var(--neon-cyan) 92%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.pb-hour-chip { min-height: 56px; }

/* ═══════════════════════════════════════════════════════════════════════════
   6. MOTION PROMOTION
   The old markup was bare checkboxes wrapped in labels with the type name as
   a text node — roughly 13px of tappable target on a phone, and no visual
   state at all when one was disabled. These are real controls now.
   ═══════════════════════════════════════════════════════════════════════════ */
#motion-subscriptions { display: flex; flex-direction: column; gap: 10px; }

.motion-master-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.motion-switch {
  display: inline-flex; align-items: center; gap: 10px;
  cursor: pointer; min-height: 44px;
  font-size: 13px; color: var(--text);
}
.motion-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.motion-switch > i {
  flex-shrink: 0;
  width: 44px; height: 26px; border-radius: 999px;
  background: rgba(255, 255, 255, .09);
  border: 1px solid var(--line);
  position: relative;
  transition: background .2s var(--ease-out), border-color .2s;
}
.motion-switch > i::after {
  content: ""; position: absolute;
  top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%;
  background: var(--st-denied);
  transition: transform .22s var(--ease-out), background .2s;
}
.motion-switch input:checked + i {
  background: rgba(64, 224, 255, .22);
  border-color: color-mix(in srgb, var(--neon-cyan) 55%, transparent);
}
.motion-switch input:checked + i::after {
  transform: translateX(18px);
  background: var(--neon-cyan);
  box-shadow: 0 0 12px var(--neon-cyan);
}
.motion-switch input:focus-visible + i {
  box-shadow: 0 0 0 3px rgba(64, 224, 255, .24);
}

.motion-save-state {
  font-family: var(--font-mono); font-size: 9.5px;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--faint); min-width: 58px; text-align: right;
  transition: color .2s;
}
.motion-save-state[data-state="saving"] { color: var(--st-degraded); }
.motion-save-state[data-state="saved"]  { color: var(--st-healthy); }
.motion-save-state[data-state="error"]  { color: var(--st-offline); }

.motion-dwell-default {
  display: flex; align-items: center; gap: 9px;
  font-size: 12.5px; color: var(--text-2);
}
.motion-dwell-default input {
  width: 74px; min-height: 40px; text-align: center;
  background: rgba(255, 255, 255, .05);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--text); font-family: var(--font-mono); font-size: 13px;
}
.motion-dwell-default small {
  font-family: var(--font-mono); font-size: 9.5px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--faint);
}

.motion-subscription-row {
  display: flex; flex-direction: column; gap: 8px;
  padding: 11px; border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .028);
  border: 1px solid var(--line);
}
.motion-cam-name {
  font-size: 12.5px; font-weight: 600; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.motion-types {
  display: grid; gap: 8px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.motion-type { display: flex; flex-direction: column; gap: 6px; min-width: 0; }

/* The whole tile is the target, not a 13px box. */
.motion-check {
  display: flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 10px;
  border-radius: var(--r-xs);
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, .03);
  cursor: pointer; user-select: none;
  transition: background .14s, border-color .14s;
}
.motion-check input { position: absolute; opacity: 0; width: 0; height: 0; }
.motion-check > i {
  flex-shrink: 0;
  width: 18px; height: 18px; border-radius: 5px;
  border: 1.5px solid var(--line-hi);
  background: rgba(255, 255, 255, .04);
  position: relative;
  transition: background .16s, border-color .16s;
}
.motion-check > i::after {
  content: ""; position: absolute;
  left: 5px; top: 1px; width: 5px; height: 10px;
  border: solid #04121b; border-width: 0 2.2px 2.2px 0;
  transform: rotate(42deg) scale(0);
  transition: transform .18s var(--ease-out);
}
.motion-check > span {
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.motion-check input:checked + i {
  background: var(--neon-cyan);
  border-color: var(--neon-cyan);
  box-shadow: 0 0 12px -2px var(--neon-cyan);
}
.motion-check input:checked + i::after { transform: rotate(42deg) scale(1); }
.motion-check input:checked ~ span { color: var(--text); }
.motion-check:has(input:checked) {
  background: rgba(64, 224, 255, .09);
  border-color: color-mix(in srgb, var(--neon-cyan) 34%, transparent);
}
.motion-check input:focus-visible + i { box-shadow: 0 0 0 3px rgba(64, 224, 255, .26); }

/* Denied means the server said no. It looks different from merely unchecked,
   which is the distinction the old build could not express — and getting that
   wrong is what made every box appear permanently dead. */
.motion-type.is-denied .motion-check {
  opacity: .4; cursor: not-allowed;
  border-style: dashed;
}
.motion-type.is-denied .motion-dwell-override { opacity: .4; pointer-events: none; }

.motion-dwell-override {
  min-height: 34px; width: 100%; text-align: center;
  background: rgba(255, 255, 255, .04);
  border: 1px solid var(--line); border-radius: var(--r-xs);
  color: var(--text-2); font-family: var(--font-mono); font-size: 11px;
}
.motion-dwell-override::placeholder { color: var(--faint); }

/* Master off greys the whole list without disabling it — you can still stage
   your subscriptions before switching promotion on. */
#motion-subscriptions[data-enabled="false"] .motion-subscription-row { opacity: .55; }

@media (max-width: 420px) {
  .motion-types { grid-template-columns: 1fr; }
  .motion-type { flex-direction: row; align-items: center; gap: 8px; }
  .motion-check { flex: 1; }
  .motion-dwell-override { width: 62px; flex-shrink: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   7. LAYOUT
   ═══════════════════════════════════════════════════════════════════════════ */

/* The stat bar was a horizontally scrolling row, so Bitrate and Recording were
   off-screen on every phone and nothing indicated they existed. Two rows of
   real cells instead. */
@media (max-width: 700px) {
  .stat-bar {
    display: grid; overflow: visible;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px 4px; padding: 12px 14px;
  }
  .sb-sep { display: none; }
  .sb-item { padding: 0; align-items: center; text-align: center; }
  .sb-num { font-size: 17px; }
  .sb-label { font-size: 8px; letter-spacing: .12em; }
}

/* Two 16:10 tiles side by side on a 390px screen gives each about 170px of
   width — too small to identify a scene. One 16:9 tile per row is legible,
   and vertical scrolling is free. */
@media (max-width: 520px) {
  .cam-grid { grid-template-columns: 1fr; gap: 10px; }
  .cam-card { aspect-ratio: 16 / 9; }
  .cam-info strong { font-size: 14px; }
  .cam-info small  { font-size: 10px; }
}
@media (min-width: 521px) and (max-width: 720px) {
  .cam-grid { grid-template-columns: repeat(2, 1fr); }
}

/* The system sub-pills scrolled away with the content. They are the tab bar
   for that section, so they stay put under the fixed chrome. */
.sys-pills {
  position: sticky;
  top: calc(var(--chrome-h) + 4px);
  z-index: 12;
  background: rgba(7, 13, 24, .94);
  border-radius: var(--r-sm);
  padding: 6px;
  margin-bottom: 12px;
}

/* dvh, so the drill-in and the sheets stop resizing every time the URL bar
   collapses. lvh fallback for anything without dvh support. */
.sheet-backdrop, .eve-sheet { min-height: 100vh; min-height: 100dvh; }
.drillin:not(.hidden) { max-height: 100vh; max-height: 100dvh; }

/* The dock is the primary control surface on a phone; keep it clear of the
   home indicator. */
@media (max-width: 820px) and (pointer: coarse) {
  body:has(#drillin:not(.hidden)) #fs-dock:not(.hidden) {
    padding-bottom: calc(15px + var(--safe-bottom));
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   8. REDUCED MOTION — everything added above, switched off
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .cam-gas, .cam-tube, .cam-haze, .wall-tube::before, .wall-wrap::after {
    animation: none !important;
  }
  .cam-gas { background: linear-gradient(180deg,
    color-mix(in srgb, var(--cc) 66%, transparent),
    color-mix(in srgb, var(--cc) 38%, transparent)) !important; }
  .wall-tube::before { background: color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 46%, transparent) !important; }
  .cam-tube { opacity: 1; }
  .cam-haze { opacity: .5; transform: none; }
}
body:has(#reduce-motion:checked) .cam-gas,
body:has(#reduce-motion:checked) .cam-tube,
body:has(#reduce-motion:checked) .cam-haze,
body:has(#reduce-motion:checked) .wall-tube::before,
body:has(#reduce-motion:checked) .wall-wrap::after { animation: none !important; }
body:has(#reduce-motion:checked) .cam-gas {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--cc) 66%, transparent),
    color-mix(in srgb, var(--cc) 38%, transparent)) !important;
}

/* An out-of-flow .ui-exit resolves `top: 0` against .page-wrap's PADDING box,
   which now begins above the fixed chrome. Without this the outgoing section
   jumped up by the full chrome height on the first frame of its exit — a
   visible snap before the animation had played a single step. */
.page-wrap > .section.ui-exit { top: var(--chrome-h) !important; }

/* The offline tube kept a `filter: drop-shadow(...) !important` from v14d,
   which reinstated a main-thread filter pass on every dead card — exactly the
   thing this pass removed everywhere else. Offline is a box-shadow now too. */
.cam-card.offline .cam-tube {
  filter: none !important;
  box-shadow: 0 0 4px -1px rgba(125, 142, 163, .30) !important;
}

/* ═══════════════════════════════════════════════════════════════════════════
   9. ANIMATION BUDGET — the census pass
   ───────────────────────────────────────────────────────────────────────────
   Counting what actually runs on a phone with nine cameras on screen found
   roughly forty simultaneous infinite animations, several of them on
   properties that cannot be composited. Fixed below in order of cost.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── crt-roll animated `top` ────────────────────────────────────────────────
   `top` is a layout property. A fixed, full-width, 34vh element animating it
   from -34vh to 100vh forever forced a layout on every single frame, for a
   decorative scanline. Same motion, transform-only, compositor-only. */
.page-wrap::after {
  top: 0 !important;
  transform: translate3d(0, -34vh, 0);
  animation: crt-roll-t 9s linear infinite !important;
  will-change: transform;
}
@keyframes crt-roll-t {
  0%   { transform: translate3d(0, -34vh, 0); }
  100% { transform: translate3d(0, 100vh, 0); }
}

/* ── Coarse-pointer budget ─────────────────────────────────────────────────
   Keep: the gas ring, the tube bloom, and the wall. Those three carry the
   identity. Everything else here is a second or third decorative loop on the
   same element and is not perceptible as a separate thing on a 390px screen.  */
@media (pointer: coarse) {
  /* .cam-card::before breathes the inner glass line while .cam-tube already
     breathes the bloom. Two opacity loops per card for one visual idea. */
  .cam-card::before { animation: none !important; opacity: .72 !important; }

  /* Nine slot badges pulsing behind a two-character number. */
  .cam-slot::after { animation: none !important; opacity: .62 !important; }

  /* The full-viewport scanline sweep and the header gradient sweep are both
     ambient. One is a promoted layer over the whole page, the other repaints
     a text gradient. Neither survives contact with a battery. */
  .page-wrap::after { animation: none !important; opacity: .28; }
  .section-hdr h2 { animation: none !important; background-position: 0 0 !important; }
  .stat-bar::after { animation: none !important; }

  /* Wordmark flicker: 5.2s steps() on a gradient-clipped text node inside the
     fixed topbar, which forces that layer to repaint. */
  .brand-text strong { animation: none !important; }
  .topbar::after { animation: none !important; opacity: .45; }

  /* Reconnecting fired gas-surge on ::before *in addition to* the ring swirl —
     two animations per card, on nine cards, at the exact moment the device is
     already re-establishing a stream. One opacity pulse on the parent reads
     the same and costs a ninth as much. */
  body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas::before,
  .wall-tube:has(#wall-state.reconnecting) ~ * .cam-gas::before {
    animation: none !important;
  }
  body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas,
  .wall-tube:has(#wall-state.reconnecting) ~ * .cam-gas {
    animation: gas-turn 14s linear infinite, gas-surge 2.4s ease-in-out infinite !important;
  }

  /* Offline cards: dead glass stutter is one loop per dead camera. A single
     shared 13s step animation is fine, the per-card ::before breath is not. */
  .cam-card.offline::before { animation: none !important; }
}

/* Firestorm is an alarm state and is allowed to be loud, but it should not
   also un-budget every card it touches on a phone. */
@media (pointer: coarse) {
  body:has(#btn-firestorm[aria-pressed="true"]) .cam-gas { animation-duration: 5s !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   10. NEON, SECOND PASS — correcting §3
   ───────────────────────────────────────────────────────────────────────────
   §3 replaced the rotating-child conic with a conic whose `from` angle is an
   animated registered custom property, on the theory that @property makes it
   interpolable and therefore cheap.

   Interpolable is not the same as compositable. A registered custom property
   animation runs on the MAIN THREAD: every frame it invalidates style on each
   element that reads the property and then repaints the gradient. Measured
   against the previous build over a 6s window with nine cards on screen, that
   cost +95% Style and +65% Paint — the exact opposite of the intent.

   `transform: rotate()` on a child, which is what v14d actually did, IS
   compositor work: the gradient rasterises once and the layer is then spun by
   the GPU for free.

   So the rotation goes back to a transform. The part of §3 that was correct
   and stays: `filter: blur()` is deleted. That was the genuinely expensive
   thing — a non-compositable filter re-rastering an oversized layer every
   frame — and the softness it faked is reproduced with feathered gradient
   stops, which is both cheaper and cleaner.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The ring body goes back to a static fill. No animation on this element. */
.cam-gas {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--cc) 60%, transparent),
    color-mix(in srgb, var(--cc) 32%, transparent) 45%,
    color-mix(in srgb, var(--cc) 72%, transparent)) !important;
  animation: none !important;
}

/* The gas: rasterised once, rotated on the compositor. Many closely spaced
   stops replace blur(3.4px) — the falloff is exact instead of post-processed. */
.cam-gas::before {
  content: "";
  position: absolute;
  left: -50%; top: -50%; width: 200%; height: 200%;
  border-radius: 0;
  box-shadow: none;
  background: conic-gradient(from 0deg,
    transparent                                       0deg,
    color-mix(in srgb, var(--cc) 10%, transparent)   10deg,
    color-mix(in srgb, var(--cc) 26%, transparent)   22deg,
    color-mix(in srgb, var(--cc) 48%, transparent)   32deg,
    color-mix(in srgb, var(--cc) 72%, #fff)          40deg,
    color-mix(in srgb, var(--cc) 90%, #fff)          48deg,
    color-mix(in srgb, #fff 82%, var(--cc))          56deg,
    color-mix(in srgb, var(--cc) 90%, #fff)          64deg,
    color-mix(in srgb, var(--cc) 70%, #fff)          72deg,
    color-mix(in srgb, var(--cc) 44%, transparent)   86deg,
    color-mix(in srgb, var(--cc) 22%, transparent)  104deg,
    color-mix(in srgb, var(--cc)  8%, transparent)  126deg,
    transparent                                     150deg,
    color-mix(in srgb, var(--cc) 10%, transparent)  196deg,
    color-mix(in srgb, var(--cc) 28%, transparent)  216deg,
    color-mix(in srgb, var(--cc) 56%, transparent)  232deg,
    color-mix(in srgb, var(--cc) 82%, #fff)         244deg,
    color-mix(in srgb, #fff 72%, var(--cc))         256deg,
    color-mix(in srgb, var(--cc) 80%, #fff)         268deg,
    color-mix(in srgb, var(--cc) 46%, transparent)  286deg,
    color-mix(in srgb, var(--cc) 18%, transparent)  310deg,
    transparent                                     340deg) !important;
  filter: none !important;
  animation: gas-swirl 9s linear infinite !important;
  animation-delay: var(--phase);
  will-change: transform;
}

/* The wall ring, same correction. */
.wall-tube::before {
  left: 50%; top: 50%; width: 190%; height: auto; aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 0;
  background: conic-gradient(from 0deg,
    transparent                                                                0deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 12%, transparent)    16deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 34%, transparent)    30deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 66%, #fff)           42deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 90%, #fff)           52deg,
    color-mix(in srgb, #fff 80%, var(--wall-hue, var(--neon-cyan)))           62deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 88%, #fff)           72deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 52%, transparent)    90deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 20%, transparent)   112deg,
    transparent                                                             150deg,
    color-mix(in srgb, var(--neon-magenta) 22%, transparent)                 236deg,
    color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 42%, transparent)   292deg,
    transparent                                                             340deg) !important;
  animation: wall-tube-swirl 13s linear infinite !important;
}

/* Degraded/reconnecting states drove `gas-turn`, which no longer exists. */
body:has(#sentinel-health-state.sentinel-degraded) .cam-card:not(.offline) .cam-gas,
body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas {
  animation: gas-fault 2.6s ease-in-out infinite !important;
}
body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas::before,
.wall-tube:has(#wall-state.reconnecting) ~ * .cam-gas::before {
  animation: gas-swirl 4.2s linear infinite !important;
}
.cam-card.offline .cam-gas {
  animation: dead-glass 13s steps(1, end) infinite !important;
}
.cam-card.offline .cam-gas::before {
  animation: gas-drain 1.25s cubic-bezier(.5,0,.75,0) both !important;
}
@media (hover: hover) and (pointer: fine) {
  .cam-card:hover .cam-gas { animation: none !important; }
  .cam-card:hover .cam-gas::before { animation-duration: 3.2s !important; }
}
@media (pointer: coarse) {
  .cam-gas::before { animation-duration: 16s !important; }
  .cam-card:active .cam-gas::before { animation-duration: 3.4s !important; }
  body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas,
  .wall-tube:has(#wall-state.reconnecting) ~ * .cam-gas {
    animation: none !important;
  }
  body:has(#sentinel-health-state.sentinel-recovering) .cam-card:not(.offline) .cam-gas::before,
  .wall-tube:has(#wall-state.reconnecting) ~ * .cam-gas::before {
    animation: gas-swirl 7s linear infinite !important;
  }
  body:has(#btn-firestorm[aria-pressed="true"]) .cam-gas::before { animation-duration: 5s !important; }
  .wall-tube::before { animation-duration: 18s !important; }
}
@media (prefers-reduced-motion: reduce) {
  .cam-gas::before, .wall-tube::before { animation: none !important; }
}
body:has(#reduce-motion:checked) .cam-gas::before,
body:has(#reduce-motion:checked) .wall-tube::before { animation: none !important; }

/* content-visibility: auto on the cards was the Layout regression. It makes
   the browser do containment bookkeeping as each card crosses the viewport,
   and with nine cards in a short scroll that is constant. The cards are always
   within a screen of the viewport anyway, so it bought nothing. */
.cam-grid > .cam-card { content-visibility: visible; contain-intrinsic-size: auto; }

/* ═══════════════════════════════════════════════════════════════════════════
   11. PLAYBACK PANEL STACKING
   The base layout puts .pb-header at 72px and .pb-browser at 126px — header
   above browser. The portrait phone query inverted that (browser 72, header
   118) and the two overlapped: the camera name printed straight through the
   hour tiles. Restore the base ordering at every size and give the browser the
   room the removed ribbon freed up.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (orientation: portrait) and (max-width: 720px) {
  .pb-header  { top: 58px !important; left: 14px !important; right: 14px !important; }
  .pb-browser { top: 108px !important; left: 12px !important; right: 12px !important;
                max-height: 44vh !important; }
}
@media (max-width: 520px) {
  .pb-browser { max-height: 42vh; }
}
.pb-cam-name { font-size: 19px; line-height: 1.1; }
.pb-header { gap: 2px; }

/* On a phone in windowed (non-fullscreen) drill-in, the playback panel lives
   inside a 16:9 wall frame — about 230px tall on a 412px screen. The header
   ("SOUTHGATE - PLAYBACK" / "Choose a recording") ate a fifth of that to say
   something the breadcrumb and the dock title already say. Drop it there and
   give the whole frame to the browser, which scrolls internally. */
@media (orientation: portrait) and (max-width: 720px) {
  .playback-panel .pb-header { display: none !important; }
  .playback-panel .pb-browser {
    top: 56px !important; bottom: 8px !important;
    max-height: none !important;
    height: auto !important;
  }
  .playback-panel .pb-nav { min-height: 30px; }
  .pb-timeline[data-step="period"] .pb-period-chip { min-height: 60px; }
  .pb-timeline[data-step="period"] .pb-period-chip strong { font-size: 24px; }
}

/* The browser floats over the video layer, so it needs to be opaque enough
   that a "reconnecting" state underneath does not print through the tiles. */
.pb-browser { background: rgba(5, 11, 20, .96) !important;
  border: 1px solid color-mix(in srgb, var(--neon-cyan) 16%, transparent); }

/* Camera grid density: keep the original 2-up. The single-column 16:9 pass in
   §7 traded density for legibility; density wins — nine tiles you can take in
   at a glance is the point of a wall. */
@media (max-width: 520px) {
  .cam-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  .cam-card { aspect-ratio: 16 / 10; }
  .cam-info strong { font-size: 11.5px; }
  .cam-info small  { font-size: 8.5px; }
  /* At this size the slot number and the kbd badge fight for the same corner. */
  .cam-kbd { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   12. GLITCH EVERYWHERE, AT EVERY WINDOW LEVEL
   ───────────────────────────────────────────────────────────────────────────
   Two separate bugs made the transitions feel absent:

   1. #shutter is a child of <body>. Native fullscreen promotes #wall-frame to
      the browser's top layer, and nothing outside that subtree paints - so
      every wipe ran correctly into a layer that was no longer on screen.
      app.js now reparents the shutter into whatever owns the screen; this
      block makes sure it fills that host whichever one it is.

   2. The v15 perf pass disabled the chromatic-aberration copies on coarse
      pointers, which removed the most legible part of the effect on exactly
      the device where the screen is smallest. They are back - rebuilt from
      transform and opacity only, so they cost what a moving div costs.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Works whether the shutter is parented to <body> or to the fullscreen node. */
.shutter {
  position: fixed !important;
  inset: 0 !important;
  z-index: 2147483000;
  pointer-events: none;
}
#wall-frame:fullscreen .shutter,
#wall-frame:-webkit-full-screen .shutter { position: absolute !important; }

/* A harder variant for the biggest jumps: fullscreen enter/exit, practice
   mode. Same bars, longer throw, one extra bounce. */
.shutter.run-hard .sh-bar  { animation: sh-wipe-hard .62s var(--ease-gta) both; }
.shutter.run-hard .sh-bar2 { animation: sh-wipe-hard .62s var(--ease-gta) .06s both; }
@keyframes sh-wipe-hard {
  0%   { transform: translate3d(-102%, 0, 0) skewX(-14deg); opacity: 0; }
  14%  { opacity: 1; }
  42%  { transform: translate3d(6%, 0, 0) skewX(-6deg); }
  56%  { transform: translate3d(-3%, 0, 0) skewX(-9deg); }
  100% { transform: translate3d(104%, 0, 0) skewX(-14deg); opacity: 0; }
}

/* Aberration copies, restored on touch. No filter, no blend - two offset
   clones on the compositor. */
@media (pointer: coarse) {
  .page-wrap > .section.ui-glitch-enter::before,
  .page-wrap > .section.ui-glitch-enter::after {
    display: block;
    mix-blend-mode: normal;
    opacity: .55;
  }
  .ui-glitch-enter::before { animation: ab-left  .34s steps(3, end) both; }
  .ui-glitch-enter::after  { animation: ab-right .34s steps(3, end) both; }
  @keyframes ab-left {
    0%   { transform: translate3d(-9px, 2px, 0); opacity: .8; }
    60%  { transform: translate3d(3px, -1px, 0); opacity: .4; }
    100% { transform: none; opacity: 0; }
  }
  @keyframes ab-right {
    0%   { transform: translate3d(9px, -2px, 0); opacity: .8; }
    60%  { transform: translate3d(-3px, 1px, 0); opacity: .4; }
    100% { transform: none; opacity: 0; }
  }
  /* The glitch is the point of the product's feel - it survives the budget. */
  .ui-glitch-enter { animation: ui-glitch-expand .44s var(--ease-gta) both !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   13. SAVE CLIP
   ═══════════════════════════════════════════════════════════════════════════ */
#pb-download {
  border-color: color-mix(in srgb, var(--neon-cyan) 42%, transparent);
  color: var(--neon-cyan);
  font-weight: 600;
}
#pb-download:hover:not(:disabled) { background: rgba(64, 224, 255, .16); }
#pb-download:disabled { opacity: .55; cursor: progress; }

/* ═══════════════════════════════════════════════════════════════════════════
   14. HELP — FAQ
   ═══════════════════════════════════════════════════════════════════════════ */
.faq { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.faq-item {
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .03);
  overflow: hidden;
}
.faq-item[open] {
  border-color: color-mix(in srgb, var(--neon-cyan) 32%, transparent);
  background: rgba(64, 224, 255, .06);
}
.faq-q {
  list-style: none; cursor: pointer;
  padding: 13px 14px; min-height: 44px;
  display: flex; align-items: center; gap: 10px;
  font-size: 13.5px; font-weight: 600; color: var(--text);
}
.faq-q::-webkit-details-marker { display: none; }
.faq-q::before {
  content: "+";
  flex-shrink: 0;
  width: 18px; text-align: center;
  font-family: var(--font-mono); font-size: 15px;
  color: var(--neon-cyan);
  transition: transform .18s var(--ease-out);
}
.faq-item[open] .faq-q::before { content: "\2212"; }
.faq-a {
  margin: 0; padding: 0 14px 14px 42px;
  font-size: 13px; line-height: 1.62; color: var(--text-2);
}
.faq-count { margin-top: 10px; text-align: right; opacity: .7; }

/* ═══════════════════════════════════════════════════════════════════════════
   15. GUIDED PRACTICE
   The simulated cell deliberately reuses .cam-tube / .cam-gas / .cam-haze so
   the visual language someone learns here is the language they will meet on a
   real camera.
   ═══════════════════════════════════════════════════════════════════════════ */
.tut-launch { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.tut-progress-note {
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--faint);
}

.tut-overlay {
  position: fixed; inset: 0; z-index: 2147482000;
  background: rgba(2, 5, 11, .96);
  display: grid; place-items: center;
  padding: calc(12px + env(safe-area-inset-top, 0px)) 12px calc(12px + env(safe-area-inset-bottom, 0px));
  overflow-y: auto;
}
.tut-overlay.hidden { display: none !important; }
body.tut-active { overflow: hidden; }

.tut-stage {
  width: min(100%, 620px);
  display: flex; flex-direction: column; gap: 12px;
}
.tut-topline { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.tut-step-count {
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: .16em; text-transform: uppercase; color: var(--faint);
}
.tut-quit {
  background: none; border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--muted); padding: 8px 12px; min-height: 40px;
  font-size: 12px; cursor: pointer;
}
.tut-quit:hover { color: var(--text); border-color: var(--line-hi); }

.tut-cell {
  --cc: var(--neon-cyan);
  --phase: 0s;
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--r-md);
  overflow: hidden;
  isolation: isolate;
  transition: box-shadow .3s var(--ease-out);
}
.tut-cell[data-hl="true"] {
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--neon-cyan) 60%, transparent),
              0 0 40px -8px var(--neon-cyan);
}
.tut-canvas { position: absolute; inset: 6px; width: calc(100% - 12px); height: calc(100% - 12px);
  border-radius: var(--r-sm); z-index: 0; }
.tut-cell .cam-haze, .tut-cell .cam-tube, .tut-cell .cam-gas { z-index: 1; }
.tut-cell-label {
  position: absolute; left: 14px; bottom: 12px; z-index: 2;
  display: flex; flex-direction: column; gap: 1px;
  text-shadow: 0 1px 8px rgba(0, 0, 0, .95);
}
.tut-cell-label strong { font-size: 13px; color: #fff; letter-spacing: .02em; }
.tut-cell-label small {
  font-family: var(--font-mono); font-size: 9px;
  letter-spacing: .16em; color: var(--neon-cyan);
}

.tut-controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: center; min-height: 4px; }
.tut-pad {
  display: grid; gap: 5px;
  grid-template-areas: ". up ." "left . right" ". down .";
  grid-template-columns: repeat(3, 52px);
}
.tut-zoom { display: flex; gap: 6px; }
.tut-btn {
  min-width: 52px; min-height: 52px;
  border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--neon-cyan) 26%, transparent);
  background: rgba(10, 19, 32, .9);
  color: var(--text); font-size: 17px; cursor: pointer;
  display: grid; place-items: center;
  transition: background .13s, transform .1s, box-shadow .2s;
}
.tut-btn:hover { background: rgba(64, 224, 255, .16); }
.tut-btn:active { transform: scale(.94); }
.tut-wide { min-width: 210px; font-size: 13px; padding: 0 16px; }
.tut-demo-done {
  border-color: var(--st-healthy);
  color: var(--st-healthy);
  font-family: var(--font-mono); font-size: 11px;
}
/* The step being taught pulses so there is exactly one obvious next action. */
[data-hl="true"].tut-pad .tut-btn,
[data-hl="true"].tut-zoom .tut-btn,
.tut-btn[data-hl="true"] {
  border-color: var(--neon-cyan);
  animation: tut-beckon 1.5s ease-in-out infinite;
}
@keyframes tut-beckon {
  0%, 100% { box-shadow: 0 0 0 0 rgba(64, 224, 255, .5); }
  50%      { box-shadow: 0 0 0 7px rgba(64, 224, 255, 0); }
}
.tut-pulse { display: flex; align-items: center; gap: 9px; font-size: 12px; color: var(--text-2); }
.tut-pulse[data-hl="true"] { color: var(--neon-cyan); }
.tut-pulse input { width: 150px; }

.tut-coach {
  border: 1px solid color-mix(in srgb, var(--neon-cyan) 26%, transparent);
  border-radius: var(--r-md);
  background: rgba(8, 15, 26, .96);
  padding: 15px 16px;
  display: flex; flex-direction: column; gap: 12px;
}
.tut-coach h4 {
  margin: 0; font-family: var(--font-display);
  font-size: 16px; font-weight: 700; color: #fff; letter-spacing: .01em;
}
.tut-coach p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--text-2); }
.tut-coach-actions { display: flex; gap: 9px; justify-content: flex-end; }
.tut-coach-actions .chip:disabled { opacity: .4; cursor: default; }

@media (max-width: 520px) {
  .tut-pad { grid-template-columns: repeat(3, 46px); }
  .tut-btn { min-width: 46px; min-height: 46px; }
  .tut-wide { min-width: 100%; }
  .tut-pulse input { width: 120px; }
  .tut-coach h4 { font-size: 15px; }
}
@media (prefers-reduced-motion: reduce) {
  .tut-btn, .tut-pad .tut-btn, .tut-zoom .tut-btn { animation: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   16. TIME OF DAY
   body[data-tod] is set from real sun times for the map's own coordinates.
   Each phase retints the whole app through the existing custom properties, so
   nothing needs to know about theming except this block.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── NIGHT ────────────────────────────────────────────────────────────────
   Dimmer overall, but MORE neon: the tubes are the only light source in the
   room now, so they carry more of the picture. Everything slows down. */
body[data-tod="night"] {
  --bg-0: #02040a;
  --bg-1: #05090f;
  --text:   #cfe0ee;
  --text-2: #93a8bd;
  --pulse: 3600ms;
  --dim-o: .2;
}
body[data-tod="night"] .cam-tube {
  box-shadow:
    0 0 8px -1px color-mix(in srgb, var(--cc) 78%, transparent),
    0 0 26px -3px color-mix(in srgb, var(--cc) 52%, transparent),
    0 0 60px -14px color-mix(in srgb, var(--cc) 42%, transparent);
}
body[data-tod="night"] .cam-gas::before { animation-duration: 13s !important; }
body[data-tod="night"] .wall-tube {
  box-shadow:
    0 22px 56px -24px rgba(0, 0, 0, .95),
    0 0 54px -12px color-mix(in srgb, var(--wall-hue, var(--neon-cyan)) 72%, transparent);
}
/* The map recedes so the tubes read as the brightest thing on screen. */
body[data-tod="night"] #bg-canvas { opacity: .58; filter: saturate(.8); }
body[data-tod="night"] .glass { background: rgba(4, 8, 16, .82); }

/* ── DUSK ─────────────────────────────────────────────────────────────────
   A one-shot fall into night: the whole page cools over eight seconds. */
body[data-tod="dusk"] { --pulse: 3200ms; }
.tod-fx[data-fx="dusk"] {
  background: linear-gradient(180deg,
    rgba(20, 8, 40, .34) 0%, rgba(8, 6, 22, .16) 46%, transparent 100%);
  animation: dusk-fall 9s var(--ease-out) both;
}
@keyframes dusk-fall {
  0%   { opacity: 0; }
  22%  { opacity: 1; }
  100% { opacity: 0; }
}

/* ── DAWN ─────────────────────────────────────────────────────────────────
   The one moment the app is warm. A low sun wash climbs from the horizon and
   a skein of birds crosses the plate. Fifteen seconds, once a day. */
.tod-fx {
  position: fixed; inset: 0;
  z-index: 3; pointer-events: none;
  overflow: hidden;
}
.tod-fx.hidden { display: none !important; }
.tod-fx[data-fx="sunrise"]::before {
  content: "";
  position: absolute; inset: 0;
  background:
    radial-gradient(120% 62% at 50% 104%, rgba(255, 176, 92, .40), transparent 62%),
    radial-gradient(90% 44% at 62% 100%, rgba(255, 226, 150, .30), transparent 58%),
    linear-gradient(180deg, transparent 34%, rgba(255, 150, 78, .14) 100%);
  animation: sunrise-wash 15s var(--ease-out) both;
}
@keyframes sunrise-wash {
  0%   { opacity: 0; transform: translate3d(0, 16%, 0); }
  18%  { opacity: 1; transform: translate3d(0, 0, 0); }
  74%  { opacity: .8; }
  100% { opacity: 0; transform: translate3d(0, -6%, 0); }
}
body[data-tod="dawn"] { --pulse: 2400ms; }

/* Birds. Each is a pair of wing strokes on a shared body, so a single element
   gives a silhouette that flaps: ::before and ::after are the two wings, and
   the wingbeat is a scaleY on each. Cheap, and it reads instantly. */
.tod-flock { position: absolute; inset: 0; }
.tod-bird {
  position: absolute;
  top: var(--top);
  left: -8%;
  width: calc(21px * var(--scale));
  height: calc(21px * var(--scale));
  animation: bird-cross var(--dur) linear var(--delay) both;
}
/* Warm and backlit, not silhouetted. A dark bird is the right choice against a
   bright dawn sky; this app's background is near-black, so a dark bird is an
   invisible bird. These read as catching the low sun instead. */
.tod-bird::before, .tod-bird::after {
  content: "";
  position: absolute; top: 50%; width: 52%; height: 2.4px;
  background: linear-gradient(90deg, rgba(255, 214, 158, .95), rgba(255, 168, 96, .75));
  box-shadow: 0 0 7px rgba(255, 186, 110, .75);
  border-radius: 2px;
  transform-origin: 100% 50%;
  animation: wingbeat calc(.42s + var(--i) * .04s) ease-in-out infinite;
}
.tod-bird::before { left: 0; }
.tod-bird::after  { right: 0; transform-origin: 0 50%; animation-delay: -.02s; }
@keyframes bird-cross {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  8%   { opacity: .9; }
  50%  { transform: translate3d(58vw, -5vh, 0); }
  88%  { opacity: .85; }
  100% { transform: translate3d(122vw, -12vh, 0); opacity: 0; }
}
@keyframes wingbeat {
  0%, 100% { transform: rotate(24deg) scaleY(1); }
  50%      { transform: rotate(-22deg) scaleY(1); }
}
.tod-bird::after { animation-name: wingbeat-r; }
@keyframes wingbeat-r {
  0%, 100% { transform: rotate(-24deg) scaleY(1); }
  50%      { transform: rotate(22deg) scaleY(1); }
}

/* ── MORNING ──────────────────────────────────────────────────────────────
   Ninety minutes of gold that decays back to the normal palette. Warm, but
   still legible - this is not a filter, it is a slightly different theme. */
body[data-tod="morning"] {
  --text:   #f2ece2;
  --text-2: #c9bcaa;
}
body[data-tod="morning"] #bg-canvas { filter: sepia(.16) saturate(1.12) brightness(1.06); }
body[data-tod="morning"] .glass {
  background: rgba(22, 18, 14, .70);
  border-color: rgba(255, 206, 150, .16);
}
body[data-tod="morning"] .section-hdr h2 {
  background: linear-gradient(96deg, #fff 8%, #ffcf92 62%, var(--neon-cyan) 100%);
  -webkit-background-clip: text; background-clip: text;
}
/* The morning tint fades out over the phase rather than snapping off at 90
   minutes, so nobody ever sees the moment it ends. */
body[data-tod="morning"]::after {
  content: "";
  position: fixed; inset: 0; z-index: 2; pointer-events: none;
  background: linear-gradient(184deg, rgba(255, 190, 110, .10), transparent 58%);
  animation: morning-decay 5400s linear both;
}
@keyframes morning-decay { from { opacity: 1; } to { opacity: 0; } }

/* ── PER-CAMERA DAY/NIGHT ─────────────────────────────────────────────────
   Detected per camera from the colour of its own thumbnail, because cameras
   flip minutes apart and some never flip at all. A card that has gone infrared
   gets a cool cast and a one-off flare when it changes over. */
.cam-card[data-ir="1"] .cam-thumb-frame { filter: grayscale(.72) brightness(1.06) contrast(1.08); }
.cam-card[data-ir="1"] .cam-info small::after {
  content: " \00B7 IR";
  color: color-mix(in srgb, var(--neon-cyan) 70%, #fff);
  letter-spacing: .1em;
}
.cam-card.cam-nightflip .cam-tube { animation: nightflip 2.4s var(--ease-out) 1; }
@keyframes nightflip {
  0%   { opacity: 1; }
  10%  { opacity: 1; filter: brightness(1.9); }
  26%  { opacity: .5; }
  40%  { opacity: 1; filter: brightness(1.5); }
  100% { opacity: 1; filter: none; }
}
.cam-card.cam-nightflip .cam-gas::before { animation-duration: 2.2s !important; }

/* ── Motion cooloff note ──────────────────────────────────────────────── */
.motion-cool-note { margin-top: -2px; opacity: .8; line-height: 1.5; }

@media (prefers-reduced-motion: reduce) {
  .tod-fx { display: none !important; }
  .cam-card.cam-nightflip .cam-tube { animation: none !important; }
  body[data-tod="morning"]::after { animation: none; opacity: .5; }
}
body:has(#reduce-motion:checked) .tod-fx { display: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   17. ARRIVAL EFFECTS
   The phase you last saw is persisted, so these fire on the change - including
   a change that happened entirely while the app was closed.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Brief sunrise: you slept through the real one. Same language, half the
   dwell, and the flock is already mid-sky rather than entering from the edge. */
.tod-fx[data-fx="sunrise-brief"]::before {
  content: "";
  position: absolute; inset: 0;
  background:
    radial-gradient(110% 56% at 50% 104%, rgba(255, 182, 100, .30), transparent 60%),
    linear-gradient(180deg, transparent 42%, rgba(255, 158, 84, .10) 100%);
  animation: sunrise-wash 7s var(--ease-out) both;
}
.tod-fx[data-fx="sunrise-brief"] .tod-bird {
  animation-duration: 5.5s !important;
  animation-delay: 0s !important;
}
.tod-fx[data-fx="sunrise-brief"] .tod-bird:nth-child(n+5) { display: none; }

/* Nightfall: reaching night without having watched dusk. A cool settle from
   the top rather than a sunset replay - the sun is already down, and pretending
   otherwise would be a lie the rest of the theme immediately contradicts. */
.tod-fx[data-fx="nightfall"]::before {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg, rgba(10, 14, 34, .52) 0%, rgba(6, 9, 20, .22) 54%, transparent 100%),
    radial-gradient(80% 40% at 50% -6%, rgba(90, 130, 200, .18), transparent 70%);
  animation: nightfall-settle 6.5s var(--ease-out) both;
}
@keyframes nightfall-settle {
  0%   { opacity: 0; transform: translate3d(0, -12%, 0); }
  20%  { opacity: 1; transform: translate3d(0, 0, 0); }
  70%  { opacity: .75; }
  100% { opacity: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   18. CAMERA GRID — ORIENTATION, NOT WIDTH
   ───────────────────────────────────────────────────────────────────────────
   The grid was governed by five overlapping width breakpoints that had grown
   over successive passes and disagreed with each other. The worst artifact was
   a cliff at exactly 960px: two 456x285 cards at 960, four 222x139 cards at
   961. One pixel of window width halved the card.

   Orientation is the honest variable. A portrait screen is tall and narrow and
   wants two columns; a landscape screen is wide and short and wants four. That
   holds on a phone, a tablet and a desktop without a single magic number, and
   the --max-w cap keeps four columns from ever becoming absurd on a 4K panel.

   This block deliberately overrides every earlier .cam-grid rule.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (orientation: portrait) {
  .cam-grid { grid-template-columns: repeat(2, 1fr) !important; }
}
@media (orientation: landscape) {
  .cam-grid { grid-template-columns: repeat(4, 1fr) !important; }
}

/* Gap tracks available room rather than orientation: a narrow phone needs the
   pixels back, everything else can breathe. */
.cam-grid { gap: 12px !important; }
@media (max-width: 560px) {
  .cam-grid { gap: 8px !important; }
}

/* Four columns on a phone held sideways gets tight below roughly 600px of
   width - an old SE in landscape would land near 124px per card. Three keeps
   the scene readable, and the only devices that hit this are ones where four
   was never going to work. */
@media (orientation: landscape) and (max-width: 600px) {
  .cam-grid { grid-template-columns: repeat(3, 1fr) !important; }
}

.cam-card { aspect-ratio: 16 / 10; }

/* ═══════════════════════════════════════════════════════════════════════════
   19. LANDSCAPE CHROME
   ───────────────────────────────────────────────────────────────────────────
   A phone in landscape has roughly 390px of height, and the fixed topbar plus
   section nav were taking about 120px of it - just under a third of the screen
   spent on chrome, on the orientation someone rotates INTO precisely because
   they want more wall.

   Compacted here rather than globally: portrait has the height to spare and
   the larger targets are worth keeping there.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (orientation: landscape) and (max-height: 560px) {
  :root { --topbar-h: 44px; --subnav-h: 36px; --chrome-h: calc(44px + 36px + 4px + var(--safe-top)); }
  .topbar { height: calc(44px + var(--safe-top)); }
  .brand-mark { width: 30px; height: 30px; }
  .brand-text strong { font-size: 15px; }
  .brand-text small  { font-size: 7.5px; letter-spacing: .24em; }
  .section-nav { top: calc(44px + var(--safe-top)); }
  .nav-btn { padding: 7px 14px; font-size: 12px; min-height: 36px; }
  .section { padding-top: 10px; }
  .section-hdr { margin-bottom: 8px; }
  .section-hdr h2 { font-size: 20px; }
  /* The stat bar is reference data, not something you rotate the phone to read.
     It collapses to a single scannable row here. */
  .stat-bar { padding: 7px 12px; gap: 6px; }
  .sb-num { font-size: 14px; }
  .sb-label { font-size: 7px; }
}

/* The bars were rgba(...,.93) and the camera grid scrolling beneath them was
   faintly legible through the chrome - text on text. Fixed chrome has to be
   opaque or it reads as a rendering fault. */
.topbar, .section-nav { background: #070d18; }
@supports (backdrop-filter: blur(6px)) {
  .topbar, .section-nav {
    background: rgba(7, 13, 24, .88);
    backdrop-filter: blur(14px) saturate(1.2);
    -webkit-backdrop-filter: blur(14px) saturate(1.2);
  }
}

/* ========================================================================== 
   20. TWO-VIDEO FULLSCREEN COMPOSITOR
   Video ancestors must settle without a transform. A retained perspective()
   creates a Chromium hardware-overlay containing block even when DOM bounds
   still report the full viewport.
   ========================================================================== */
.drillin:not(.hidden),
.detail-video,
.playback-panel:not(.hidden) {
  animation: none !important;
  transform: none !important;
  filter: none !important;
  perspective: none !important;
  clip-path: none !important;
}

/* The fullscreen panels live beneath .page-wrap. Clearing the panel itself is
   insufficient: perspective on any ancestor still creates the hardware-video
   containing block that shrinks Chromium's overlay into the upper-left. */
body:has(.drillin.camera-native-fullscreen) .page-wrap,
body:has(.playback-panel:not(.hidden)) .page-wrap,
body:has(.drillin.camera-native-fullscreen) .page-wrap > .section,
body:has(.playback-panel:not(.hidden)) .page-wrap > .section {
  animation: none !important;
  transform: none !important;
  filter: none !important;
  perspective: none !important;
  clip-path: none !important;
  transform-style: flat !important;
  will-change: auto !important;
}

.drillin.camera-native-fullscreen {
  position: fixed !important;
  inset: 0 !important;
  width: 100dvw !important;
  height: 100dvh !important;
  max-width: none !important;
  max-height: none !important;
  border-radius: 0 !important;
  isolation: isolate;
  overflow: hidden;
  z-index: 1000;
}

/* Playback is a viewer mode, not content squeezed into the page's 16:9 wall
   card. Keeping this surface fixed makes the portrait transport grid and
   landscape dock reachable regardless of the page's prior scroll offset. */
.drillin.playback-mode {
  position: fixed !important;
  inset: 0 !important;
  width: 100dvw !important;
  height: 100dvh !important;
  max-width: none !important;
  max-height: none !important;
  border-radius: 0 !important;
  isolation: isolate;
  overflow: hidden;
  z-index: 1000;
}

.detail-video { z-index: 0; }
.detail-video #viewer-video {
  z-index: 1;
  object-fit: contain;
  transform: none !important;
  filter: none !important;
  clip-path: none !important;
}

.viewer-transition {
  position: absolute;
  inset: 0;
  z-index: 34;
  pointer-events: none;
  background: #02060d;
  opacity: 0;
}
.viewer-transition.viewer-motion-first-open { animation: viewer-first-open var(--viewer-motion-ms, 520ms) var(--ease-gta) both; }
.viewer-transition.viewer-motion-source-change { animation: viewer-source-change var(--viewer-motion-ms, 360ms) var(--ease-gta) both; }
.viewer-transition.viewer-motion-stream-commit { animation: viewer-stream-commit var(--viewer-motion-ms, 420ms) var(--ease-gta) both; }
.viewer-transition.viewer-motion-close-ack { animation: viewer-close-ack var(--viewer-motion-ms, 300ms) var(--ease-gta) both; }
.viewer-transition.viewer-motion-wall-reveal { animation: viewer-wall-reveal var(--viewer-motion-ms, 300ms) var(--ease-gta) both; }
.viewer-transition.viewer-motion-rollback { animation: viewer-rollback var(--viewer-motion-ms, 300ms) var(--ease-gta) both; }
@keyframes viewer-first-open {
  0% { opacity: .98; clip-path: inset(0); background: #02060d; }
  34% { opacity: .94; clip-path: inset(42% 0 42% 0); background: #071421; }
  100% { opacity: 0; clip-path: inset(50% 0 50% 0); background: #02060d; }
}
@keyframes viewer-source-change {
  0% { opacity: 0; clip-path: inset(48% 0 48% 0); background: rgba(64,224,255,.7); }
  24% { opacity: .95; clip-path: inset(24% 0 24% 0); background: #02060d; }
  58% { opacity: .72; clip-path: inset(4% 0 54% 0); background: rgba(255,62,200,.5); }
  100% { opacity: 0; clip-path: inset(0 0 100% 0); background: #02060d; }
}
@keyframes viewer-stream-commit {
  0% { opacity: .96; clip-path: inset(0); background: #02060d; }
  38% { opacity: .7; clip-path: inset(0 0 46% 0); background: #071421; }
  100% { opacity: 0; clip-path: inset(0 0 100% 0); background: #02060d; }
}
@keyframes viewer-close-ack {
  0% { opacity: 0; clip-path: inset(50% 0 50% 0); }
  42% { opacity: .82; clip-path: inset(44% 0 44% 0); }
  100% { opacity: 0; clip-path: inset(50% 0 50% 0); }
}
@keyframes viewer-wall-reveal {
  0% { opacity: 0; clip-path: inset(0 100% 0 0); }
  44% { opacity: .98; clip-path: inset(0); }
  58% { opacity: .98; clip-path: inset(0); }
  100% { opacity: 0; clip-path: inset(0 0 0 100%); }
}
@keyframes viewer-rollback {
  0% { opacity: .9; clip-path: inset(0); }
  100% { opacity: 0; clip-path: inset(0 100% 0 0); }
}

#crop-canvas.crop-reveal-out { animation: crop-reveal-out var(--crop-reveal-ms, 420ms) var(--ease-gta) both; }
#crop-canvas.crop-presentation-commit { animation: crop-presentation-commit var(--viewer-motion-ms, 420ms) var(--ease-gta) both; }
@keyframes crop-reveal-out {
  0% { opacity: 1; clip-path: inset(0); }
  100% { opacity: 0; clip-path: inset(0 0 100% 0); }
}
@keyframes crop-presentation-commit {
  0% { opacity: .7; }
  100% { opacity: 1; }
}

/* Hardware video is the floor; every status and interactive surface is above
   it. These layers deliberately do not depend on DOM order. */
#detail-state { z-index: 35; }
.playback-panel { z-index: 40; }
.ptz-overlay { z-index: 45; }
.preset-strip, .waypoint-save-sheet { z-index: 50; }
.fs-topbar, .fs-btn, .fs-hud, .fs-zoom { z-index: 60; }
.pb-header, .pb-browser, .pb-player, .pb-prepare, .pb-back-top, .pb-times-toggle, .pb-close { z-index: 65; }

/* Playback navigation is a sibling of the content panel, not one of its
   children. This rail is the guaranteed touch layer: later browser/player/
   preparation nodes cannot win on DOM order, and the safe-area inset is
   applied once at the rail rather than independently to overlapping buttons. */
.drillin.playback-mode .fs-topbar {
  z-index: 90 !important;
  min-height: calc(64px + env(safe-area-inset-top, 0px));
  padding:
    calc(8px + env(safe-area-inset-top, 0px))
    calc(10px + env(safe-area-inset-right, 0px))
    8px
    calc(10px + env(safe-area-inset-left, 0px));
  justify-content: flex-start;
  background: linear-gradient(180deg, rgba(2, 6, 13, .98) 0%, rgba(2, 6, 13, .86) 72%, transparent 100%);
}
.drillin.playback-mode .fs-identity,
.drillin.playback-mode .fs-exit,
.drillin.playback-mode .fs-camera-fullscreen {
  display: none !important;
}
.drillin.playback-mode #pb-back-top,
.drillin.playback-mode #pb-times-toggle,
.drillin.playback-mode #pb-close {
  position: static !important;
  inset: auto !important;
  z-index: 95 !important;
  flex: 0 0 auto;
  min-width: 88px;
  min-height: 50px;
  height: 50px;
  padding: 0 16px;
  pointer-events: auto !important;
  touch-action: manipulation;
  isolation: isolate;
}
.drillin.playback-mode #pb-back-top {
  order: -2;
  border-color: rgba(64, 224, 255, .52);
  background: rgba(5, 16, 28, .98);
}
.drillin.playback-mode #pb-times-toggle { order: -1; }
.drillin.playback-mode #pb-close {
  order: 5;
  margin-left: auto;
  border-radius: var(--r-md);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.drillin.playback-mode .playback-panel { z-index: 40; }

/* A motion alert remains visible during playback, but it starts below the
   navigation rail instead of intercepting its tap targets. */
body:has(.drillin.playback-mode) .motion-notification {
  top: calc(70px + env(safe-area-inset-top, 0px));
}

/* The shared decoder remains the hardware floor. The crop canvas is activated
   only for proven handoff coverage or presentation transforms. */
#pb-video, .wall-warm-video { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  .viewer-transition, #crop-canvas { animation-duration: .12s !important; }
}
body:has(#reduce-motion:checked) .viewer-transition,
body:has(#reduce-motion:checked) #crop-canvas { animation-duration: .12s !important; }

/* Portrait live: identity at the safe top, video unobstructed through the
   middle, utility tray along the bottom, PTZ in its own central band. */
@media (orientation: portrait) {
  .drillin.camera-native-fullscreen .ptz-zin { bottom: 124px; }
  .drillin.camera-native-fullscreen .ptz-zout { bottom: 64px; }
}

@media (orientation: portrait) and (max-width: 720px) {
  .drillin.camera-native-fullscreen .fs-topbar {
    top: 0;
    height: calc(58px + env(safe-area-inset-top, 0px));
    padding: calc(8px + env(safe-area-inset-top, 0px)) 10px 6px;
    align-items: center;
  }
  .drillin.camera-native-fullscreen .fs-btn:not(.fs-btn-right) {
    top: auto !important;
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    width: 48px;
    height: 48px;
  }
  .drillin.camera-native-fullscreen #fs-audio { left: 6px; }
  .drillin.camera-native-fullscreen #fs-talk { left: 57px; }
  .drillin.camera-native-fullscreen #fs-snapshot { left: 108px; }
  .drillin.camera-native-fullscreen #fs-playback { left: 159px; }
  .drillin.camera-native-fullscreen #fs-pip { left: 210px; }
  .drillin.camera-native-fullscreen #fs-waypoints-toggle { left: 261px; }
  .drillin.camera-native-fullscreen #fs-hud-toggle {
    top: auto !important;
    left: 210px;
    right: auto;
    bottom: calc(62px + env(safe-area-inset-bottom, 0px));
    width: 48px;
    height: 48px;
  }
  .drillin.camera-native-fullscreen #fs-orientation-lock {
    top: auto !important;
    left: 261px;
    right: auto;
    bottom: calc(62px + env(safe-area-inset-bottom, 0px));
    width: 48px;
    height: 48px;
  }
  .drillin.camera-native-fullscreen .ptz-overlay {
    top: calc(62px + env(safe-area-inset-top, 0px));
    bottom: calc(118px + env(safe-area-inset-bottom, 0px));
  }
  .drillin.camera-native-fullscreen .ptz-btn { width: 54px; height: 54px; }
  .drillin.camera-native-fullscreen .ptz-up { top: 8%; }
  .drillin.camera-native-fullscreen .ptz-down { bottom: 70px; }
  .drillin.camera-native-fullscreen .ptz-left { left: 4%; }
  .drillin.camera-native-fullscreen .ptz-right { right: 4%; }
  .drillin.camera-native-fullscreen .ptz-zin { right: 4%; bottom: 124px; }
  .drillin.camera-native-fullscreen .ptz-zout { right: 4%; bottom: 64px; }
  .drillin.camera-native-fullscreen .ptz-duration {
    left: 50%;
    right: auto;
    bottom: 6px;
    transform: translateX(-50%);
    max-width: min(210px, 54vw);
  }

  .drillin.playback-mode .pb-header {
    top: calc(70px + env(safe-area-inset-top, 0px));
    left: 14px;
    right: 14px;
  }
  .drillin.playback-mode .pb-browser {
    top: calc(120px + env(safe-area-inset-top, 0px)) !important;
    left: 10px;
    right: 10px;
    max-height: calc(100dvh - 190px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  }
  .drillin.playback-mode #pb-progress {
    left: 12px;
    right: 12px;
    /* The embedded portrait wall frame can be only ~190px tall. Anchoring the
       scrubber 174px from its bottom placed it directly across the Back rail.
       Keep it below navigation and immediately above the transport dock. */
    bottom: calc(104px + env(safe-area-inset-bottom, 0px));
    width: auto;
    transform: none;
  }
  .drillin.playback-mode .pb-player-actions {
    position: absolute;
    left: 8px;
    right: 8px;
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    display: grid;
    grid-template-columns: repeat(5, minmax(48px, 1fr));
    gap: 6px;
    pointer-events: auto;
  }
  .drillin.playback-mode .pb-ctrl {
    position: static;
    width: 100%;
    min-width: 48px;
    height: 54px;
    min-height: 48px;
    transform: none !important;
  }
  .drillin.playback-mode #pb-prepare-mp4,
  .drillin.playback-mode #pb-download {
    grid-column: 1 / -1;
    min-height: 48px;
  }
}

@media (orientation: portrait) and (max-width: 360px) {
  .drillin.camera-native-fullscreen .ptz-left,
  .drillin.camera-native-fullscreen .ptz-right,
  .drillin.camera-native-fullscreen .ptz-left:active,
  .drillin.camera-native-fullscreen .ptz-right:active { top: 42%; }
}

/* Landscape live: utilities occupy fixed side rails. The bottom centre stays
   available for pulse duration and playback transport. */
@media (orientation: landscape) and (max-height: 560px) {
  .drillin.camera-native-fullscreen .fs-topbar {
    top: 0;
    height: calc(54px + env(safe-area-inset-top, 0px));
    padding: calc(6px + env(safe-area-inset-top, 0px)) calc(8px + env(safe-area-inset-right, 0px)) 4px calc(8px + env(safe-area-inset-left, 0px));
  }
  .drillin.camera-native-fullscreen .fs-btn:not(.fs-btn-right) {
    left: calc(8px + env(safe-area-inset-left, 0px));
    width: 48px;
    height: 48px;
  }
  .drillin.camera-native-fullscreen #fs-audio { top: calc(60px + env(safe-area-inset-top, 0px)); }
  .drillin.camera-native-fullscreen #fs-talk { top: calc(112px + env(safe-area-inset-top, 0px)); }
  .drillin.camera-native-fullscreen #fs-snapshot { top: calc(164px + env(safe-area-inset-top, 0px)); }
  .drillin.camera-native-fullscreen #fs-playback { top: calc(216px + env(safe-area-inset-top, 0px)); }
  .drillin.camera-native-fullscreen #fs-pip { top: calc(268px + env(safe-area-inset-top, 0px)); }
  .drillin.camera-native-fullscreen #fs-waypoints-toggle { top: calc(320px + env(safe-area-inset-top, 0px)); }
  .drillin.camera-native-fullscreen #fs-hud-toggle,
  .drillin.camera-native-fullscreen #fs-orientation-lock {
    right: calc(8px + env(safe-area-inset-right, 0px));
    width: 48px;
    height: 48px;
  }
  .drillin.camera-native-fullscreen #fs-hud-toggle { top: calc(60px + env(safe-area-inset-top, 0px)); }
  .drillin.camera-native-fullscreen #fs-orientation-lock { top: calc(112px + env(safe-area-inset-top, 0px)); }
  .drillin.camera-native-fullscreen .ptz-overlay {
    top: calc(54px + env(safe-area-inset-top, 0px));
    bottom: calc(62px + env(safe-area-inset-bottom, 0px));
    left: calc(58px + env(safe-area-inset-left, 0px));
    right: calc(58px + env(safe-area-inset-right, 0px));
  }
  .drillin.camera-native-fullscreen .ptz-btn { width: 50px; height: 50px; }
  .drillin.camera-native-fullscreen .ptz-down { bottom: 66px; }
  .drillin.camera-native-fullscreen .ptz-zin { bottom: 58px; right: 2%; }
  .drillin.camera-native-fullscreen .ptz-zout { bottom: 2px; right: 2%; }
  .drillin.camera-native-fullscreen .ptz-duration {
    left: 50%;
    right: auto;
    bottom: 2px;
    transform: translateX(-50%);
  }

  .drillin.playback-mode .pb-header {
    top: calc(66px + env(safe-area-inset-top, 0px));
    left: calc(70px + env(safe-area-inset-left, 0px));
    right: calc(70px + env(safe-area-inset-right, 0px));
  }
  .drillin.playback-mode .pb-browser {
    top: calc(106px + env(safe-area-inset-top, 0px)) !important;
    left: calc(64px + env(safe-area-inset-left, 0px));
    right: calc(64px + env(safe-area-inset-right, 0px));
    max-height: calc(100dvh - 112px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  }
  .drillin.playback-mode .pb-prev {
    left: calc(12px + env(safe-area-inset-left, 0px));
    top: 50%;
    bottom: auto;
  }
  .drillin.playback-mode .pb-next {
    right: calc(12px + env(safe-area-inset-right, 0px));
    top: 50%;
    bottom: auto;
  }
  .drillin.playback-mode #pb-progress {
    bottom: calc(72px + env(safe-area-inset-bottom, 0px));
    width: min(520px, 62vw);
  }
  .drillin.playback-mode .pb-toggle { bottom: calc(8px + env(safe-area-inset-bottom, 0px)); width: 58px; height: 58px; }
  .drillin.playback-mode .pb-seek-back { bottom: calc(10px + env(safe-area-inset-bottom, 0px)); left: calc(50% - 116px); width: 54px; height: 54px; }
  .drillin.playback-mode .pb-seek-forward { bottom: calc(10px + env(safe-area-inset-bottom, 0px)); left: calc(50% + 62px); width: 54px; height: 54px; }
}

@media (prefers-reduced-motion: reduce) {
  .viewer-transition { animation: none !important; opacity: 0 !important; }
}

/* Motion incident history */
.nav-badge { min-width: 18px; height: 18px; padding: 0 5px; margin-left: 5px; border-radius: 9px; background: var(--neon-magenta); color: #07101a; font: 700 10px/18px var(--font-mono); }
.motion-event-filters { display: grid; grid-template-columns: repeat(4, minmax(130px, 1fr)) auto; gap: 10px; padding: 14px; margin-bottom: 14px; align-items: end; }
.motion-event-filters label { display: grid; gap: 5px; color: var(--faint); font: 10px var(--font-mono); text-transform: uppercase; letter-spacing: .1em; }
.motion-event-filters select, .motion-event-filters input { min-height: 42px; width: 100%; padding: 8px 10px; color: var(--text); background: rgba(3,10,18,.86); border: 1px solid var(--line); border-radius: var(--r-sm); color-scheme: dark; }
.motion-events-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(285px, 1fr)); gap: 12px; }
.motion-event-card { overflow: hidden; padding: 0; border: 1px solid var(--line); border-radius: var(--r-md); background: rgba(8,17,29,.78); color: var(--text); text-align: left; }
.motion-event-thumb { display: grid; place-items: center; width: 100%; aspect-ratio: 16/9; padding: 0; border: 0; border-radius: 0; background: #02050a; color: var(--faint); font: 11px var(--font-mono); overflow: hidden; cursor: pointer; }
.motion-event-thumb:disabled { opacity: 1; cursor: default; }
.motion-event-thumb.preview-error { padding: 12px; color: var(--warn); }
.motion-event-thumb img { width: 100%; height: 100%; object-fit: cover; }
.motion-event-body { display: grid; gap: 6px; padding: 12px; }
.motion-event-title { display: flex; justify-content: space-between; gap: 10px; font-weight: 700; }
.motion-event-type { color: var(--neon-cyan); text-transform: uppercase; font: 10px var(--font-mono); letter-spacing: .1em; }
.motion-event-meta { color: var(--muted); font: 11px var(--font-mono); }
.motion-event-playback { justify-self: start; min-height: 36px; margin-top: 4px; padding: 7px 11px; }
.motion-event-playback:disabled { opacity: .55; cursor: wait; }
.motion-events-more { display: block; margin: 16px auto; min-height: 44px; }
@media (max-width: 720px) {
  .motion-event-filters { grid-template-columns: 1fr 1fr; }
  .motion-event-filters .chip { grid-column: 1 / -1; min-height: 44px; }
  .motion-events-list { grid-template-columns: 1fr; }
}

.pb-event-tools {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 8px;
  align-items: center;
  margin: 8px 0 6px;
}
.pb-event-readout {
  min-width: 0;
  text-align: center;
  font-size: 0.75rem;
  opacity: 0.82;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pb-event-track {
  position: relative;
  height: 18px;
  margin: 0 2px 4px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 10%, transparent);
  overflow: hidden;
}
.pb-event-marker {
  position: absolute;
  top: 2px;
  bottom: 2px;
  min-width: 4px;
  border: 0;
  border-radius: 6px;
  padding: 0;
  cursor: pointer;
  opacity: 0.72;
  background: currentColor;
}
.pb-event-marker[data-event-type="person"] {
  box-shadow: inset 0 0 0 2px currentColor;
  background: transparent;
}
.pb-event-marker[data-event-type="tamper"] {
  background: repeating-linear-gradient(135deg, currentColor 0 3px, transparent 3px 6px);
}
.pb-event-marker.active {
  opacity: 1;
  outline: 2px solid currentColor;
  outline-offset: 1px;
}
.pb-event-marker:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}
@media (max-width: 720px) {
  .pb-event-tools {
    grid-template-columns: 1fr 1fr;
  }
  .pb-event-readout {
    grid-column: 1 / -1;
    grid-row: 1;
  }
  #pb-prev-event { grid-column: 1; }
  #pb-next-event { grid-column: 2; }
}
