@charset "UTF-8";

/* ==========================================================================
   SENTRY — laptop anti-theft annunciator
   ==========================================================================

   Design direction: an aircraft caution-and-warning panel, not a dashboard.
   Everything here follows from three decisions:

   1. The escalation ladder is the product, so it is the permanent furniture of
      the interface — a seven-lamp annunciator strip across the top of every
      screen, legends engraved in the panel (OFF CAL ARM SUS WRN ALM LOST), lit
      one at a time. It is the signature element; everything else stays quiet
      so it can carry the drama.
   2. State is carried by the whole page. `[data-state]` on the root re-themes
      every token, so the alarm modules, the meters and the gallery shift
      together and no component has to know what tier it is in.
   3. Colour is never the only signal. Every lamp has a legend, every capability
      row has a glyph and a word, every state has a text banner.

   No build step, no preprocessor, no webfonts, no images. Icons are inline SVG
   drawn by the UI modules.

   Contents
     1  Tokens
     2  State themes
     3  Base and utilities
     4  Shell — bar, annunciator, banner, footer
     5  Primitives — placard, readout, bar, panel, buttons, inputs
     6  Screens
     7  Unlock
     8  Meters
     9  Gallery
    10  Settings
    11  Strobe contract (src/alarm/strobe.ts)
    12  Lockdown and fullscreen
    13  Reduced motion
    14  Responsive
    15  Print
   ========================================================================== */

/* ------------------------------------------------------------------ 1 tokens */

:root {
  /* Panel body: a very dark, slightly cool graphite. Never pure black — an
     anodised bezel has a colour, and pure black kills the lamp glow. */
  --ink-900: #070b0d;
  --ink-800: #0b1114;
  --ink-700: #101a1f;
  --ink-600: #16232a;
  --edge: #1c2a31;
  --edge-hi: #294048;

  --text: #d6e2e4;
  --text-dim: #83979d;
  --text-faint: #55666d;

  /* Annunciator lamp colours, straight from the vernacular: green advisory,
     white status, blue in progress, amber caution, red warning. */
  --lamp-green: #3fd08a;
  --lamp-white: #c9dce0;
  --lamp-blue: #58b6e8;
  --lamp-amber: #f2a81d;
  --lamp-red: #ff3b30;
  --lamp-red-deep: #d61111;

  /* Set per state in section 2. Components only ever read these. */
  --bg: var(--ink-900);
  --panel: var(--ink-800);
  --accent: var(--lamp-white);
  --accent-ink: #04090b;
  --glow: color-mix(in srgb, var(--accent) 40%, transparent);
  --banner-bg: transparent;

  /* Meters read these back with getComputedStyle — the palette lives here, not
     in the drawing code. */
  --spark-line: var(--accent);
  --spark-fill: color-mix(in srgb, var(--accent) 14%, transparent);
  --spark-grid: #1a272d;

  /* Strobe surface colours. The alarm module owns timing and opacity; the
     stylesheet owns every colour. See section 11. */
  --flash-a: #ffffff;
  --flash-b: #05090b;
  --flash-reduced: #f5d08a;

  --font-ui: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', 'Roboto Mono', Menlo, Consolas, monospace;

  --step-0: 0.9375rem;
  --step-1: 1.125rem;
  --step-2: 1.5rem;
  --step-3: clamp(1.75rem, 1.2rem + 2.4vw, 2.75rem);
  --step-4: clamp(2.4rem, 1rem + 6vw, 5rem);
  --step-5: clamp(3rem, 0.5rem + 11vw, 9rem);

  --gap: 1rem;
  --gap-lg: 1.75rem;
  --radius: 3px;
  --hair: 1px solid var(--edge);

  color-scheme: dark;
}

/* ------------------------------------------------------------ 2 state themes */

/* Written on both <html> (by ScreenManager, so the strobe and any sibling
   surfaces inherit) and on .sentry, hence the unqualified attribute selector. */

[data-state='disarmed'] {
  --accent: var(--lamp-white);
  --bg: var(--ink-900);
}

[data-state='calibrating'] {
  --accent: var(--lamp-blue);
  --bg: var(--ink-900);
}

/* Armed and suspicious are byte-for-byte the same theme. That is the point of
   the covert tier: see .covert-tell and the comment on ScreenManager#screenArmed.
   Do not "helpfully" tint the suspicious state — it would announce the recording
   to the person being recorded. */
[data-state='armed'],
[data-state='suspicious'] {
  --accent: var(--lamp-green);
  --bg: var(--ink-900);
}

[data-state='warning'] {
  --accent: var(--lamp-amber);
  --accent-ink: #1a1100;
  --bg: #120c02;
  --panel: #17100340;
  --banner-bg: color-mix(in srgb, var(--lamp-amber) 14%, transparent);
}

[data-state='alarm'] {
  --accent: var(--lamp-red);
  --accent-ink: #200000;
  --bg: #2a0206;
  --panel: #3a030880;
  --edge: #6b1116;
  --edge-hi: #93191f;
  --text: #ffe9e9;
  --text-dim: #f0b3b3;
  --banner-bg: color-mix(in srgb, var(--lamp-red) 22%, transparent);
}

[data-state='stolen'] {
  --accent: var(--lamp-blue);
  --accent-ink: #041018;
  --bg: #05141d;
  --panel: #08202c80;
  --edge: #14384a;
  --edge-hi: #1d5570;
  --text: #e2f0f6;
  --banner-bg: color-mix(in srgb, var(--lamp-blue) 16%, transparent);
}

/* ------------------------------------------------------- 3 base + utilities */

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

html {
  background: var(--ink-900);
  /* A lock screen should not rubber-band or scroll away under a thief's hand. */
  overscroll-behavior: none;
}

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--step-0);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  transition: background-color 400ms ease;
}

h1,
h2,
h3,
h4,
p,
ul,
ol,
dl,
dd,
figure {
  margin: 0;
}

ul,
ol {
  padding: 0;
  list-style: none;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

[hidden] {
  display: none !important;
}

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

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.muted {
  color: var(--text-dim);
}

.hint {
  margin-top: 0.5rem;
  color: var(--text-dim);
  font-size: 0.8125rem;
  line-height: 1.45;
  max-width: 62ch;
}

.hint--warn {
  color: color-mix(in srgb, var(--lamp-amber) 80%, var(--text));
}

.icon {
  display: block;
  flex: none;
}

/* The engraved placard: a legend stamped into the panel, not a heading. */
.placard {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.placard--loud {
  color: var(--accent);
  letter-spacing: 0.32em;
}

/* Every number in this app is monospace and tabular. Readouts must not reflow
   as they count. */
.readout {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  letter-spacing: 0.02em;
}

.readout--lg {
  font-size: var(--step-2);
}

.readout--xl {
  font-size: var(--step-3);
  line-height: 1;
  color: var(--accent);
}

.tag {
  display: inline-block;
  padding: 0.05rem 0.4rem;
  border: var(--hair);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--text-dim);
}

.tag--critical {
  margin-left: 0.5rem;
  border-color: var(--lamp-amber);
  color: var(--lamp-amber);
}

/* --------------------------------------------------------------- 4 the shell */

.sentry {
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  min-height: 100vh;
  min-height: 100dvh;
}

/* While disarmed the screen is a setup console, not a lock screen, and the
   sensor/evidence/settings tabs sit directly below it. Holding a full viewport
   here pushes them off the bottom of the screen with nothing to suggest they
   exist. Every other state keeps the full-bleed treatment. */
html[data-state='disarmed'] .sentry {
  min-height: 0;
}

.sentry__bar {
  display: flex;
  align-items: center;
  gap: var(--gap);
  padding: 0.5rem 0.9rem;
  border-bottom: var(--hair);
  /* Fine horizontal grille, the way a rack panel is brushed. Pure CSS. */
  background:
    repeating-linear-gradient(
      180deg,
      rgb(255 255 255 / 1.6%) 0 1px,
      transparent 1px 3px
    ),
    var(--ink-800);
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--accent);
}

.brand__name {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.34em;
  color: var(--text);
}

.bar__clock {
  margin-left: auto;
  color: var(--text-dim);
  font-size: 0.8125rem;
}

/* The annunciator strip — the signature element. */
.annunciator {
  display: flex;
  gap: 2px;
  padding: 2px;
  border: var(--hair);
  border-radius: var(--radius);
  background: #050a0c;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 4%);
}

.lamp {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.28rem 0.6rem;
  border-radius: 2px;
  background: #0a1013;
  color: var(--text-faint);
  transition:
    background-color 180ms ease,
    color 180ms ease;
}

.lamp__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #1e2c33;
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 60%);
}

.lamp__code {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.14em;
}

/* Passed rungs stay faintly lit, so the ladder reads as a ladder. */
.lamp[data-passed='1'] {
  color: var(--text-dim);
}

.lamp[data-passed='1'] .lamp__dot {
  background: #35474e;
}

.lamp[data-on='1'] {
  background: color-mix(in srgb, var(--accent) 16%, #0a1013);
  color: var(--accent);
}

.lamp[data-on='1'] .lamp__dot {
  background: var(--accent);
  box-shadow:
    0 0 6px var(--glow),
    0 0 14px var(--glow);
}

.banner {
  padding: 0.55rem 0.95rem;
  border-bottom: var(--hair);
  background: var(--banner-bg);
  color: var(--text-dim);
  font-size: 0.875rem;
}

[data-state='warning'] .banner,
[data-state='alarm'] .banner,
[data-state='stolen'] .banner {
  color: var(--text);
  font-weight: 600;
}

.screen {
  display: flex;
  flex-direction: column;
  padding: var(--gap-lg) var(--gap);
  min-width: 0;
}

.sentry__foot {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--gap-lg);
  padding: 0.5rem 0.95rem;
  border-top: var(--hair);
  background: var(--ink-800);
}

.foot__item {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.foot__value {
  font-size: 0.8125rem;
  color: var(--text);
}

.sentry__foot[data-peer='online'] [data-foot='peer'] {
  color: var(--lamp-green);
}

.sentry__foot[data-peer='offline'] [data-foot='peer'] {
  color: var(--text-dim);
}

.sentry__foot[data-level='warning'] [data-foot='score'],
.sentry__foot[data-level='alarm'] [data-foot='score'] {
  color: var(--accent);
}

/* -------------------------------------------------------------- 5 primitives */

.panel {
  border: var(--hair);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgb(255 255 255 / 2%), transparent 40%), var(--panel);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 4%);
}

.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: 0.6rem 0.9rem;
  border-bottom: var(--hair);
}

.panel__title {
  font-size: 0.6875rem;
}

.panel__note[data-ok='0'] {
  color: var(--lamp-amber);
}

.panel__note[data-ok='1'] {
  color: var(--lamp-green);
}

/* A horizontal fill bar driven by --v (0-1). scaleX keeps it off the layout
   path, which matters because the meters redraw these at display rate. */
.bar {
  position: relative;
  height: 4px;
  border-radius: 999px;
  background: #0a1216;
  overflow: hidden;
}

.bar--lg {
  height: 10px;
}

.bar__fill {
  display: block;
  height: 100%;
  transform: scaleX(var(--v, 0));
  transform-origin: left center;
  background: var(--accent);
  box-shadow: 0 0 10px var(--glow);
  transition: transform 120ms linear;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  padding: 0.5rem 0.9rem;
  border: var(--hair);
  border-radius: var(--radius);
  background: var(--ink-700);
  color: var(--text);
  font-size: 0.875rem;
  cursor: pointer;
  transition:
    border-color 140ms ease,
    background-color 140ms ease,
    color 140ms ease;
}

.btn:hover {
  border-color: var(--edge-hi);
  background: var(--ink-600);
}

.btn--primary {
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  background: color-mix(in srgb, var(--accent) 18%, var(--ink-700));
  color: var(--text);
  font-weight: 600;
}

.btn--primary:hover {
  background: color-mix(in srgb, var(--accent) 28%, var(--ink-700));
}

.btn--ghost {
  background: transparent;
  color: var(--text-dim);
}

.btn--ghost:hover {
  color: var(--text);
}

/* Deliberately the least attractive control on the page. It has to be present
   and reachable on the alarm screen without reading as "press to stop". */
.btn--quiet {
  padding: 0.25rem 0.5rem;
  border-color: transparent;
  background: transparent;
  color: var(--text-dim);
  font-size: 0.8125rem;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.btn--quiet:hover {
  background: transparent;
  border-color: transparent;
  color: var(--text);
}

.btn--danger {
  border-color: color-mix(in srgb, var(--lamp-red) 50%, transparent);
  color: color-mix(in srgb, var(--lamp-red) 85%, var(--text));
}

.btn--danger[data-confirming='1'] {
  background: var(--lamp-red-deep);
  border-color: var(--lamp-red);
  color: #fff;
}

/* The one big, unmistakable, physical-feeling control in the app. */
.btn--arm {
  flex-direction: column;
  gap: 0.15rem;
  width: 100%;
  padding: 1.15rem 1rem;
  border-color: color-mix(in srgb, var(--lamp-green) 45%, transparent);
  background:
    radial-gradient(120% 140% at 50% 0%, rgb(63 208 138 / 14%), transparent 60%),
    var(--ink-700);
}

.btn--arm:hover {
  border-color: var(--lamp-green);
  background:
    radial-gradient(120% 140% at 50% 0%, rgb(63 208 138 / 24%), transparent 60%),
    var(--ink-600);
}

.btn__key {
  font-size: 1.25rem;
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  color: var(--lamp-green);
}

.btn__sub {
  color: var(--text-dim);
  font-size: 0.75rem;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.btn-row--center {
  justify-content: center;
}

.input {
  padding: 0.45rem 0.6rem;
  border: var(--hair);
  border-radius: var(--radius);
  background: #060c0f;
  color: var(--text);
  font-family: var(--font-mono);
}

.input:focus-visible {
  border-color: var(--accent);
}

.input--num {
  width: 6.5rem;
}

.input--select {
  font-family: var(--font-ui);
}

.slider {
  width: min(18rem, 100%);
  accent-color: var(--accent);
}

/* ----------------------------------------------------------------- 6 screens */

/* --- disarmed: the arming console --- */

.console {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  grid-template-areas:
    'checks arm'
    'feed arm';
  gap: var(--gap);
  width: min(1100px, 100%);
  margin: 0 auto;
  align-content: start;
}

.panel--checklist {
  grid-area: checks;
}

.panel--arm {
  grid-area: arm;
}

.panel--feed {
  grid-area: feed;
}

.checklist {
  padding: 0.35rem 0.5rem 0.6rem;
}

.check {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: start;
  gap: 0.65rem;
  padding: 0.55rem 0.4rem;
  border-bottom: 1px solid color-mix(in srgb, var(--edge) 60%, transparent);
}

.check:last-of-type {
  border-bottom: 0;
}

.check__lamp {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border: var(--hair);
  border-radius: 50%;
  color: var(--text-faint);
}

.check[data-ok='1'] .check__lamp {
  border-color: color-mix(in srgb, var(--lamp-green) 60%, transparent);
  color: var(--lamp-green);
  box-shadow: 0 0 8px color-mix(in srgb, var(--lamp-green) 35%, transparent);
}

.check[data-ok='0'] .check__lamp {
  border-color: color-mix(in srgb, var(--lamp-amber) 50%, transparent);
  color: var(--lamp-amber);
}

.check__text {
  display: block;
  min-width: 0;
}

.check__label {
  display: block;
  font-size: 0.9375rem;
}

.check__hint {
  display: block;
  margin-top: 0.15rem;
  color: var(--text-dim);
  font-size: 0.8125rem;
  line-height: 1.4;
}

.profiles {
  display: grid;
  gap: 0.5rem;
  padding: 0.75rem 0.9rem;
}

.profile {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    'radio name'
    '. desc';
  gap: 0.1rem 0.6rem;
  padding: 0.55rem 0.7rem;
  border: var(--hair);
  border-radius: var(--radius);
  cursor: pointer;
}

.profile:hover {
  border-color: var(--edge-hi);
}

.profile__input {
  grid-area: radio;
  margin: 0.3rem 0 0;
  accent-color: var(--accent);
}

.profile__name {
  grid-area: name;
  font-weight: 600;
}

.profile__desc {
  grid-area: desc;
  color: var(--text-dim);
  font-size: 0.8125rem;
}

.profile:has(.profile__input:checked) {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.arm-block {
  display: grid;
  gap: 0.75rem;
  padding: 0 0.9rem 0.9rem;
}

.legal-ack {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.5rem;
  color: var(--text-dim);
  font-size: 0.75rem;
  line-height: 1.4;
}

.feed {
  display: grid;
  padding: 0.4rem 0.5rem 0.6rem;
  max-height: 15rem;
  overflow-y: auto;
}

.feed__row {
  display: grid;
  grid-template-columns: auto 5.5rem 1fr;
  gap: 0.6rem;
  padding: 0.22rem 0.4rem;
  font-size: 0.8125rem;
}

.feed__time {
  color: var(--text-faint);
  font-size: 0.75rem;
}

.feed__msg {
  color: var(--text-dim);
  min-width: 0;
  overflow-wrap: anywhere;
}

.feed__row[data-level='alert'] .feed__msg,
.feed__row[data-level='error'] .feed__msg {
  color: var(--lamp-amber);
}

.feed__row--empty {
  grid-template-columns: 1fr;
  color: var(--text-faint);
}

/* --- calibrating --- */

.cal {
  display: grid;
  gap: 0.75rem;
  justify-items: start;
  align-content: center;
  width: min(720px, 100%);
  margin: auto;
}

.cal__title {
  font-size: var(--step-3);
  font-weight: 300;
  letter-spacing: -0.02em;
}

.cal__sub {
  color: var(--text-dim);
  max-width: 52ch;
}

.cal__progress {
  display: grid;
  gap: 0.5rem;
  width: 100%;
  margin-top: 0.5rem;
}

.cal__nums {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}

.cal__rows {
  display: grid;
  gap: 1px;
  width: 100%;
  margin-top: 0.5rem;
  border: var(--hair);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--edge);
}

.cal__row {
  display: flex;
  justify-content: space-between;
  gap: var(--gap);
  padding: 0.4rem 0.7rem;
  background: var(--ink-800);
  font-size: 0.875rem;
}

.cal__state {
  color: var(--lamp-blue);
  font-size: 0.75rem;
}

/* --- armed / suspicious --- */

.watch {
  display: grid;
  gap: var(--gap-lg);
  justify-items: center;
  align-content: center;
  width: min(720px, 100%);
  margin: auto;
  text-align: center;
}

.monitor-card {
  position: relative;
  display: grid;
  gap: 0.5rem;
  justify-items: center;
  padding: 2rem 2.5rem;
  border: var(--hair);
  border-radius: var(--radius);
  background: var(--panel);
}

.monitor-card__mark {
  color: var(--accent);
  opacity: 0.9;
}

.monitor-card__title {
  font-size: var(--step-2);
  font-weight: 400;
  letter-spacing: -0.01em;
}

.monitor-card__body {
  color: var(--text-dim);
  max-width: 44ch;
}

/* The covert tell.
   In SUSPICIOUS the machine is recording silently and nothing on screen may
   say so — the deterrent value of the tier is that the person disturbing the
   laptop learns nothing. This 6px dot in the corner of the card is the entire
   difference between the two screens: legible to an owner who knows to look,
   meaningless to anyone who does not. Do not enlarge it, label it, or animate
   it. */
.covert-tell {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #16232a;
}

.covert-tell[data-on='1'] {
  background: var(--lamp-amber);
}

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-lg);
  justify-content: center;
}

.stat {
  display: grid;
  gap: 0.2rem;
  justify-items: center;
  min-width: 8rem;
}

.stat__unit,
.stat__label {
  color: var(--text-faint);
}

.stat__unit {
  font-size: 0.75rem;
}

.stat .bar {
  width: 8rem;
}

/* --- warning --- */

.warn {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: var(--gap-lg);
  align-items: center;
  width: min(1100px, 100%);
  margin: auto;
}

.warn__copy {
  display: grid;
  gap: 0.75rem;
  justify-items: start;
}

.warn__msg {
  font-size: var(--step-4);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: var(--text);
}

.warn__sub {
  color: var(--text-dim);
  max-width: 40ch;
}

/* Countdown ring driven by --p (0-1 elapsed). */
.countdown {
  position: relative;
  display: grid;
  place-items: center;
  width: 108px;
  height: 108px;
  border-radius: 50%;
  background:
    conic-gradient(var(--accent) calc(var(--p, 0) * 360deg), #ffffff14 0),
    var(--ink-800);
  box-shadow: inset 0 0 0 1px var(--edge);
}

.countdown::after {
  content: '';
  position: absolute;
  width: 84px;
  height: 84px;
  border-radius: 50%;
  background: var(--bg);
}

.countdown__num,
.countdown__label {
  position: relative;
  z-index: 1;
}

.countdown__num {
  font-size: var(--step-2);
  font-weight: 700;
  color: var(--accent);
  line-height: 1;
}

.countdown[data-active='0'] .countdown__num {
  color: var(--text-faint);
}

.countdown__label {
  font-size: 0.5625rem;
}

/* The mirror. Being shown your own face on a stranger's screen is a stronger
   deterrent than noise, and it is the only one that works in a library. */
.mirror {
  position: relative;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: #000;
  overflow: hidden;
  aspect-ratio: 4 / 3;
}

.mirror__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Mirrored: people expect to see themselves as in a mirror, not as a camera
     sees them, and the recognition has to be instant. */
  transform: scaleX(-1);
}

.mirror__rec {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.5rem;
  border-radius: 999px;
  background: rgb(0 0 0 / 55%);
  color: #fff;
}

.rec-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--lamp-red);
  box-shadow: 0 0 8px var(--lamp-red);
  animation: rec-pulse 2s ease-in-out infinite;
}

@keyframes rec-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

.mirror__fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 0.5rem;
  padding: 1rem;
  background: var(--ink-800);
  color: var(--text-dim);
  text-align: center;
}

/* --- alarm --- */

.alarm {
  display: grid;
  gap: 0.75rem;
  justify-items: center;
  align-content: center;
  height: 100%;
  text-align: center;
  /* Nothing on this screen is worth selecting, and a selection rectangle
     dragged across it is a thief fiddling. */
  user-select: none;
}

.alarm__title {
  font-size: var(--step-5);
  font-weight: 900;
  line-height: 0.85;
  letter-spacing: -0.045em;
  color: #fff;
  text-shadow: 0 0 40px rgb(255 59 48 / 55%);
}

.alarm__sub {
  font-size: var(--step-1);
  max-width: 30ch;
}

.alarm__foot {
  display: grid;
  gap: 0.35rem;
  justify-items: center;
  margin-top: 1.5rem;
}

/* --- stolen --- */

.lost {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) auto;
  gap: var(--gap-lg);
  align-items: center;
  width: min(1000px, 100%);
  margin: auto;
}

.lost__copy {
  display: grid;
  gap: 0.75rem;
  justify-items: start;
}

.lost__title {
  font-size: var(--step-3);
  font-weight: 700;
  letter-spacing: -0.02em;
}

.lost__sub {
  color: var(--text-dim);
  max-width: 46ch;
}

.lost__contact {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.3rem 1rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--edge-hi);
  border-radius: var(--radius);
  background: var(--panel);
  /* The one place on a triggered screen where selection is wanted: a finder
     copying an email address is the entire point of this screen. */
  user-select: text;
}

.lost__contact dd {
  font-size: var(--step-1);
}

.lost__steps {
  display: grid;
  gap: 0.35rem;
  counter-reset: step;
  color: var(--text-dim);
  font-size: 0.875rem;
}

.lost__steps li {
  counter-increment: step;
  padding-left: 1.6rem;
  position: relative;
}

.lost__steps li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  font-family: var(--font-mono);
  color: var(--accent);
}

.lost__qr {
  display: grid;
  gap: 0.5rem;
  justify-items: center;
}

/* QR container. Another module renders a <canvas> or <svg> into this element —
   it is deliberately empty here, with only a size and a light backing plate,
   because a QR code needs a quiet zone and a white background to scan. */
.qr-slot {
  display: grid;
  place-items: center;
  width: clamp(150px, 26vw, 230px);
  aspect-ratio: 1;
  padding: 10px;
  border-radius: var(--radius);
  background: #fff;
}

.qr-slot:empty::after {
  content: 'QR';
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.2em;
  color: #9aa;
}

.qr-slot > canvas,
.qr-slot > svg,
.qr-slot > img {
  display: block;
  width: 100%;
  height: auto;
  image-rendering: pixelated;
}

/* ------------------------------------------------------------------ 7 unlock */

.unlock {
  width: min(360px, 100%);
  border: var(--hair);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ink-800) 70%, transparent);
}

.unlock--quiet {
  border-color: transparent;
  background: transparent;
}

.unlock__summary {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.45rem 0.7rem;
  color: var(--text-dim);
  font-size: 0.8125rem;
  cursor: pointer;
  list-style: none;
}

.unlock__summary::-webkit-details-marker {
  display: none;
}

.unlock__summary:hover {
  color: var(--text);
}

.unlock__body {
  display: grid;
  gap: 0.75rem;
  padding: 0.5rem 0.7rem 0.8rem;
  border-top: var(--hair);
}

.unlock__field {
  display: grid;
  gap: 0.25rem;
}

.input--pin {
  width: 100%;
  letter-spacing: 0.4em;
}

.pin-display {
  min-height: 1rem;
  color: var(--accent);
  letter-spacing: 0.4em;
}

.keypad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}

.key {
  padding: 0.55rem 0;
  border: var(--hair);
  border-radius: var(--radius);
  background: var(--ink-700);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 1.0625rem;
  cursor: pointer;
}

.key:hover {
  background: var(--ink-600);
  border-color: var(--edge-hi);
}

.key:active {
  transform: translateY(1px);
}

.key--fn {
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.unlock__methods {
  display: grid;
  gap: 0.2rem;
  justify-items: start;
}

.unlock__error:not(:empty) {
  padding: 0.35rem 0.5rem;
  border-left: 2px solid var(--lamp-amber);
  color: var(--lamp-amber);
  font-size: 0.8125rem;
}

/* ------------------------------------------------------------------ 8 meters */

.meters {
  display: grid;
  gap: var(--gap);
  border: var(--hair);
  border-radius: var(--radius);
  background: var(--panel);
}

.gauge {
  display: grid;
  gap: 0.4rem;
  padding: 0 0.9rem;
}

.gauge__track {
  position: relative;
  height: 16px;
  border: var(--hair);
  border-radius: 2px;
  background: #060c0f;
  overflow: hidden;
}

.gauge__fill {
  display: block;
  height: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(
    90deg,
    var(--lamp-green),
    var(--lamp-amber) 60%,
    var(--lamp-red) 100%
  );
  transition: transform 100ms linear;
}

.gauge__ticks {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Threshold marks are drawn and named, not just implied by colour — the gauge
   has to be readable in a screenshot pasted into a bug report. */
.gauge__tick {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--at, 0) * 100%);
  width: 1px;
  background: rgb(255 255 255 / 45%);
}

.gauge__tick-label {
  position: absolute;
  top: 100%;
  left: 0;
  padding-top: 2px;
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
  /* Slide the label from left-aligned at 0% to right-aligned at 100%, passing
     through centred at 50%. Centring every label instead pushes the one on the
     ALARM tick past the right edge and gives the whole document a horizontal
     scrollbar — on a lock screen, of all things. `--at` inherits from the tick. */
  transform: translateX(calc(var(--at, 0) * -100%));
}

.gauge__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-top: 0.85rem;
}

.gauge__value {
  font-size: var(--step-2);
}

.gauge[data-level='suspicious'] .gauge__value {
  color: var(--lamp-white);
}

.gauge[data-level='warning'] .gauge__value {
  color: var(--lamp-amber);
}

.gauge[data-level='alarm'] .gauge__value {
  color: var(--lamp-red);
}

.spark {
  padding: 0 0.9rem;
}

.spark__canvas {
  display: block;
  width: 100%;
  height: 56px;
  border: var(--hair);
  border-radius: 2px;
  background: #060c0f;
}

.srows {
  display: grid;
  gap: 1px;
  border-top: var(--hair);
  background: var(--edge);
}

.srow {
  display: grid;
  grid-template-columns: 10px 11rem minmax(4rem, 1fr) 4.5rem 3.5rem minmax(6rem, 1.2fr);
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.9rem;
  background: var(--ink-800);
  font-size: 0.8125rem;
}

.srow__lamp {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #22333a;
}

.srow[data-state='ok'] .srow__lamp {
  background: var(--lamp-green);
  box-shadow: 0 0 6px color-mix(in srgb, var(--lamp-green) 60%, transparent);
}

.srow[data-state='error'] .srow__lamp {
  background: var(--lamp-red);
}

.srow[data-state='off'] {
  color: var(--text-faint);
}

.srow__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.srow__meter {
  position: relative;
  height: 8px;
  border-radius: 2px;
  background: #060c0f;
  overflow: hidden;
}

.srow__fill {
  display: block;
  height: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  background: var(--text-dim);
}

.srow__fill[data-over='1'] {
  background: var(--lamp-amber);
}

.srow__thresh {
  position: absolute;
  top: -1px;
  bottom: -1px;
  left: calc(var(--at, 1) * 100%);
  width: 2px;
  background: var(--lamp-red);
}

.srow__val,
.srow__hz {
  text-align: right;
  color: var(--text-dim);
  font-size: 0.75rem;
}

.srow__hz {
  color: var(--text-faint);
}

.srow__last {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-faint);
  font-size: 0.75rem;
}

.srow[data-state='error'] .srow__last {
  color: var(--lamp-red);
}

/* ----------------------------------------------------------------- 9 gallery */

.gallery {
  display: grid;
  gap: var(--gap);
  border: var(--hair);
  border-radius: var(--radius);
  background: var(--panel);
}

.gallery > *:not(.panel__head) {
  padding: 0 0.9rem 0.9rem;
}

.gallery__empty,
.locked {
  display: grid;
  gap: 0.4rem;
  justify-items: start;
  color: var(--text-dim);
}

.locked {
  color: var(--accent);
}

.locked__title {
  color: var(--text);
  font-size: var(--step-1);
  font-weight: 600;
}

.passphrase {
  display: flex;
  gap: 0.5rem;
  align-items: flex-end;
  margin-top: 0.5rem;
}

.passphrase__field {
  display: grid;
  gap: 0.25rem;
}

.incidents {
  display: grid;
  gap: 1px;
  background: var(--edge);
  border-top: var(--hair);
  border-bottom: var(--hair);
}

.incident {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-right: 0.6rem;
  background: var(--ink-800);
}

.incident__open {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.6rem;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.incident__open:hover {
  background: var(--ink-700);
}

.incident__thumb {
  display: grid;
  place-items: center;
  width: 64px;
  height: 48px;
  border: var(--hair);
  border-radius: 2px;
  background: #050a0c;
  color: var(--text-faint);
  overflow: hidden;
}

.incident__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.incident__meta {
  display: grid;
  gap: 0.15rem;
  min-width: 0;
}

.incident__when {
  color: var(--text-dim);
  font-size: 0.75rem;
}

.incident__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.incident[data-state='alarm'] .incident__thumb,
.incident[data-state='stolen'] .incident__thumb {
  border-color: color-mix(in srgb, var(--lamp-red) 55%, transparent);
}

.detail {
  display: grid;
  gap: var(--gap);
}

.detail__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem var(--gap);
}

.detail__title {
  font-size: var(--step-1);
}

.detail__when {
  color: var(--text-dim);
  font-size: 0.8125rem;
}

.detail__head .btn-row {
  margin-left: auto;
}

.detail__media {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 0.6rem;
}

.tile {
  display: grid;
  gap: 0.3rem;
  padding: 0.4rem;
  border: var(--hair);
  border-radius: var(--radius);
  background: #050a0c;
}

.tile__cap {
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  font-size: 0.6875rem;
  color: var(--text-faint);
}

.detail__img,
.detail__video {
  width: 100%;
  height: auto;
  border-radius: 2px;
  background: #000;
}

.detail__audio {
  width: 100%;
}

.timeline {
  display: grid;
  gap: 1px;
  margin-top: 0.4rem;
  background: var(--edge);
  border: var(--hair);
  border-radius: var(--radius);
  overflow: hidden;
}

.tl-item {
  display: grid;
  grid-template-columns: auto 6rem 1fr;
  gap: 0.6rem;
  padding: 0.3rem 0.6rem;
  background: var(--ink-800);
  font-size: 0.8125rem;
}

.tl-item__time {
  color: var(--text-faint);
  font-size: 0.75rem;
}

.tl-item[data-level='alert'] {
  background: color-mix(in srgb, var(--lamp-red) 10%, var(--ink-800));
}

.tl-item--empty {
  grid-template-columns: 1fr;
  color: var(--text-faint);
}

/* --------------------------------------------------------------- 10 settings */

.settings {
  display: grid;
  gap: var(--gap);
  border: var(--hair);
  border-radius: var(--radius);
  background: var(--panel);
}

.settings > *:not(.panel__head) {
  margin: 0 0.9rem;
}

.settings > *:last-child {
  margin-bottom: 0.9rem;
}

.fieldset {
  display: grid;
  gap: 0.5rem;
  padding: 0.75rem 0.9rem 0.9rem;
  border: var(--hair);
  border-radius: var(--radius);
}

.fieldset > legend {
  padding: 0 0.4rem;
}

.field {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem var(--gap);
  padding: 0.3rem 0;
}

.field__text {
  display: grid;
  gap: 0.1rem;
  min-width: 0;
  flex: 1 1 18rem;
}

.field__label {
  font-size: 0.9375rem;
}

.field__hint {
  color: var(--text-dim);
  font-size: 0.75rem;
  line-height: 1.4;
  max-width: 58ch;
}

.field__control {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.field__unit,
.field__out {
  color: var(--text-dim);
  font-size: 0.8125rem;
  min-width: 3rem;
}

.field--toggle {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.6rem;
  align-items: start;
  cursor: pointer;
}

.switch__input {
  position: absolute;
  opacity: 0;
  width: 34px;
  height: 20px;
  margin: 0;
  cursor: pointer;
}

.switch {
  position: relative;
  width: 34px;
  height: 20px;
  border: var(--hair);
  border-radius: 999px;
  background: #060c0f;
  transition: background-color 140ms ease;
}

.switch::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--text-faint);
  transition: transform 140ms ease, background-color 140ms ease;
}

.switch__input:checked + .switch {
  background: color-mix(in srgb, var(--accent) 30%, #060c0f);
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
}

.switch__input:checked + .switch::after {
  transform: translateX(14px);
  background: var(--accent);
}

.switch__input:focus-visible + .switch {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.fieldset--sensors {
  gap: 0.75rem;
}

.sensor-block {
  display: grid;
  gap: 0.25rem;
  padding: 0.5rem 0.6rem;
  border: var(--hair);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ink-900) 55%, transparent);
}

.sensor-block__name {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text);
}

.field--pin {
  justify-content: flex-start;
}

/* The honest capability table. Status is a glyph AND a word, never a colour. */
.captable {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8125rem;
}

.captable th,
.captable td {
  padding: 0.3rem 0.5rem;
  border-bottom: 1px solid color-mix(in srgb, var(--edge) 70%, transparent);
  text-align: left;
  vertical-align: top;
  font-weight: 400;
}

.captable thead th {
  color: var(--text-faint);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.captable__grouprow th {
  padding-top: 0.7rem;
  border-bottom-color: var(--edge-hi);
}

.captable__status {
  white-space: nowrap;
}

.captable__glyph {
  margin-right: 0.35rem;
  font-family: var(--font-mono);
}

.captable tr[data-ok='1'] .captable__status {
  color: var(--lamp-green);
}

.captable tr[data-ok='0'] .captable__status {
  color: var(--text-dim);
}

.captable tr[data-ok='0'][data-critical='1'] .captable__status {
  color: var(--lamp-amber);
}

.captable__hint {
  color: var(--text-dim);
  max-width: 46ch;
}

.legal-note {
  padding: 0.5rem 0.7rem;
  border-left: 2px solid var(--edge-hi);
  color: var(--text-dim);
  font-size: 0.75rem;
  line-height: 1.45;
}

/* ------------------------------------------------- 11 strobe (alarm contract)

   Contract with src/alarm/strobe.ts. That module toggles the classes below on
   its root element (default <body>) and writes --strobe-* to the root's inline
   style; it never creates elements and never chooses a colour. So the flash
   surface is a ::after pseudo-element, and every colour is chosen here.

   THERE IS DELIBERATELY NO CSS ANIMATION IN THIS SECTION.
   Opacity is bound to --strobe-opacity, which the module drives per animation
   frame under a hard 3 Hz clamp (WCAG 2.3.1; photosensitive epilepsy). A CSS
   animation here would run on top of that clamp and could reintroduce faster
   flashing that the JS cannot see or stop. Do not add one.
   -------------------------------------------------------------------------- */

.sentry-strobe::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9000;
  pointer-events: none;
  background: var(--flash-a);
  /* --strobe-intensity scales how hard the flash hits, so a WARNING wash and an
     ALARM flash can share one driver. */
  opacity: calc(var(--strobe-opacity, 0) * (0.4 + 0.6 * var(--strobe-intensity, 1)));
}

/* Phase B — the alternate colour. Absent means phase A. */
.sentry-strobe--phase-b::after {
  background: var(--flash-b);
}

.sentry-strobe--on::after {
  will-change: opacity;
}

/* Tier palettes. Exactly one is present at a time, or none. */
.sentry-strobe--tier-warning {
  /* Calm, and appears without flashing: both phases are the same colour, so
     even a stray phase toggle cannot turn the warning tier into a strobe. */
  --flash-a: #f2a81d;
  --flash-b: #f2a81d;
  --flash-reduced: #f2a81d;
}

.sentry-strobe--tier-alarm {
  /* White against deep red. White is not decorative: it is the camera fill
     light that makes evidence stills of a face rather than of a dark rectangle. */
  --flash-a: #ffffff;
  --flash-b: #d61111;
  --flash-reduced: #ff6a5f;
}

.sentry-strobe--tier-stolen {
  /* Visually distinct from ALARM — this one is meant to attract a passer-by,
     not to frighten someone standing over the machine. */
  --flash-a: #58b6e8;
  --flash-b: #06131a;
  --flash-reduced: #58b6e8;
}

/* Reduced motion: one colour only, never an alternation. The module drives a
   slow ~0.4 Hz opacity pulse; this rule guarantees the colour cannot switch
   even if the phase class is toggled anyway. */
.sentry-strobe--reduced::after,
.sentry-strobe--reduced.sentry-strobe--phase-b::after {
  background: var(--flash-reduced);
}

/* Camera fill light: sustained, full white, overrides every tier colour. */
.sentry-strobe--capture::after,
.sentry-strobe--capture.sentry-strobe--phase-b::after {
  background: #ffffff;
  opacity: 1;
}

/* ------------------------------------------------ 12 lockdown and fullscreen

   src/alarm/lockdown.ts toggles no classes at all — it only calls browser APIs
   (fullscreen, keyboard lock, pointer lock, wake lock). All that is needed here
   is that the shell fills a fullscreened viewport, plus one opt-in utility for
   hiding the cursor, which the app may apply while the pointer is locked.
   -------------------------------------------------------------------------- */

:fullscreen .sentry,
:fullscreen body {
  min-height: 100vh;
}

:fullscreen .sentry {
  background: var(--bg);
}

/* Opt-in, applied by the app — never by CSS state. A cursor hidden on a screen
   that still has an unlock control is a usability trap, so this is only for
   pointer-locked states. */
.no-cursor,
.no-cursor * {
  cursor: none !important;
}

/* ---------------------------------------------------------- 13 reduced motion

   The strobe's own reduced-motion behaviour lives in section 11 and is driven
   by JS. This is the belt to that pair of braces: it holds even if the module
   is not running, and it also quiets everything else that moves.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* Single colour under every tier and both phases — no alternation at all. */
  .sentry-strobe::after,
  .sentry-strobe--phase-b::after {
    background: var(--flash-reduced);
  }

  /* Cap the surface so the pulse is a wash rather than a flash, and let it
     ride the JS-driven ~0.4 Hz opacity ramp smoothly. This transition is the
     one exception to the rule above, and it is a slowdown, not an animation. */
  .sentry-strobe:not(.sentry-strobe--capture)::after {
    opacity: calc(var(--strobe-opacity, 0) * 0.65);
    transition: opacity 600ms linear !important;
  }

  .rec-dot {
    animation: none;
    opacity: 1;
  }
}

/* ------------------------------------------------------------- 14 responsive

   phone.html reuses this stylesheet, so everything has to fold to one column
   on a handset. The annunciator keeps its dots and drops the legends of the
   lamps that are not lit, because the strip is the one thing that must survive
   at any width.
   -------------------------------------------------------------------------- */

@media (max-width: 900px) {
  .console {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      'arm'
      'checks'
      'feed';
  }

  .warn,
  .lost {
    grid-template-columns: minmax(0, 1fr);
  }

  .warn {
    align-content: start;
  }

  .mirror {
    order: -1;
    width: min(420px, 100%);
    margin-inline: auto;
  }

  .lost__qr {
    justify-self: start;
  }

  .srow {
    grid-template-columns: 10px 9rem minmax(3rem, 1fr) 4rem;
  }

  .srow__hz,
  .srow__last {
    display: none;
  }
}

@media (max-width: 560px) {
  :root {
    --gap-lg: 1.15rem;
  }

  .sentry__bar {
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  .annunciator {
    order: 3;
    width: 100%;
    justify-content: space-between;
  }

  .bar__clock {
    font-size: 0.75rem;
  }

  .lamp {
    padding: 0.28rem 0.35rem;
  }

  .lamp[data-on='0'] .lamp__code {
    display: none;
  }

  .screen {
    padding: var(--gap) 0.75rem;
  }

  .check {
    grid-template-columns: auto 1fr;
  }

  .check__action {
    grid-column: 2;
    justify-self: start;
  }

  .feed__row,
  .tl-item {
    grid-template-columns: auto 1fr;
  }

  .feed__src,
  .tl-item__src {
    display: none;
  }

  .detail__head .btn-row {
    margin-left: 0;
  }

  .stats {
    gap: var(--gap);
  }
}

/* ------------------------------------------------------------------ 15 print

   For the exported incident report: something you can hand to campus security.
   Ink on paper, no panel chrome, and every timestamp legible.
   -------------------------------------------------------------------------- */

@media print {
  :root {
    --text: #000;
    --text-dim: #333;
    --text-faint: #555;
    --edge: #bbb;
    --edge-hi: #888;
    --accent: #000;
  }

  html,
  body,
  .sentry {
    background: #fff !important;
    color: #000 !important;
    min-height: 0;
  }

  /* Live controls mean nothing on paper. */
  .sentry__bar,
  .sentry__foot,
  .banner,
  .btn,
  .btn-row,
  .keypad,
  .unlock,
  .spark,
  .profiles,
  .arm-block,
  .no-print,
  .sentry-strobe::after {
    display: none !important;
  }

  .screen,
  .gallery,
  .settings,
  .meters {
    display: block;
    padding: 0;
    border: 0;
    background: none;
  }

  .panel,
  .fieldset,
  .timeline,
  .tile,
  .lost__contact {
    border: 1px solid #bbb !important;
    background: none !important;
    box-shadow: none !important;
  }

  .placard {
    color: #444;
  }

  /* Evidence must not be split across a page break. */
  .tile,
  .tl-item,
  .incident,
  .captable tr {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  .detail__media {
    grid-template-columns: repeat(2, 1fr);
  }

  .detail__img {
    max-height: 9cm;
    object-fit: contain;
  }

  .timeline,
  .captable {
    font-size: 10pt;
  }

  .qr-slot {
    border: 1px solid #bbb;
  }

  /* A printed report is evidence: show where a link actually pointed. */
  .detail a[href]::after {
    content: ' (' attr(href) ')';
    font-size: 9pt;
    color: #555;
  }

  .print-only {
    display: revert !important;
  }
}

.print-only {
  display: none;
}

/* --------------------------------------------------------------- 14 workbench

   The setup surface below the arming screen: sensor meters, evidence and
   settings. Only ever on screen while disarmed, so it is styled as a quiet
   companion to the arming console rather than competing with it.

   Named `workbench` rather than `console` because `.console` is already the
   arming screen's internal `checks | arm` grid, and the collision squeezed the
   sensor rows into a column narrower than their own minimum width. */

.workbench {
  width: min(1100px, 100%);
  margin: 0 auto;
  padding: 0 var(--gap) calc(var(--gap) * 2);
}

.workbench[hidden] {
  display: none;
}

.workbench__tabs {
  display: flex;
  gap: 2px;
  border-bottom: var(--hair);
  margin-bottom: var(--gap);
}

.workbench__tab {
  appearance: none;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  padding: 0.55rem 0.95rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}

.workbench__tab:hover {
  color: var(--text);
}

.workbench__tab[aria-selected='true'] {
  color: var(--text);
  border-bottom-color: var(--lamp-green);
}

.workbench__tab:focus-visible {
  outline: 2px solid var(--focus, currentColor);
  outline-offset: -2px;
}

.workbench__panel[hidden] {
  display: none;
}

/* The sensor rows have a real minimum width — six columns of readout that stop
   being legible if they wrap. Below that, scroll the panel rather than
   crushing it. */
.workbench__panel {
  overflow-x: auto;
}

/* ------------------------------------------------------------------ 16 phone

   phone.html only. Everything above is shared with the laptop and is not
   touched here — the phone reuses `.sentry`, `.annunciator`, `.panel`,
   `.placard`, `.readout`, `.bar`, `.btn` and the `[data-state]` theming
   verbatim, so the two screens describe the same machine in the same language.
   What follows is only what a handset needs and a laptop does not.

   Two rules govern this section:
     - The dead man's switch is the headline, so it is the largest, loudest
       block on the page and its `suspect` tier has the biggest tap target in
       the app. A cancel button you have to aim at is a cancel button you miss
       while your laptop is being carried away.
     - Nothing here flashes. The phone's alarm overlay is a steady colour with a
       slow pulse well under the 3 Hz photosensitivity limit, for the same
       reason section 11 has no CSS animation in it.
   -------------------------------------------------------------------------- */

.ph-body {
  /* A phone alarm screen must not rubber-band away under a panicking thumb. */
  overscroll-behavior: none;
}

.ph {
  grid-template-rows: auto auto 1fr auto;
}

.ph-screen {
  display: grid;
  gap: var(--gap);
  align-content: start;
  padding: var(--gap) 0.75rem;
  /* Deep enough that the sticky footer never permanently covers the last row
     of content, plus clearance for the home indicator on a modern handset. */
  padding-bottom: calc(4rem + env(safe-area-inset-bottom));
}

.ph-slot:empty {
  display: none;
}

.ph-foot {
  position: sticky;
  bottom: 0;
  padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
  /* The link state is the one fact worth keeping on screen while scrolling. */
  backdrop-filter: blur(6px);
}

/* --- the dead man's switch --------------------------------------------- */

.ph-dm {
  border-color: var(--dm-edge, var(--edge));
}

.ph-dm__body-wrap {
  display: grid;
  gap: 0.7rem;
  padding: 0.9rem;
}

.ph-dm__title {
  font-size: var(--step-3);
  line-height: 1;
  letter-spacing: 0.06em;
  color: var(--dm-ink, var(--text));
}

.ph-dm__body {
  color: var(--text-dim);
  font-size: 0.875rem;
  line-height: 1.45;
  max-width: 48ch;
}

.ph-dm__hint {
  text-align: right;
  color: var(--text-faint);
}

/* At the narrowest widths the panel header is the title and nothing else —
   this line is a nicety, and the title is not. */
@media (max-width: 420px) {
  .ph-dm__hint {
    display: none;
  }
}

.ph-dm__stats {
  display: flex;
  gap: var(--gap-lg);
  margin: 0;
}

.ph-dm__stats div {
  display: grid;
  gap: 0.1rem;
}

.ph-dm__stats dd {
  margin: 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* Tone follows the switch's own state, not the laptop's — this panel is the
   one place on the page that reports on the phone rather than the machine. */
.ph-dm[data-tone='green'] {
  --dm-ink: var(--lamp-green);
  --dm-edge: color-mix(in srgb, var(--lamp-green) 35%, var(--edge));
}

.ph-dm[data-tone='amber'] {
  --dm-ink: var(--lamp-amber);
  --dm-edge: var(--lamp-amber);
}

.ph-dm[data-tone='red'] {
  --dm-ink: var(--lamp-red);
  --dm-edge: var(--lamp-red);
}

.ph-dm[data-tone='blue'] {
  --dm-ink: var(--lamp-blue);
  --dm-edge: color-mix(in srgb, var(--lamp-blue) 35%, var(--edge));
}

.ph-dm[data-tone='grey'] {
  --dm-ink: var(--text-dim);
}

.ph-dm__countdown {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}

.ph-dm__count {
  font-size: var(--step-5);
  line-height: 0.9;
  color: var(--lamp-amber);
  font-variant-numeric: tabular-nums;
}

.ph-dm__actions {
  gap: 0.5rem;
}

/* The one control that has to be hittable without looking. 56px is the floor
   for a reliable thumb target; this is comfortably above it. */
.ph-dm__big {
  flex: 1 1 100%;
  min-height: 3.75rem;
  font-size: 1.0625rem;
  font-weight: 600;
}

.ph-dm[data-dm='suspect'] .ph-dm__big {
  border-color: var(--lamp-amber);
  background: color-mix(in srgb, var(--lamp-amber) 22%, var(--ink-700));
}

/* --- the alarm overlay -------------------------------------------------- */

.ph-overlay {
  position: fixed;
  inset: 0;
  z-index: 9500;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--gap);
  padding: var(--gap-lg) var(--gap);
  text-align: center;
  color: #fff;
  background: radial-gradient(120% 90% at 50% 20%, #ff2d20, #7a0208 70%, #3d0004);
  animation: ph-alarm-pulse 1.6s ease-in-out infinite alternate;
}

.ph-overlay[hidden] {
  display: none;
}

/* ~0.6 Hz, one colour, brightness only. Deliberately far below the 3 Hz
   photosensitivity limit — this has to be survivable at arm's length in the
   dark, and it is on screen for as long as the owner takes to react.

   Brightness rather than opacity on purpose: pulsing opacity lets the page
   underneath ghost through, which turns the one screen that has to be readable
   at a glance into a smear. */
@keyframes ph-alarm-pulse {
  from {
    filter: brightness(0.82);
  }
  to {
    filter: brightness(1.1);
  }
}

.ph-overlay__kicker {
  color: rgb(255 255 255 / 75%);
}

.ph-overlay__title {
  font-size: var(--step-4);
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0.02em;
  max-width: 14ch;
}

.ph-overlay__reason {
  font-family: var(--font-mono);
  color: rgb(255 255 255 / 85%);
}

.ph-overlay__btn {
  min-height: 3.75rem;
  min-width: min(20rem, 80vw);
  border-color: #fff;
  background: rgb(0 0 0 / 25%);
  color: #fff;
  font-size: 1.0625rem;
  font-weight: 600;
}

.ph-overlay__quiet {
  color: rgb(255 255 255 / 80%);
}

/* The page underneath goes red too, so a glance at a half-covered screen still
   reads as an alarm. */
.ph-alarming .ph-screen {
  filter: saturate(1.2);
}

/* --- status rows -------------------------------------------------------- */

.ph-status__score {
  margin: 0 0.9rem;
}

.ph-rows {
  display: grid;
  gap: 0.35rem;
  margin: 0;
  padding: 0.7rem 0.9rem 0.9rem;
}

.ph-rows__row {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: 0.6rem;
  align-items: baseline;
}

.ph-rows__row[hidden] {
  display: none;
}

.ph-rows dt {
  color: var(--text-faint);
}

.ph-rows__v {
  margin: 0;
  font-size: 0.8125rem;
  overflow-wrap: anywhere;
}

/* --- commands ----------------------------------------------------------- */

.ph-cmd__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
  padding: 0.9rem;
}

.ph-cmd__grid .btn {
  min-height: 3rem;
}

.ph-cmd__grid .btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.ph-cmd__grid .btn[data-on='1'] {
  border-color: var(--lamp-green);
  color: var(--lamp-green);
}

.ph-cmd__status {
  padding: 0 0.9rem 0.6rem;
  color: var(--text-dim);
}

.ph-cmd__status[data-ok='0'] {
  color: var(--lamp-amber);
}

.ph-cmd__media {
  padding: 0 0.9rem 0.9rem;
}

.ph-cmd__video {
  width: 100%;
  border: var(--hair);
  border-radius: var(--radius);
  background: #000;
  aspect-ratio: 4 / 3;
  object-fit: contain;
}

.ph-cmd__medianote {
  margin-top: 0.35rem;
  color: var(--text-faint);
}

/* --- alerts ------------------------------------------------------------- */

.ph-feed {
  display: grid;
  gap: 0.6rem;
  padding: 0.9rem;
}

.ph-feed__empty {
  font-size: 0.875rem;
}

.ph-feed__item {
  display: grid;
  gap: 0.4rem;
  padding: 0.6rem;
  border: var(--hair);
  border-radius: var(--radius);
  background: #070d10;
}

/* Colour by the tier the incident reached, using the same lamp vocabulary as
   the annunciator. Never colour alone — the tier is spelled out as text. */
.ph-feed__item[data-tier='warning'] {
  border-left: 3px solid var(--lamp-amber);
}

.ph-feed__item[data-tier='alarm'],
.ph-feed__item[data-tier='stolen'] {
  border-left: 3px solid var(--lamp-red);
}

.ph-feed__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.ph-feed__tier {
  color: var(--text);
}

.ph-feed__ago,
.ph-feed__clock {
  font-size: 0.75rem;
}

.ph-feed__reason {
  font-size: 0.9375rem;
}

.ph-feed__thumb {
  width: 100%;
  max-height: 46vh;
  object-fit: contain;
  border-radius: var(--radius);
  background: #000;
}

.ph-feed__strip {
  display: flex;
  gap: 0.35rem;
  overflow-x: auto;
  padding-bottom: 0.2rem;
}

.ph-feed__shot {
  width: 5.5rem;
  height: 4rem;
  object-fit: cover;
  border-radius: 2px;
  background: #000;
  flex: none;
}

.ph-feed__sensors {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.ph-feed__score {
  color: var(--text-faint);
}

/* --- presence ----------------------------------------------------------- */

.ph-presence__body {
  display: grid;
  gap: 0.5rem;
  padding: 0.9rem 0.9rem 0.4rem;
}

.ph-presence .hint {
  padding: 0 0.9rem;
}

.ph-presence .hint:last-child {
  padding-bottom: 0.9rem;
}

.ph-presence__state {
  font-size: 0.75rem;
  color: var(--text-dim);
}

.ph-presence__body .btn[data-on='1'] {
  border-color: var(--lamp-green);
  color: var(--lamp-green);
}

/* --- pairing ------------------------------------------------------------ */

.ph-pair__body {
  padding: 0.9rem;
}

.ph-pair__row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.ph-pair__name {
  font-size: 1.0625rem;
}

/* Read aloud against the laptop's screen, so it is spaced and unmistakable. */
.ph-pair__fp {
  display: block;
  margin: 0.15rem 0 0.6rem;
  letter-spacing: 0.22em;
  color: var(--accent);
  overflow-wrap: anywhere;
}

.ph-pair__paired[hidden],
.ph-pair__unpaired[hidden] {
  display: none;
}

.ph-pair__form {
  display: flex;
  gap: 0.5rem;
  margin: 0.6rem 0;
  flex-wrap: wrap;
}

.ph-pair__code {
  flex: 1 1 10rem;
  min-width: 0;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

/* --- this phone --------------------------------------------------------- */

.ph-setup__checks {
  display: grid;
  padding: 0.6rem 0.9rem;
}

.ph-setup__check {
  padding: 0.35rem 0;
}

.ph-setup .btn-row {
  padding: 0 0.9rem 0.6rem;
}

.ph-setup .hint {
  padding: 0 0.9rem 0.6rem;
}

.ph-setup__details {
  padding: 0 0.9rem 0.9rem;
}

.ph-setup__details summary {
  cursor: pointer;
  padding: 0.3rem 0;
}

.ph-setup__log {
  display: grid;
  gap: 0.2rem;
  max-height: 14rem;
  overflow-y: auto;
  font-size: 0.75rem;
}

.ph-setup__logrow {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: 0.5rem;
  color: var(--text-dim);
}

.ph-setup__logrow .readout {
  font-size: 0.6875rem;
}

/* --- responsive + motion ------------------------------------------------- */

@media (min-width: 600px) {
  .ph-screen {
    width: min(640px, 100%);
    margin-inline: auto;
  }

  .ph-cmd__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (prefers-reduced-motion: reduce) {
  .ph-overlay {
    animation: none;
    filter: none;
  }
}

/* ------------------------------------------------------- 17 workbench order

   The workbench (sensors / evidence / settings) lives after `#screen-root` in
   the document, which puts it after the shell's status footer — so while
   disarmed the settings panel rendered *below* the footer bar and read as a
   layout bug rather than a panel.

   Fixing it by moving the DOM would mean reaching into the subtree ScreenManager
   owns and re-renders wholesale. Ordering it instead is both smaller and more
   robust: body becomes a flex column while disarmed, and the workbench is
   ordered ahead of the shell's own footer by pulling the footer last.

   Only while disarmed. In every other state the workbench is hidden anyway and
   the shell keeps its normal full-bleed layout. */

html[data-state='disarmed'] body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

html[data-state='disarmed'] #screen-root {
  display: contents;
}

/* `display: contents` lifts .sentry's children into the body flex line, so the
   grid rows become flex items and can be ordered against the workbench. */
html[data-state='disarmed'] .sentry {
  display: contents;
}

html[data-state='disarmed'] .workbench {
  order: 90;
}

html[data-state='disarmed'] .sentry__foot {
  order: 100;
  margin-top: auto;
}

/* ------------------------------------------------------------------ 18 modal

   Used for the two moments the app has to stop and get something from the user:
   setting an unlock PIN, and showing a pairing QR. Sits above everything,
   including the strobe surface (z-index 9000), because a PIN prompt that a
   flashing alarm renders unreadable is not a way out. */

.modal {
  position: fixed;
  inset: 0;
  z-index: 9500;
  display: grid;
  place-items: center;
  padding: var(--gap);
  background: rgb(2 5 7 / 82%);
  backdrop-filter: blur(3px);
}

.modal__panel {
  width: min(30rem, 100%);
  max-height: 90vh;
  overflow-y: auto;
  padding: var(--gap-lg);
  border: 1px solid var(--edge-hi);
  border-radius: var(--radius);
  background: var(--ink-800);
  box-shadow: 0 24px 60px rgb(0 0 0 / 55%);
}

.modal__title {
  margin-bottom: var(--gap);
}

.modal__body {
  display: grid;
  gap: 0.75rem;
}

.modal__text {
  color: var(--text-dim);
  font-size: 0.875rem;
  line-height: 1.5;
}

.modal__error:empty {
  display: none;
}

.modal__error {
  margin: 0;
  color: var(--lamp-amber);
  font-size: 0.8125rem;
}

.modal__actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: var(--gap-lg);
}

/* Pairing */

.pair__qr {
  display: grid;
  place-items: center;
  padding: var(--gap);
  background: #fff; /* QR contrast is not negotiable — scanners need true white. */
  border-radius: var(--radius);
}

.pair__qr canvas,
.pair__qr svg {
  display: block;
  max-width: 100%;
  height: auto;
}

.pair__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 0.75rem;
}

.pair__code {
  display: block;
  margin-top: 0.15rem;
  font-size: 1rem;
  letter-spacing: 0.12em;
  color: var(--text);
}

/* ------------------------------------------------- 18 first-run setup

   The guided setup that runs before a machine can be armed. Deliberately built
   as a full-bleed panel rather than a small dialog: it is not an interruption
   to something else, it *is* the screen until it is done, and every step has
   real prose on it that a 30rem modal would turn into a scroll bar.

   Two things it borrows wholesale rather than restating: `.check` from the
   pre-flight checklist (same lamp, same word, same `[data-ok]` theming, so a
   granted permission looks identical here and on the console), and `.panel`,
   `.placard`, `.readout`, `.btn`, `.hint` from the primitives.

   z-index sits *below* the strobe at 9000 on purpose. Setup only ever runs
   while disarmed, but if anything ever manages to raise an alarm underneath it,
   the alarm must be the thing you see. */

.ob {
  position: fixed;
  inset: 0;
  z-index: 8000;
  display: grid;
  place-items: center;
  padding: clamp(0.5rem, 2vw, 2rem);
  background:
    radial-gradient(120% 90% at 50% 0%, rgb(88 182 232 / 6%), transparent 65%),
    color-mix(in srgb, var(--ink-900) 92%, transparent);
  backdrop-filter: blur(3px);
  overflow: auto;
}

.ob__panel {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  width: min(64rem, 100%);
  max-height: min(52rem, 100%);
  background: linear-gradient(180deg, rgb(255 255 255 / 3%), transparent 30%), var(--ink-800);
}

.ob__bar {
  display: flex;
  align-items: center;
  gap: var(--gap);
  padding: 0.55rem 0.9rem;
  border-bottom: var(--hair);
}

.ob__counter {
  margin-left: auto;
  color: var(--text-faint);
  font-size: 0.75rem;
}

.ob__exit {
  flex: none;
}

/* The progress strip: the annunciator idiom again, legends engraved into the
   panel and exactly one lit. Never colour alone — each step carries its code,
   and its full name is in the accessibility tree. */
.ob__steps {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 1.1rem;
  padding: 0.5rem 0.9rem;
  border-bottom: var(--hair);
}

.ob__step {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.ob__step-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--edge-hi);
}

.ob__step[data-passed='1'] .ob__step-dot {
  background: color-mix(in srgb, var(--lamp-green) 70%, transparent);
}

.ob__step[data-on='1'] .ob__step-dot {
  background: var(--accent);
  box-shadow: 0 0 8px var(--glow);
}

.ob__step-code {
  font-size: 0.625rem;
}

.ob__step[data-on='1'] .ob__step-code {
  color: var(--text);
}

.ob__step[data-passed='1'] .ob__step-code {
  color: var(--text-dim);
}

.ob__main {
  padding: clamp(0.9rem, 2vw, 1.6rem);
  overflow-y: auto;
}

/* Focus moves here on every step change, so the ring is doing real work — it is
   the sighted equivalent of the live-region announcement. `inline-block` so it
   hugs the words instead of drawing a full-width box that reads as a text
   field. */
.ob__title {
  display: inline-block;
  max-width: 100%;
  font-size: var(--step-2);
  line-height: 1.15;
  letter-spacing: -0.01em;
}

.ob__title:focus-visible {
  outline-offset: 5px;
}

.ob__lead {
  margin-top: 0.5rem;
  max-width: 74ch;
  color: var(--text-dim);
  line-height: 1.55;
}

.ob__body {
  display: grid;
  gap: 0.9rem;
  margin-top: 1.1rem;
}

.ob__body > .hint {
  margin-top: 0;
}

.ob__error {
  padding: 0.5rem 0.7rem;
  border: 1px solid color-mix(in srgb, var(--lamp-red) 45%, transparent);
  border-radius: var(--radius);
  color: color-mix(in srgb, var(--lamp-red) 80%, var(--text));
  font-size: 0.875rem;
}

/* --- what each thing buys you --- */

.ob-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 0.75rem;
}

.ob-card {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.15rem 0.6rem;
  padding: 0.75rem 0.85rem;
  border: var(--hair);
  border-radius: var(--radius);
  background: rgb(255 255 255 / 1.5%);
}

.ob-card__mark {
  grid-row: 1 / span 2;
  color: var(--accent);
}

.ob-card__title {
  font-size: 0.9375rem;
  font-weight: 600;
}

.ob-card__list {
  display: grid;
  gap: 0.25rem;
  margin-top: 0.35rem;
  color: var(--text-dim);
  font-size: 0.8125rem;
  line-height: 1.45;
}

.ob-card__list > li {
  position: relative;
  padding-left: 0.85rem;
}

.ob-card__list > li::before {
  content: '—';
  position: absolute;
  left: 0;
  color: var(--text-faint);
}

/* --- the honest-facts list on the first screen --- */

.ob-facts {
  display: grid;
  gap: 0.6rem;
}

.ob-facts > li {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: 0.6rem;
  align-items: baseline;
  max-width: 82ch;
  line-height: 1.5;
}

/* An aside the user is meant to actually read, not a hint they can skim past. */
.ob-note {
  padding: 0.6rem 0.8rem;
  border-left: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
  background: rgb(255 255 255 / 2%);
  max-width: 82ch;
  font-size: 0.875rem;
  line-height: 1.5;
}

.ob-status {
  border: var(--hair);
  border-radius: var(--radius);
}

/* --- the PIN step --- */

.ob-pin {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem var(--gap);
}

.ob-pin__field {
  display: grid;
  gap: 0.25rem;
}

.ob__input {
  width: min(16rem, 100%);
}

.ob__ack {
  font-size: 0.8125rem;
  color: var(--text);
}

.ob__ack input {
  margin-top: 0.25rem;
  accent-color: var(--accent);
}

/* --- the pairing step --- */

.ob-pair {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--gap);
  align-items: start;
}

.ob-pair__qr {
  display: grid;
  justify-items: center;
  gap: 0.4rem;
  padding: 0.7rem;
  border: var(--hair);
  border-radius: var(--radius);
  /* QR codes need a light quiet zone to scan reliably; this is the one place in
     the app that is deliberately not dark. */
  background: #f4f7f7;
}

.ob-pair__qr > canvas,
.ob-pair__qr > svg {
  display: block;
  width: min(15rem, 100%);
  height: auto;
}

.ob-pair__qr > figcaption {
  color: #3d4a4e;
}

.ob-pair__text {
  display: grid;
  gap: 0.35rem;
  align-content: start;
}

.ob-pair__row {
  display: grid;
  gap: 0.1rem;
}

.ob-pair__code {
  font-size: var(--step-1);
  letter-spacing: 0.14em;
  color: var(--accent);
  overflow-wrap: anywhere;
}

/* --- footer --- */

.ob__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: 0.7rem 0.9rem;
  border-top: var(--hair);
  background: rgb(0 0 0 / 18%);
}

.ob__foot-left,
.ob__foot-right {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.ob__foot-right {
  margin-left: auto;
}

.ob__next[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

@media (width <= 720px) {
  .ob {
    padding: 0;
  }

  .ob__panel {
    max-height: none;
    min-height: 100%;
    border: 0;
    border-radius: 0;
  }

  .ob-pair {
    grid-template-columns: minmax(0, 1fr);
  }

  .ob-facts > li {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.1rem;
  }
}

@media (prefers-reduced-transparency: reduce) {
  .ob {
    background: var(--ink-900);
    backdrop-filter: none;
  }
}

/* The build stamp sits at the far end of the footer and stays out of the way —
   it is a diagnostic, not a status readout. */
.foot__item--build {
  margin-left: auto;
  opacity: 0.45;
  font-size: 0.625rem;
  letter-spacing: 0.06em;
}

.foot__item--build:hover {
  opacity: 0.9;
}

/* "Run setup" in the pre-flight header — an escape hatch back into the guided
   flow, quiet enough not to compete with the ARM button. */
.panel__setup {
  margin-left: auto;
  margin-right: 0.75rem;
  padding: 0.15rem 0.5rem;
  border: var(--hair);
  border-radius: var(--radius);
  background: none;
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
}

.panel__setup:hover {
  color: var(--text);
  border-color: var(--edge-hi);
}
