/* app.css — the whole stylesheet. The map is the page; everything floats on it except the AR view
   cards, which push the map aside. Colours are never written here: theme.js publishes every token
   as a --group-key custom property and this file reads them, so dark mode is a palette swap. */

/* The map is the page and everything floats on it, except the AR view cards, whose column
   PUSHES the map's right edge aside (--dock, the column's width). The 10 px inset is the desk. */
/* The page owns its box model. It rendered border-box for a year only because the vendored search
   box injected `* { box-sizing: border-box !important }` into the document; the 1.18 build scopes
   that rule to its own classes, and the Overlays's rows grew 12 px the day it was dropped in. */
*, *::before, *::after { box-sizing: border-box; }
/* `hidden` MEANS hidden: any `display` rule beats the browser's own [hidden], and a patch per class
   (nine of them) still missed .overlays-row — a filter hit sat over a blank 36 px row. One rule. */
[hidden] { display: none !important; }
/* clip, not hidden: a parked AR pane is translated past the screen's edge, and on a phone that overflow
   let the whole page be dragged and bounced. A clipped root still scrolls by CODE: body's `position` (below) is what
   leaves it nothing to scroll (Scars) */
html { overflow: clip; }
body {
  margin: 0;
  /* the shell's scale (--ui, below); zoom inflates dvh too, so the height is brought back */
  zoom: var(--ui); height: calc(100dvh / var(--ui));
  /* relative: the containing block of every absolute box under it (#ar-cards, its parked pane), so the clip below holds
     them. Without it they hung off the root, whose clip is the viewport's `hidden` — scrollable by code: a section's
     scrollIntoView lifted the phone's whole app 239 px over a black foot (2026-10-26) */
  position: relative;
  /* both from the palette: NIGHT swaps them together, and everything else inherits */
  /* clip, not hidden: a hidden box still scrolls for scrollIntoView, and focusing a sheet row (Tab, VoiceOver) pushed the
     phone's whole app up 213 px over a black band (the audit, 2026-10-22) */
  background: var(--surface-page); color: var(--text-ink); overflow: clip;
  font: 14px/20px var(--font);
  /* ONE UI FACE: the platform's own (SF on Apple, Segoe on Windows, Roboto on Android), as Primer, Atlassian, Polaris and
     Fluent ship — the generic sans-serif was Helvetica here and Arial on Windows. Sizes and lines: rules.test's TYPE */
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* CORNERS (rules.test): s a chip or key · m a control · l a panel or picture · xl a card or sheet · full a capsule;
     a corner inside a rimmed one is the outer less its inset */
  --radius-s: 4px; --radius-m: 8px; --radius-l: 12px; --radius-xl: 16px; --radius-full: 999px;
  --row-h: 24px; /* a track row's pitch: shell/overlaysPanel/rows.js places rows at multiples of it (44 under a thumb) */
  /* THE SHELL'S SCALE: 1 up to 1920, more on a 4K desk at 100 % (the breakpoints below). The
     FURNITURE zooms, never a picture: GL JS sizes a canvas off getBoundingClientRect, which
     zoom inflates (measured 2026-09-24: a 6659px canvas on a 3840 screen), so #map, the AR
     frames and the seam zoom back to 1 and every per-frame pixel written over them is real.
     Whatever a zoomed element shares with an unzoomed one (--dock, the viewport) is divided
     by --ui where it crosses; util/dom.js's uiScale() is the same division for a JS writer. */
  --ui: 1;
  /* the cards' width — the seam drag writes --fw inline. REGISTERED (@property below), so
     arview/ reads a resolved length back, never this calc */
  --fw: calc(380px * var(--ui));
  /* MOTION: seven clocks and two curves, and every transition and animation in this file reads
     one of them — a change of pace is one line and reduced motion is one block. --slide is a
     layout move (the cards, the Overlays, the map's edge); --fade opacity and colour; --tap hover
     and pressed; --reveal-open/--reveal-shut the pair M and N share; --ease brings a thing in,
     decelerating onto its mark and never past it, --ease-exit takes it out, accelerating away (no overshoot
     or wind-up on a working screen: rules.test, 2026-10-22); --handover the one deliberate slow fade, a pin handing
     over to the map's own symbol; --wake the instrument cluster setting into its dash, and 60% of
     it taking it back out. IN MS THROUGHOUT: dom.js's msOf() parses the number, and a
     `2s` would read as 2. */
  --slide: 280ms; --fade: 200ms; --tap: 130ms; --handover: 2000ms; --wake: 1000ms;
  --loader-spin: 1.1s; --loader-breathe: 1.8s; /* .loader's ring and its glyph (the map at boot, the camera card) */
  --reveal-open: 300ms; --reveal-shut: 190ms;
  --ease: cubic-bezier(.22, 1, .36, 1);
  --ease-exit: cubic-bezier(.32, 0, .67, 0);
  /* the column's height, from which a stacked card's WIDTH follows (.ar-card). Read INSIDE the
     zoomed card, so the viewport is brought back to the card's pixels */
  --dockh: calc((100dvh - 20px) / var(--ui));
  /* EVERYTHING UNDER A CARD'S PICTURE: the head (the 44px pill + its 6px margin; the command bar's 38px row
     went with it, 2026-10-07, and the 24px chips 2026-10-17). DECLARED HERE, not in the bottom-sheet block, so the coarse-pointer
     override below wins on order: the desk card, the phone sheet and arview/'s usableFw() all
     size the picture off this one number, and a row it forgets is a card that overflows. */
  --chrome-h: 50px;
  /* the Overlays's width; the tool row and the message stack start where it ends */
  --sidew: min(300px, calc((100vw - var(--dock, 0px)) / var(--ui) - 40px));
}
@property --fw { syntax: '<length>'; inherits: true; initial-value: 380px; }
/* a form control never inherits `color` — the UA hands it `buttontext`, which does not move
   with the palette; one rule, not a fix per control */
:is(button, input, select, textarea) { color: inherit; }
/* …and the UA's control box — ButtonFace and a 2px outset — reset once for every control that is a
   glyph on nothing. :where, so a control that paints its own plate wins on any rule of its own */
:where(#menu-btn, .ar-pin, .overlays-row button, button.overlays-row, .callout-x, .key-hint, .nato-q,
       .nato-sw, .nato-chev, .rewind-play, .rewind-shut, #performance button) { border: none; background: none; }
/* the surfaces WE do not draw (scrollbar, caret) follow the palette too */
body[data-light="night"] { color-scheme: dark; }
::selection { background: var(--fov-line); color: var(--text-on-dark); }
body.cards { --dock: calc(var(--fw) + 10px); }
/* narrow windows: the cards give ground first */
@media (max-width: 900px) { body { --fw: 280px; } }
/* wide desks at 100 %: the furniture grows, the map stays the map */
/* …and the AR column keeps the desk's share of the screen (380 of 1440): grown by --ui alone it was 665 of 3840, a
   660 px picture over 1,700 px of empty column (UX audit, 2026-10-22). A dragged width still wins (arview/, inline) */
@media (min-width: 2560px) { body { --ui: 1.35; --fw: max(calc(380px * var(--ui)), 26vw); } }
@media (min-width: 3600px) { body { --ui: 1.75; } }
/* THE PICTURES STAY AT 1 (see --ui): the map, every AR frame and the pop-out canvas, the seam
   beside them and the cockpit's two GL pods; the DOM that is furniture inside a picture — the
   card around its frame, Mapbox's controls, the promotion bar's pill and list, a details card,
   the instrument cluster, the corner readout — zooms again. */
#map, #ar-cards, #ar-seam, .ar-frame, .ar-cast { zoom: calc(1 / var(--ui)); }
.ar-card, .mapboxgl-control-container, .nato-pill, .callout, #cockpit, #performance { zoom: var(--ui); }
/* what APPEARS rather than slides — a new card, the drive pad, the promotion bar — fades in */
.ar-card, #drive-pad, .nato-bar { transition: opacity var(--fade); }
@starting-style { .ar-card, #drive-pad, .nato-bar { opacity: 0; } }
.nato-bar.leaving { opacity: 0; pointer-events: none; } /* and leaves as it came (nato.js finish): it vanished in one frame */
/* day ↔ night: the basemap sweeps its light over a second and the furniture used to snap.
   daynight.js wears `relight` on <body> for one --fade. NAMED surfaces, never `*`: a blanket
   transition would replace --slide on whatever is mid-toggle. */
body.relight, body.relight :is(#overlays, #toolbar button, .pill-button-bar, .search-box, .mapboxgl-ctrl-group, .mapboxgl-ctrl-scale, .co-card,
                                .ar-frame, .ar-head > *, .callout, .nato-pill, .rewind-bar,
                                .rewind-live, .cg, #performance) {
  transition: background-color var(--fade), color var(--fade), border-color var(--fade);
}
/* PUSHED by the cards: the right edge is the only thing that moves */
#map {
  position: absolute; top: 10px; bottom: 10px; left: 10px;
  right: calc(10px + var(--dock, 0px));
  /* its own stacking context: a full-bleed child (draw/'s surface) must not cover the controls */
  z-index: 0;
  /* the two PICTURES — this and every AR view frame — wear the same outline and radius */
  border: 2px solid var(--line-frame); border-radius: var(--radius-l);
  transition: right var(--slide) var(--ease);
}
/* The Overlays floats ON the map as a plate of the THICK glass (overlays.js: `.thick` while open — a panel of
   text wears the tint at 55 %, and theme.test holds every text token to AA on it over the brightest map); shut by default,
   the whole panel collapses to the ☰, a plate of the thin glass. Nothing paints the bare id: the glass rule is
   its plate (a cream on the id outranked `.glass` once, and the ☰ was a solid disc). */
#overlays {
  position: absolute; z-index: 3; border-radius: var(--radius-l);
  top: 20px; left: 20px;
  width: var(--sidew);
  /* stops short of the Performance readout in the bottom-left corner */
  max-height: calc(100% - 80px);
  display: flex; flex-direction: column;
  /* the box glides between the ☰ and the panel in step with the tool row beside it. The
     contents swap at once and the moving edge REVEALS them: #overlays-box keeps the open width
     throughout, so nothing inside reflows for the length of the slide */
  overflow: hidden;
  transition: width var(--slide) var(--ease), border-radius var(--slide) var(--ease), background-color var(--slide) var(--ease);
}
#overlays-box {
  /* the panel's width less its 2 px rim a side: at the full --sidew the box ran 4 px under the right rim, and the right
     inset read 4 px tighter than the left */
  flex: 1; min-height: 0; width: calc(var(--sidew) - 4px);
  /* the head and the filter stand still; only the list scrolls (overlays.js Scars) */
  padding: 8px 12px; display: flex; flex-direction: column; overflow: hidden;
  /* the sides were 10 until the 4 px grid's tie went tighter, to 8, and the rows met the rim: the looser step
     (the owner, 2026-10-22: "it used to be better before") */
  position: relative;
}
/* …and what stands above it (the head, the filter, the phone's Rewind row) keeps its own height: a flex column shrank them */
#overlays-box > :not(#overlays-list) { flex: none; }
/* the one scroller, and the positioning context overlaysPanel/ measures its row windows against */
#overlays-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; /* the end of the list is not the page's */
  position: relative; }
/* shut, nothing to clip and a focus ring to show: the ☰ fills the box. Its radius is half its height, never
   --radius-full: the open panel eases its corners from this, and 999 → 12 held it a tall capsule for the first 150 ms
   of the slide. While it FOLDS shut (.morph, overlays.js shutPanel) the box still clips and the title, the filter and
   the rows stay in, for the closing edges to cover; the fold eases the box's inset, the head's gap and the ☰'s size
   too, so nothing inside jumps when the classes flip (2026-10-22) */
#overlays.shut { width: 44px; border-radius: calc(44px / 2); }
#overlays.shut #overlays-box { flex: none; padding: 0; }
#overlays.shut #overlays-head { margin: 0; }
#overlays.shut:not(.morph), #overlays.shut:not(.morph) #overlays-box { overflow: visible; }
#overlays.shut:not(.morph) :is(#overlays-head h2, #track-filter, #overlays-list) { display: none; }
#overlays-head { display: flex; align-items: center; gap: 8px; font-weight: bold; margin-bottom: 8px; }
/* shut, the ☰ IS the 44px circle the toolbar lines up against; open, a 32px chip beside the title */
#menu-btn { cursor: pointer;
            width: 32px; height: 32px; padding: 0 4px;
            position: relative; border-radius: 50%; } /* the wash under the pointer is round, as the ☰ is (a square in the circle, the owner, 2026-10-17) */
#overlays.shut #menu-btn { width: 40px; height: 40px; } /* inside the PillButtonBar edge */
#menu-btn .icon { width: 20px; height: 20px; }
/* open, the layers glyph turns into a back chevron: pressing it leaves the panel (the owner, 2026-10-22) */
#overlays:not(.shut) #menu-btn .icon { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15.4 6.4 9.8 12l5.6 5.6-1.7 1.7L6.4 12l7.3-7.3z'/%3E%3C/svg%3E"); }
/* a feed is broken and the panel is shut: one dot on the only control still on screen */
#menu-btn.warn::after {
  content: ''; position: absolute; top: 2px; right: 0;
  width: 7px; height: 7px; border-radius: 50%; background: var(--text-danger);
}

/* The AR view cards: a COLUMN at the right edge the map is pushed aside for, slid in
   when it has something to show. `visibility`, not display — a display:none map measures
   0×0 and throws its tiles away; it flips at the END of the slide out (the 0s delay). */
#ar-cards {
  overflow-anchor: none; /* a new live card is prepended; the strip must not hold the old one */
  position: absolute; z-index: 3;
  /* #map's own 10px inset: the column stands BESIDE the map, tops on one line */
  top: 10px; right: 10px; bottom: 10px;
  width: var(--fw);
  display: flex; flex-direction: column; gap: 8px;
  /* a card with its Compute Graph open is taller than its share of the column; the column
     scrolls rather than running off the bottom of the map */
  overflow-y: auto;
  pointer-events: none; /* the column is a layout; only its cards take the pointer */
  overscroll-behavior: contain; /* a scroll that reaches the column's end stays in the column */
  /* where it stands when it is AWAY — off the right edge, or below a phone's screen (the 560px
     block): one spelling, so every rule that puts it away (H, the cluster, his eye) says the same */
  --away: translateX(calc(100% + 20px));
  transform: var(--away);
  visibility: hidden;
  transition: transform var(--slide) var(--ease), visibility 0s var(--slide);
}
/* the 10px gap between the two pictures IS the splitter: dragging it sets --fw (arview/);
   faint at rest, full under the pointer */
#ar-seam {
  position: absolute; z-index: 4; top: 10px; bottom: 10px;
  right: calc(var(--fw) + 10px); width: 10px;
  display: none; cursor: col-resize; touch-action: none;
  /* shown with the cards it fades in once their column has slid into place: it stood at its final x while the column
     was still on its way, a line ahead of the edge it belongs to (2026-10-22) */
  transition: right var(--slide) var(--ease), opacity var(--fade) var(--ease) var(--slide);
}
body.cards #ar-seam { display: block; }
@starting-style { body.cards #ar-seam { opacity: 0; } }
#ar-seam::after {
  content: ''; position: absolute; inset: 0 4px; border-radius: var(--radius-full);
  background: var(--line-frame); opacity: .2; transition: opacity var(--tap);
}
body.sizing #ar-seam::after { opacity: .55; }
/* mid-drag the edges follow the POINTER: no eased transition */
body.sizing :is(#map, #ar-seam, #toolbar) { transition: none; }
body.cards #ar-cards {
  transform: none; visibility: visible;
  transition: transform var(--slide) var(--ease), visibility 0s;
}
/* One card (arview/), up to four. A card is a 3:2 PICTURE plus its chrome — the head of chips,
   --chrome-h, taller under a thumb. Every card is the column's width, so the seam grows them all and the
   column scrolls to the ones that do not fit; the min() keeps one card no taller than the column.
   Right-aligned: what narrows is the gap to the map. */
.ar-card {
  pointer-events: auto;
  flex: none; align-self: flex-end;
  /* a QUERY CONTAINER: a card's width is the splitter's, which no media query can see;
     .ar-toggle asks it */
  container: arcard / inline-size;
  /* --aspect: the picture's own width over height — a land vehicle's HUD lens (card.js shape), else 3:2 */
  width: min(100%, calc((var(--dockh) - var(--chrome-h)) * var(--aspect, 1.5)));
  display: flex; flex-direction: column;
}
/* a card showing nothing takes no room — while there are others. The LAST card keeps its box
   as it empties, so the column slides out carrying it rather than an empty strip. */
body.cards .ar-card.idle { display: none; }
/* an OPEN Compute Graph is the one thing in a card with no natural height: the picture is
   3:2 and the head is fixed, so the card claims the column's free space and hands all of it
   to the graph. Nothing to grow into (a full column, a second card) and grow contributes
   zero, which is the old behaviour — the column scrolls. */
.ar-card:has(> .cg:not([hidden])) { flex: 1 1 auto; min-height: 0; }
/* the picture, FIRST in the card so its top edge is the map's; no panel around it */
/* the first second of a map is a bare frame while the style loads: one line, and it FADES off
   rather than vanishing under the first tiles (visibility follows, so a reader is not read a note
   nobody sees) */
.ar-frame::after {
  content: 'loading the map…'; position: absolute; inset: 0; z-index: 1; display: grid; place-content: center;
  font: bold 12px/16px var(--font); color: var(--text-soft); pointer-events: none;
  opacity: 0; visibility: hidden;
  transition: opacity var(--fade), visibility 0s var(--fade);
}
.ar-frame.loading::after { opacity: 1; visibility: visible; transition: opacity var(--fade); }

/* THE LOADER, one block for every picture that is not there yet — the map's first two seconds
   (index.html's markup BESIDE #map) and the camera card until its still lands (roadcams/camcard.js):
   a glyph breathing inside an endless progress ring. `<div class="loader icon-x" role="img"
   aria-label>` is the whole element; the glyph is any .icon-* (--i inherits into ::after). The HOST
   hides it, and it fades off the way the map's line does: #map by dropping `loading` on the
   style's first frame, .cam-frame by gaining `live`. */
.loader {
  position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; pointer-events: none;
  color: var(--text-ink);
  transition: opacity var(--fade), visibility 0s var(--fade);
}
.cam-frame .loader { color: var(--anno-blank-ink); } /* light on the void, in every light */
/* the map's loader is #map's next sibling wearing #map's own box (GL JS warns of any child in its
   container): the same offsets, the same column edge, the same zoom */
#map + .loader { top: 10px; bottom: 10px; left: 10px; right: calc(10px + var(--dock, 0px)); zoom: calc(1 / var(--ui)); }
/* --loader-run pauses the hidden ring: the map's stays in the DOM for the session, and an
   animation nobody sees still ticks the style every frame */
#map:not(.loading) + .loader, .cam-frame.live .loader { opacity: 0; visibility: hidden; --loader-run: paused; }

.loader::before {
  content: ''; grid-area: 1 / 1; width: 64px; height: 64px; border-radius: 50%;
  background: conic-gradient(currentColor 0 28%, transparent 28% 100%);
  mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));
  animation: loader-spin var(--loader-spin) linear infinite var(--loader-run, running);
}

.loader::after {
  content: ''; grid-area: 1 / 1; width: 30px; height: 30px;
  background: currentColor; mask: var(--i) center / contain no-repeat;
  animation: loader-breathe var(--loader-breathe) ease-in-out infinite var(--loader-run, running);
}

@keyframes loader-spin { to { transform: rotate(1turn); } }

@keyframes loader-breathe { 50% { transform: scale(1.12); } }
.ar-frame {
  flex: none; aspect-ratio: var(--aspect, 1.5);
  position: relative; overflow: hidden;
  background: var(--surface-panel);
  border: 2px solid var(--line-frame); border-radius: var(--radius-l);
}
.ar-map { position: absolute; inset: 0; }
.ar-frame > .parked { visibility: hidden; }
/* the pop-out window (arview/card/popout.js): the frame goes offscreen and THIS canvas — the exact
   bytes on the second screen — stands in its place, framed like the picture it is; its aspect
   is the window's, which is why a popped-out card is 16:9 and not 3:2 */
.ar-cast {
  flex: none; display: block; width: 100%;
  background: var(--surface-void);
  border: 2px solid var(--line-frame); border-radius: var(--radius-l);
}
/* the chrome sits UNDER the frame: the name left, the feed's extras, and the card's buttons as ONE
   PillButtonBar — the tool row's segmented pill (.seg, with #toolbar's rules below), the same bar on the
   desk, under a thumb and on a phone — at a fixed height so following never nudges the picture */
.ar-head {
  flex: none; display: flex; align-items: center; gap: 6px;
  min-height: 24px; margin-top: 6px; font-size: 12px; line-height: 16px; font-weight: bold;
  white-space: nowrap;
}
/* the name is not a control, so not a chip: the caption of the window above */
.ar-label { padding: 0; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* the 📌 (NOT .ar-pin: that is the teardrop inside the frame); disabled, the column is full */
.ar-keep:disabled { opacity: .35; cursor: default; }
/* the one chip left: a readout, not a control. Both ends ellipsise; colour is set per track by arview/ (transit lateness) */
.ar-info { margin-left: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
           padding: 2px 8px; border-radius: var(--radius-s); } /* its plate: the glass rule */
/* the readouts over the picture */
.ar-near {
  position: absolute; z-index: 2; font-weight: bold;
  pointer-events: none; white-space: nowrap;
}
/* nearby incidents (arview/card/incidents.js/: place(), then draw): the map's own pin per incident, moved to
   its projected point every frame; off-frame ones hold the edge with a chevron. No plate:
   a scrim box would turn markers back into a list */
.ar-near { inset: 0; }
/* under the sight (arview/card/sight/) the frame is a sensor picture: no HUD, pins, readouts, dials
   or minimap — the picture, and the night sight's boxes (.ar-box), alone (the owner, 2026-10-07) */
.ar-card[data-sight]:not([data-sight="off"]) :is(.ar-hud, .ar-markers, .ar-near, .ar-callout, .ar-minimap-box) { display: none; } /* the cluster hides itself (card.js), lent or not */
/* the PoI NATO frames over a card (arview/card/markers.js/): each at its projected point, its size by distance alone; under
   the pins, takes no pointer. The img's margins stand its bottom centre on the point, and the scale turns about it */
.ar-markers { position: absolute; inset: 0; z-index: 2; overflow: hidden; pointer-events: none; }
/* THE REROUTE'S ONE LINE (overlays/generic/waypoints/reroute.js): the tooltip's chip, centred over the picture above where the turn
   sign parks — off the line of sight, read in one glance, then gone; it eases in and out, it never blinks */
/* the navigation disc (parts/fleet/disc.js/): the selected car, navigating, zoomed out; disc.js tilts .ego-disc by the
   map's pitch and turns the chevron by course less bearing, per frame */
.ego { width: 36px; height: 36px; display: grid; place-items: center; pointer-events: none; }
.ego-disc { width: 32px; height: 32px; border-radius: 50%; background: var(--drawn-ego); box-shadow: 0 0 0 2px var(--drawn-ego-ink);
  display: grid; place-items: center; }
.ego-chevron { width: 16px; height: 16px; display: grid; }
.ego-chevron svg { width: 100%; height: 100%; fill: var(--drawn-ego-ink); }
.nav-notice { position: absolute; z-index: 3; left: 50%; top: 16%; padding: 4px 12px; border-radius: var(--radius-full);
  color: var(--text-ink); font-size: 12px; line-height: 16px; white-space: nowrap; pointer-events: none;
  opacity: 0; transform: translate(-50%, -4px); transition: opacity var(--fade) var(--ease-exit), transform var(--fade) var(--ease-exit); }
.nav-notice.on { opacity: 1; transform: translate(-50%, 0); transition: opacity var(--fade) var(--ease), transform var(--fade) var(--ease); }
.ar-marker { position: absolute; left: 0; top: 0; }
.ar-marker.behind { visibility: hidden; } /* behind a building: occluded, as everything in an AR view (markers.js isBehind) */
.ar-marker img { position: absolute; left: 0; top: 0; transform-origin: 50% 100%; }
/* the map's own name: its ink, no halo (poi/marker.js), on a backing so a nearer name (stacked over, markers.js) covers a
   farther one instead of the two inks crossing */
.ar-marker-name { position: absolute; left: 0; top: 3px; transform: translateX(-50%); white-space: nowrap; padding: 0 4px;
  font-size: 12px; line-height: 16px; color: var(--line-glyph); background: var(--surface-panel); border-radius: var(--radius-s); }
body[data-light="night"] .ar-marker-name { color: var(--text-on-dark); }
/* the row IS the pin's box; the margin stands the pin's TIP on the projected point */
.ar-near-row {
  position: absolute; left: 0; top: 0; width: 17px; height: 24px;
  margin: -24px 0 0 -8.5px; /* the anchor, applied here so arview/ writes translate() alone */
}
/* the teardrop at map scale; the only thing here that takes the pointer. The UA chrome is
   reset by the shared bare-control rule — left on, `ButtonFace` paints a grey plate over the
   picture and its border + padding squeeze the 17px teardrop to about 1px. */
.ar-pin {
  position: absolute; inset: 0; width: 17px; height: 24px; padding: 0;
  pointer-events: auto; cursor: pointer;
  /* the hand-over to the map's own symbol for the same call (arview/card/incidents.js adds .merged at the
     card's far plane): a long fade rather than a cut, because what it has to cover is the
     symbol's first placement pass — seconds of wall clock on a card idling at 1 fps */
  transition: opacity var(--handover) linear;
}
.ar-near-row.merged .ar-pin { opacity: 0; }
.ar-pin > svg {
  width: 100%; height: 100%; display: block;
  fill: var(--pin-fill); stroke: var(--line-glyph); stroke-width: 1.5;
  filter: drop-shadow(0 1px 2px var(--overlay-shadow));
}
/* the head disc is centred at (21,21) of 42×60 — 50% across, 35% down */
.ar-pin-glyph {
  position: absolute; left: 50%; top: 35%; width: 62%;
  transform: translate(-50%, -50%); line-height: 0;
}
.ar-pin-glyph > svg { width: 100%; height: auto; display: block; }
/* off-frame: the chevron points out of the panel, rotated about the pin's HEAD */
.ar-pin-chev {
  position: absolute; left: 8.5px; top: 8.4px; margin: -6px 0 0 -6px;
  width: 12px; height: 12px; line-height: 0;
  color: var(--pin-fill); filter: drop-shadow(0 0 3px var(--line-glyph));
}
/* the arrowhead is a path (incidents.js), so the glow is a filter and not a text-shadow */
.ar-pin-chev > svg { width: 100%; height: 100%; display: block; fill: currentColor; }
/* the card a clicked pin expands into (details/), placed by the AR view's own projection */
.ar-callout { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.ar-callout > div { position: absolute; left: 0; top: 0; pointer-events: auto; }
/* the map's card is sized for the map; this panel starts at ~330×240 and clips */
.ar-frame .callout { max-width: 200px; font-size: 12px; line-height: 16px; padding: 8px 28px 8px 12px; }
.ar-frame .callout b { font-size: 14px; line-height: 20px; }
.ar-frame .callout-glyph { width: 26px; height: 26px; margin-right: 8px; padding-right: 8px; }
.ar-label:empty, .ar-info:empty { display: none; }
/* display:flex above would otherwise beat :empty's display:none */
.ar-near:empty { display: none; }
/* the minimap's box (arview/minimap3d/), inert; minimap3d.js sizes it off its slot (the cluster's pod, the visor's), which
   the ARVIEW.minimap dials size on a card, so the minimap scales with the card */
.ar-minimap-box {
  position: absolute; z-index: 1; pointer-events: none;
  aspect-ratio: 1;
  overflow: visible; /* the N and its arrow stand OUTSIDE the rim */
  /* M grows and shrinks the minimap from its CENTRE (util/keys.js/). `gone` is the shrunk
     state on the way out, @starting-style below the same state on the way in; card.js gives the
     WebGL context back only once the shrink has run, or an empty box is what would animate.
     No transform-origin: 50% 50% IS the initial value, and the box is the minimap. */
  transition: transform var(--reveal-open) var(--ease), opacity var(--tap) linear;
}
/* out: the fade is delayed so it ENDS with the shrink. ONE way out for both on the springloaded pair M and N:
   the disc and the cluster's BRAKE word */
.ar-minimap-box.gone, .ck-brake.gone .ck-brake-word {
  transform: scale(.35); opacity: 0;
  transition: transform var(--reveal-shut) var(--ease-exit),
              opacity var(--tap) linear calc(var(--reveal-shut) - var(--tap));
}
/* in: a disc fills a slot and arrives whole (the cluster's pod, the visor's); out it shrinks, above */
/* slept: scrolled out of the mobile strip, or showing a camera */
.ar-minimap-box.parked { visibility: hidden; }

/* ── the 3D minimap (arview/minimap3d/): the same disc grown a third axis. `.ar-mini3d` is the RIG, half
   again as wide as the disc (minimap3d.js box()), so the hover buttons have air; the disc sits in its middle.
   The map and the readout are clipped at the rim (R/C of the half-width); the ink overlay is not (the N and its
   arrow stand outside) and clips what may cross the rim itself. At night the canvas and the ink ADD (plus-lighter):
   black paints as nothing, as on a see-through display */
.ar-mini3d, .m3-rig { pointer-events: none; }
.m3-disc { position: absolute; left: 16.667%; top: 16.667%; width: 66.667%; height: 66.667%; }
/* the GL canvas rounded to the rim by its box, not a clip-path: inside the visor's turned display (arview/eye.js) Chrome
   drew the canvas square under a clip-path. 0.3% in, so the ring's stroke covers the canvas edge and GL's zoom stays a
   hair OVER the dial's (streets-v8's footprints are a cliff at z15) */
.m3-map { position: absolute; inset: 0.3%; border-radius: 50%; overflow: hidden; background: none; }
.m3-map .mapboxgl-ctrl { display: none !important; }
.m3-ov, .m3-lab { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.m3-ov { overflow: visible; }
.m3-rig.additive .m3-map canvas { mix-blend-mode: plus-lighter; } /* the overlay is plain: the chevron's dark edge must paint */
.m3-ctl { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; transition: opacity var(--tap); overflow: visible; }
.m3-ctl.pin, .m3-ctl:focus-within { opacity: 1; } /* a tabbed-to button shows the bar as a hover does */
/* a thumb has no hover: always shown, the bar covered the middle of a phone's pane (the audit, 2026-10-22) — a tap on the disc
   shows it for a moment (disc.js `pin`), and while it is hidden its buttons take no tap: invisible yet hit-testable was the
   2026-10-13 review's bug, and a tap meant to show it must not act */
@media (hover: none) { .m3-disc { pointer-events: auto; } .m3-ctl:not(.pin) .btn { pointer-events: none; } }
.m3-ctl .btn { pointer-events: all; cursor: pointer; outline: none; }
.m3-ctl .btn:focus-visible .face { fill-opacity: .12; stroke: currentColor; stroke-width: 2px; } /* the wash and a 2 px ring in the bar's own ink: the bar has no plate to outline */
.m3-rig.parked { visibility: hidden; }

/* the HUD canvas (arview/hud.js) */
.ar-hud, .ar-box { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
/* the camera monitor (corners.js openCam → roadcams/monitor/): the <video>'s poster carries the still, so a
   still-only camera and a dead stream need no extra element */
.ar-video, .ar-yt {
  position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1;
  /* void: the absence of a picture, black in every light */
  border: 0; background: var(--surface-void);
}
.ar-video { object-fit: contain; }
/* the embeds must not look like YouTube: no hover, and .warmup blacks the frame out while
   the opening chrome fades (brightness, not opacity, so the backing stays painted) */
.ar-yt { pointer-events: none; transition: filter var(--fade); }
.ar-yt.warmup { filter: brightness(0); transition: none; }
/* basemap off: annotations on black */
.ar-frame.blank { background: var(--surface-void); }

/* The Compute Graph (arview/card/graph/graph.js): one column of pills, each labelled with the function
   that runs the step; green = on time, orange = slow or late, red = stopped, dashed = a path this
   map is not on. Furniture, so every colour is a token. */
.cg {
  /* it TAKES the column's leftover height (see .ar-card:has below), so it is a flex
     column itself and .cg-scroll fills it — no max-height guessing at the space left */
  flex: 1 1 auto; min-height: 120px; margin-top: 6px;
  display: flex; flex-direction: column;
  background: var(--overlay-veil);
  border: 1px solid var(--line-rule); border-radius: var(--radius-l);
  font: 12px/16px var(--font);
}
.cg-scroll {
  flex: 1 1 auto; min-height: 0; padding: 12px 8px;
  overflow: auto; overscroll-behavior: contain;
}
/* the flow. Every child but the first is joined to the one above by a centred stem and the
   CHEVRON at its foot, drawn at the stem's own weight: a filled triangle is 8px across its flat
   top and leaves a shoulder either side of a 1.5px line (Scars). */
.cg-flow { display: flex; flex-direction: column; align-items: center; gap: 16px; }
.cg-flow > * + *, .cg-engine > * + *, .cg-kids > * + * { position: relative; }
.cg-flow > * + *::before, .cg-engine > * + *::before, .cg-kids > * + *::before {
  content: ''; position: absolute; left: calc(50% - 0.75px); top: -16px; height: 11px;
  border-left: 1.5px solid var(--line-strong);
}
.cg-flow > * + *::after, .cg-engine > * + *::after, .cg-kids > * + *::after {
  content: ''; position: absolute; left: calc(50% - 3px); top: -8px; width: 6px; height: 6px;
  border-right: 1.5px solid var(--line-strong);
  border-bottom: 1.5px solid var(--line-strong);
  border-bottom-right-radius: 0; transform: rotate(45deg);
}

/* A NODE is one pill, and everything that pill calls when it is open. SHRINKWRAPPED — a pill is
   as wide as its own name, so the shape of the graph is information and not the column's width. */
.cg-node {
  --cg-tone: var(--text-on);
  display: flex; flex-direction: column; align-items: center;
  width: max-content; max-width: 100%;
}
.cg-pill {
  --cg-tone: var(--text-on);
  display: flex; align-items: center; justify-content: center; gap: 6px;
  width: max-content; max-width: 100%;
  padding: 4px 12px; border-radius: var(--radius-full);
  font: inherit; text-align: center; color: var(--text-ink);
  border: 1px solid color-mix(in srgb, var(--cg-tone) 55%, transparent);
  background: color-mix(in srgb, var(--cg-tone) 16%, transparent);
}
.cg-ok { --cg-tone: var(--text-on); }        /* running, and on time */
.cg-stale { --cg-tone: var(--text-warn); }    /* later or slower than usual */
.cg-down { --cg-tone: var(--text-danger); }   /* it has stopped working */
/* a path the code has that this map is not on */
.cg-pill.off {
  background: none; border-style: dashed; border-color: var(--line-border);
  color: var(--text-faint);
}
.cg-pill.cg-fold { cursor: pointer; }
/* the chevron says the pill opens. A count badge said how MANY were inside, which is the one
   question a reader never has — they open it to see what, not how many. */
.cg-c {
  flex: none; width: 7px; height: 7px; margin-left: -1px;
  border-right: 1.5px solid color-mix(in srgb, var(--cg-tone) 85%, var(--text-ink));
  border-bottom: 1.5px solid color-mix(in srgb, var(--cg-tone) 85%, var(--text-ink));
  transform: rotate(-45deg); transition: transform var(--reveal-open) var(--ease);
}
.cg-node[data-open] > .cg-pill > .cg-c { transform: rotate(45deg); }

/* GROWN: the pill becomes the head of a box and what it calls is drawn inside it. Recursive —
   these three rules are the whole of the nesting, at every depth. */
.cg-node[data-open] {
  border: 1px solid color-mix(in srgb, var(--cg-tone) 42%, transparent);
  border-radius: var(--radius-xl);
  background: color-mix(in srgb, var(--cg-tone) 6%, transparent);
}
.cg-node[data-open] > .cg-pill {
  /* width:auto, or the max-content above wins and the head stops short of its own box */
  align-self: stretch; width: auto;
  border: none; border-radius: calc(var(--radius-xl) - 1px) calc(var(--radius-xl) - 1px) 0 0;
  border-bottom: 1px solid color-mix(in srgb, var(--cg-tone) 26%, transparent);
  background: color-mix(in srgb, var(--cg-tone) 22%, transparent);
}
.cg-kids {
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  width: max-content; max-width: 100%; padding: 12px 12px;
}
/* only the node just clicked unfolds (graph.js marks it): a click redraws the flow, and every open node's kids replayed
   this; its chevron turns from where it stood, the element being new (2026-10-22) */
.cg-kids.unfold { animation: cg-unfold var(--reveal-open) var(--ease) both; }
.cg-node[data-turned="open"] > .cg-pill > .cg-c { animation: cg-turn-open var(--reveal-open) var(--ease); }
.cg-node[data-turned="shut"] > .cg-pill > .cg-c { animation: cg-turn-shut var(--reveal-open) var(--ease); }
@keyframes cg-turn-open { from { transform: rotate(-45deg); } }
@keyframes cg-turn-shut { from { transform: rotate(45deg); } }
@keyframes cg-unfold {
  from { opacity: 0; clip-path: inset(0 0 100% 0); }
  to { opacity: 1; clip-path: inset(0 0 0 0); }
}

/* a FORK is a SEGMENTED CONTROL: both arms in one pill, the one this picture is on filled. Drawn
   as two pills under an elbow it read as a branch the picture takes BOTH ways. */
.cg-segs {
  display: flex; align-items: stretch; width: max-content; max-width: 100%;
  border-radius: var(--radius-full); overflow: hidden; background: var(--overlay-veil);
  border: 1px solid color-mix(in srgb, var(--line-strong) 45%, transparent);
}
.cg-seg {
  --cg-tone: var(--text-on);
  display: flex; align-items: center; padding: 4px 12px;
  white-space: nowrap; color: var(--text-faint);
}
.cg-seg + .cg-seg { border-left: 1px solid color-mix(in srgb, var(--line-strong) 35%, transparent); }
.cg-seg.on { background: color-mix(in srgb, var(--cg-tone) 24%, transparent); color: var(--text-ink); }
/* the question goes UNDER the control: over it, the stem arriving from the node before would land
   on a caption instead of on the thing that answers it */
.cg-why { margin-top: 4px; font-size: 12px; line-height: 16px; color: var(--text-faint); text-align: center; }

/* the engine's half, above the browser's: same pills, same colours; the connector leaving
   the hairline box IS the WebSocket */
.cg-engine {
  align-self: stretch; display: flex; flex-direction: column; align-items: center; gap: 16px;
  padding: 8px 8px 12px; border: 1px dashed var(--line-border); border-radius: var(--radius-l);
}
.cg-band {
  align-self: flex-start; font-size: 12px; line-height: 16px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-faint);
}
/* the caption is a label, not a stage: nothing flows INTO the first module from it */
.cg-engine > .cg-band + *::before, .cg-engine > .cg-band + *::after { display: none; }

/* the one starting point: the upstreams behind this picture, curving into a single line */
.cg-feeds { align-self: stretch; }
.cg-feed-row {
  display: flex; flex-wrap: wrap; gap: 8px;
  justify-content: center; align-items: flex-start; /* one line is one `top` — see fitFan */
}
/* the merge elbow: ╰──╯ under what it collects. The split it was mirrored from is gone — a fork
   is a segmented control now, and this is the only curve left. */
.cg-join {
  height: 13px; margin: 0 16%;
  border: 1.5px solid var(--line-strong); border-top: none; border-radius: 0 0 var(--radius-l) var(--radius-l);
}
/* a brace over one pill is no brace: the plain stem to the next pill stands in (graph.js fitFan) */
.cg-join.one { display: none; }

/* TWO CLOCKS OFF ONE PASS (graph.js CLOCKS `beside`): the camera and the fan-out are both driven
   by the advance pass and neither waits for the other — nose to tail they read as a chain. The
   elbows are .cg-join's curve, one of them flipped. The fork SHRINKWRAPS like every other node,
   and its legs are equal 1fr halves of a max-content grid, so a leg's axis is a clean 25%/75%
   and the legs land on it by arithmetic — fitFan() stays on the feeds row, where the pills wrap
   and the centres really are unequal. */
.cg-par { display: flex; flex-direction: column; align-items: stretch;
  width: max-content; max-width: 100%; }
.cg-par-out, .cg-par-in { height: 13px; margin: 0 25%; border: 1.5px solid var(--line-strong); }
.cg-par-out { border-bottom: none; border-radius: var(--radius-l) var(--radius-l) 0 0; }
.cg-par-in { border-top: none; border-radius: 0 0 var(--radius-l) var(--radius-l); }
.cg-legs { display: grid; grid-template-columns: repeat(2, 1fr);
  width: max-content; max-width: 100%; }
.cg-leg {
  position: relative; min-width: 0;
  display: flex; flex-direction: column; align-items: center;
  padding: 16px 8px 0;
}
/* the arrowhead belongs at the END of each leg, entering its own leg: one at the crown would
   point at nothing */
.cg-leg::before {
  content: ''; position: absolute; left: calc(50% - 3px); top: -1px; width: 6px; height: 6px;
  border-right: 1.5px solid var(--line-strong);
  border-bottom: 1.5px solid var(--line-strong);
  border-bottom-right-radius: 0; transform: rotate(45deg);
}
.cg-leg > .cg-node { max-width: 100%; }
/* the shorter leg must still REACH the merge, or its branch stops in mid-air above it */
.cg-tail { flex: 1 1 auto; align-self: center; width: 1.5px; min-height: 13px;
  background: var(--line-strong); }
/* a leg narrower than the pill in it is worse than no split. Under this they stack — still with
   no connector between them, because they are still not a chain. */
@container arcard (max-width: 330px) {
  .cg-par-out, .cg-par-in, .cg-leg::before, .cg-tail { display: none; }
  .cg-legs { grid-template-columns: 1fr; }
  .cg-leg { padding: 0 0 16px; }
}

/* the search is the LAST item of the tool row (index.html): a 44px circle like every tool, and
   a pill while focused — :focus-within is the whole mechanism. Open, it takes what the row has
   left over, so no width of anything beside it can be overlapped */
#searchbar { flex: 0 0 44px; min-width: 0; transition: flex-basis var(--slide) var(--ease); }
#searchbar:focus-within { flex: 0 1 420px; }
#searchbar input { height: 40px; } /* inside the PillButtonBar edge */
/* the span index.html leaves in the circle: only a NARROW row shows it (the container block below) */
#searchbar > .icon { display: none; }
/* collapsed, nothing but the glyph shows: font-size 0 hides the placeholder and any typed
   text in one property — once the capsule has folded (the delay), so its closing edge covers the text; it dropped
   in the blur frame while the capsule took the slide to close (2026-10-22) */
#searchbar:not(:focus-within) input { padding: 0; font-size: 0; transition: font-size 0s var(--slide), padding 0s var(--slide); }
#searchbar input { color: var(--text-ink); }
/* the placeholder in meta (a fixed grey read 2.9:1 at night, 4.2:1 by day — failed at both ends) */
#searchbar input::placeholder { color: var(--text-meta); opacity: 1; }
/* shell/search/ builds the pill: .search-box (the capsule) > .icon + input.search-q + ul.search-list.
   The capsule: a PillButtonBar's plate and edge (above), 44 = 40 + the 2px edge, radius 22 so a shut row
   shows a circle; positioned, so the list hangs off it */
.search-box { position: relative; height: 44px; border-radius: var(--radius-full); }
/* the magnifier over the field's left end at every width (11 + 19 + 11 ≈ the shut circle); .icon's
   own `margin: auto` centres it. Pointer-events off: an icon over the input once ate a tap on the
   circle's centre */
.search-box > .icon { position: absolute; inset: 0 auto 0 11px; color: var(--text-meta); pointer-events: none; }
#searchbar:not(:focus-within) .search-box > .icon { color: var(--text-ink); }
/* the field is TRANSPARENT in the capsule; ink, placeholder and height are the #searchbar input rules */
.search-q { width: 100%; padding: 0 16px 0 40px; border: none; background: none;
            font: 14px/1 var(--font); caret-color: var(--text-ink); }
/* NO RING: a text input matches :focus-visible on a CLICK too, so any outline here boxes every
   opened pill. The opening is the indication, and the rim goes to ink while the field holds focus
   (`.glass:focus-within`, the nato pill's too). The id outranks the global input rule, which is later in the file */
#searchbar .search-q:focus-visible { outline: none; }
/* the suggestions: a panel floating under the capsule at its width — hung from BODY and placed there by search.js,
   because a glass plate INSIDE the glass pill blurs nothing (a backdrop filter sees only its own backdrop root, and
   the pill is one): inside the pill the list showed the sharp map through its tint beside a frosted Overlays
   (2026-10-17). Over everything in the shell, on the tooltip's layer */
.search-list { position: fixed; z-index: 40; margin: 0; padding: 6px 0; list-style: none;
               border-radius: var(--radius-xl); overflow: hidden; } /* its plate: the thick glass (search.js) */
/* block, not flex: the name ellipsises; the place prints under it in meta */
.search-opt { padding: 8px 16px; cursor: pointer; font: 14px/20px var(--font); color: var(--text-ink);
              white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-opt i { display: block; margin-top: 2px; font: 12px/16px var(--font); font-style: normal;
                color: var(--text-meta); }
.search-opt.cur { background: var(--highlight-wash); } /* the current row: the highlight's wash, .nato-hit.cur's */
.search-none { padding: 8px 16px; font: 14px/20px var(--font); color: var(--text-faint); cursor: default; } /* the one row a query with no hit draws */
/* the map tools, ON THE MAP: behind a shut Overlays they were controls nobody could reach. One
   horizontal rule: right of the ☰ (or of the open panel), left of the map's top-right control
   stack, which rides the map's edge and so the cards — a real width, brought into the zoomed row.
   A query container: Rewind wraps by the ROW's width (a card open narrows it as much
   as a phone does) */
/* ── PillButtonBar: THE look of a bar of buttons floating on the map — the shell's glass in a capsule, a 2px
   edge at 55% ink, flat (the owner's pick off the dial, 2026-10-10) UNTIL 2026-10-17, when the owner put the
   LIQUID GLASS on every one of them (below: the plate rule IS the glass). A row; `.vertical` a column; `.seg` a segmented control
   (below, with the tool row). util/pill/pill.js is the one builder (a card's bar, the phone's pill; GL JS's
   control groups are dressed by it); the tool row's pill is the same bar as markup (index.html). As one-button
   bars with the same plate and edge: the ☰'s shut panel, Rewind's button and the search capsule. The buttons size
   themselves (44px in the row, GL JS's 32px in the stack); the bar owns nothing but the plate. The 3D minimap's
   bar (minimap3d/disc.js paintCtl) is the same recipe drawn in SVG, scaled with its disc and without the plate ── */
/* ── GLASS: the LIQUID plate (theme/glass; the owner's pick of four prototypes, 2026-10-17, then "do all 3": the tool
   row, the control stack and the card's bar, then "universally": every control floating on the map or over a picture;
   measured free of frame cost on the desk — within noise of no plate, and a FULL-SCREEN blur cost +0.04 ms, Chromium
   blurring a downsample). ONE class, put on by ONE function — theme/glass's glass(node): pillBar() calls it for every
   PillButtonBar (the tool row's seg, GL JS's groups, a card's bar, the phone's pill), and each one-off plate's builder
   calls it on the node it built (the ☰'s shut panel, Rewind's circle and bar, the search capsule, the card's readout
   chip, the camera card's buttons and note, the drive pad's slots and stick, the phone's ⊕ and ✕, the Splats verb;
   NOT the scale bar, a ruler). `.thick` (glass(node, { thick: true })) is the same plate under a PANEL OF TEXT — the open Overlays,
   the Performance readout, a details card, the F1 callouts, the search list, the tooltip, the boot banner — its tint
   surface.panel at 55 % (iOS's regular material), so meta text keeps near the contrast measured on the panel. A plate
   never sits INSIDE another plate: a backdrop filter inside a translucent plate sees only that plate's own paint and
   the compositor blends its copy OVER it, so the tint doubles and the map is not in it (the search list hangs from
   body for that; a chip inside a bar is a tint alone, no blur). `.readout`
   (glass(node, { readout: true })) is a plate with NO rim — a figure that only informs. A plate
   is for a CONTROL, a PANEL or a READOUT; a caption over the picture (a name, a key hint's cap and verb) wears a halo, never
   a plate: glass on the desk's key hint was a 2px box hugging its cap (the owner, 2026-10-17: "this border should not
   appear anywhere"). No plate is named here. A thin tint over a strong blur, the 2px edge every plate wore before the
   glass, in the unselected glyph's grey (overlay.rim: a 1px white rim vanished on a day map; the owner asked the old
   width back), a lip of light inside it — the refraction is four inset shadows, nothing drawn. The two values sit on
   BODY, where daynight writes the night tokens, for the plates that exist only in CSS (the phone's tool fan, a ::before
   dot, the mirror toast). `.shine` is the light under the pointer: a child moved by transform (glass.js), shown
   while hovered, under the content (isolate + z -1); its wearer is positioned and clips its own overflow. ── */
body { --glass-filter: blur(28px) saturate(1.9);
       --glass-thick: color-mix(in srgb, var(--surface-panel) 55%, transparent); /* a panel of text: iOS's REGULAR material — 85 % read as a solid pale slab beside the pills (the owner, 2026-10-17) */
       --glass-lip-side: color-mix(in srgb, var(--overlay-lip) 40%, transparent);
       --glass-shadow: inset 0 1px 1px var(--overlay-lip), inset 0 -1px 1px var(--glass-lip-side),
                       inset 1px 0 1px var(--glass-lip-side), inset -1px 0 1px var(--glass-lip-side),
                       0 10px 30px -10px var(--overlay-shadow);
       /* THE HIGHLIGHT's two washes (theme.js line.highlight): under its 2px edge when selected (the SELECTED rule by the
          tool row) or pressed, alone on hover — the app's one look for all three (the owner, 2026-10-24) */
       --highlight-wash: color-mix(in srgb, var(--line-highlight) 20%, transparent);
       --highlight-hover: color-mix(in srgb, var(--line-highlight) 10%, transparent);
       --highlight-inset: 5px; } /* how far a selected capsule stands inside its control's 2px rim: the AoI board's 30px ring in a 44px bar */
.glass { isolation: isolate; background: var(--overlay-glass);
         -webkit-backdrop-filter: var(--glass-filter); backdrop-filter: var(--glass-filter);
         border: 2px solid var(--overlay-rim); box-shadow: var(--glass-shadow); }
.glass.thick { background: var(--glass-thick); }
/* a READOUT — the shut Performance row, the scale bar, a card's info chip, a note, a scrub tip, the tooltip — informs and
   is not pressed: the glass with no rim (the border stays 2px and transparent, so nothing moves when a rim comes on —
   the Performance row's does as its drawer opens). The rim is for a control or a panel (the owner, 2026-10-17) */
.glass.readout { border-color: transparent; }
/* a field held inside a plate (search, the promotion bar) says so by its rim going to FULL ink while it holds focus:
   the rim is the glyph's grey, the focus the glyph's black (the search pill's pattern since before the glass) */
.glass:focus-within { border-color: var(--text-ink); }
.glass > .shine { position: absolute; z-index: -1; left: -80px; top: -40px; width: 160px; height: 80px; border-radius: 50%;
                  pointer-events: none; will-change: transform; opacity: 0; transition: opacity var(--fade);
                  background: radial-gradient(closest-side, color-mix(in srgb, var(--overlay-lip) 50%, transparent), transparent); }
.pill-button-bar { display: flex; overflow: hidden; border-radius: var(--radius-full); color: var(--text-ink); }
.pill-button-bar.vertical { flex-direction: column; }
/* the FANCY BUTTON: a capsule verb that stands out inside a panel — the highlight's 20 % wash for a fill under its 2px
   edge, the pill bar's 2px (line.highlight: the night cyan, a deep cyan by day). A button INSIDE a panel, not a plate (a
   plate in a plate doubles the tint). `.drag` / a press fills it with ink (the toolbar's armed inversion) */
.fancy-button {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 36px; padding: 0 12px; font: inherit; font-weight: 600; cursor: pointer;
  color: var(--text-ink); border-radius: var(--radius-full); border: 2px solid var(--line-highlight);
  background: var(--highlight-wash);
  transition: background var(--tap), color var(--tap);
}
.fancy-button .icon { width: 16px; height: 16px; margin: 0; }
.fancy-button:disabled { color: var(--text-faint); cursor: default; }
.fancy-button.drag, .fancy-button:active:not(:disabled) { background: var(--text-ink); color: var(--surface-panel); }
/* NO dividers between a bar's buttons (the owner, 2026-10-17: "no need for separator lines anymore" — they stood
   from 2026-10-11 to this day, a hairline 9px short of each edge): the white capsule says which one is on, the
   glyphs' spacing says where one ends, as on iOS 26's glass bars. GL JS's control groups draw their own #ddd
   hairline between buttons; off here, for the stack */
.mapboxgl-ctrl-group button + button { border-top: 0; }

#toolbar {
  position: absolute; z-index: 3; top: 20px;
  left: 74px; /* 20 + the ☰'s 44px square + 10 gap */
  right: calc(var(--dock, 0px) / var(--ui) + 70px);
  /* both edges glide: the right one rides the cards, and the search pill between them with it */
  transition: left var(--slide) var(--ease), right var(--slide) var(--ease);
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center; /* wrap: the Rewind bar takes a line of its own under 700 px (the query below), not the card column (2026-10-21) */
  container-type: inline-size;
}
/* the row's OWN buttons: a bare `#toolbar button` once caught a button inside the search pill
   and blew it up into a 44px tile hanging off the pill's end */
:where(#toolbar > button, .seg > button, #rewind .rewind-open) { border: none; } /* the UA's */
#toolbar > button, .seg > button, #rewind .rewind-open {
  flex: none; /* the pill beside them absorbs the row's slack, never the circles */
  width: 44px; height: 44px; cursor: pointer;
  border-radius: 50%; color: var(--text-ink);
}
/* armed: text.ink as a FILL, so the pair inverts with the palette */
/* .seg: a PillButtonBar as a SEGMENTED CONTROL — 40px segments inside its 2px edge, so the pill stands 44 like
   the circles beside it. Two wear it: the tool row's five arming tools (index.html) and a card's buttons under
   its picture (arview/card/chrome.js — the one bar on the desk, under a thumb and on a phone), whose buttons say
   pressed three ways: a toggle's data-on, the glasses' and the 📌's aria-pressed, the Compute Graph's aria-expanded */
.seg { flex: none; }
.seg > button { background: none; height: 40px; border-radius: 0; } /* 40 inside the 2px edge = the circles' 44 */
/* GREYED: aria-disabled, never `disabled`, so a hover still reaches its tip saying what it waits for (the nav tool's
   "Select a drivable vehicle", overlays/generic/waypoints/placing.js) — faint, and no wash under the pointer or the press */
.seg > button[aria-disabled="true"] { color: var(--text-faint); cursor: default; box-shadow: none; }
/* THE NAV TOOL SAYS ITS STEP while waypoints go down (placing.js `data-say`): "Place waypoints" armed, "Start navigation"
   once a stop is down — its segment widens to hold the words beside the glyph (the pick follows: theme/glass slidePick
   watches the row's size) and is the glyph again as the placing ends. A phone's fan prints the same words as its name */
/* THE SELECTED VEHICLE'S BAR (index.html #vehicle-slot): shut, the slot is no width at all (the row's 8px gap given back
   too) and the bar in it nothing; a selection (app.js `.on`) opens the slot to the bar's width (--vehicle-w, app.js keeps
   it, so the nav's words widen it too) — the clock and the search pill beside it glide over on --ease, never jumping —
   while the bar grows in from its middle, easing onto its size. Shutting runs it backwards on --ease-exit, the slot closing
   with it. No overshoot (the owner, 2026-10-25: a bounce was "a little too distracting"). The bar keeps its own width all
   along, overflowing a narrower slot, so its buttons never squeeze */
#vehicle-slot { display: flex; width: 0; margin-left: -8px;
  transition: width var(--reveal-shut) var(--ease-exit), margin-left var(--reveal-shut) var(--ease-exit); }
#vehicle-slot.on { width: var(--vehicle-w, auto); margin-left: 0;
  transition: width var(--reveal-open) var(--ease), margin-left var(--reveal-open) var(--ease); }
#vehicle-bar { flex: none; transform: scale(.2); opacity: 0;
  transition: transform var(--reveal-shut) var(--ease-exit), opacity var(--reveal-shut) var(--ease-exit); }
#vehicle-slot.on #vehicle-bar { transform: none; opacity: 1;
  transition: transform var(--reveal-open) var(--ease), opacity var(--fade) var(--ease); }
#nav-btn { display: flex; align-items: center; justify-content: center; }
#nav-btn > .say { display: none; }
#nav-btn[data-say] { width: auto; gap: 8px; padding: 0 16px 0 12px; }
#nav-btn[data-say] > .icon { margin: 0; }
#nav-btn[data-say] > .say { display: block; font: 600 14px/1 var(--font); white-space: nowrap; }
/* ── SELECTED, on the glass: THE HIGHLIGHT (the owner, 2026-10-24: "standardize to the whole app, so that it's uniform
   everywhere"): its 2px edge over its 20 % wash, inset --highlight-inset in the segment or the circle, the glyph staying ink — the
   deep cyan by day, the night cyan after dark (theme.js line.highlight). Not an inversion (an ink slab was "too dark,
   in your face", 2026-10-17); the lifted white capsule that followed it went for this one look (Scars). ONE rule
   for every control on the map that is ON: a segment (its three ways of saying so), Rewind's open circle, a speed
   in Rewind's bar, HQ and Task force on the promotion bar, the drive pad's slot, the phone's ⊕ while its fan is
   out. The capsule is a ::before under the glyph (isolate + z -1). A new on/off control joins this list; it
   paints no fill of its own (rules.test.js holds the sheet to that). The narrow row's pressed search circle is the
   same white disc, written where its container query lives. The capsule never moves its host: a static one gets
   `position: relative` at zero specificity, one that places itself (the ⊕ is fixed) keeps its place (Scars) ── */
:is(.seg:not(:has(> .seg-pick)) > :is([aria-pressed="true"], [data-on="1"], [aria-expanded="true"]), #rewind-open[aria-expanded="true"],
    :where(body.phone #toolbar) .seg > :is([aria-pressed="true"], [data-on="1"], [aria-expanded="true"]),
    .nato-tog[aria-pressed="true"], .slot button.on, body.tools #phone-plus) {
  isolation: isolate; color: var(--text-ink);
  :where(&) { position: relative; }
  &::before { content: ''; position: absolute; inset: var(--highlight-inset); z-index: -1; border-radius: var(--radius-full);
              background: var(--highlight-wash); box-shadow: inset 0 0 0 2px var(--line-highlight); }
}
/* …except where ONE of a bar is on (the tool row): there the capsule is one element that SLIDES to the armed tool
   (util/pill slidePick; the owner's pick, 2026-10-22), where each pressed button wore its own and the capsule jumped.
   The same highlight; it fades where it stood when nothing is armed, appears in place when one is (`snap`) */
.seg:has(> .seg-pick) { position: relative; }
.seg:has(> .seg-pick) > button { position: relative; z-index: 1; }
.seg-pick { position: absolute; top: var(--highlight-inset); bottom: var(--highlight-inset); left: 0; border-radius: var(--radius-full); pointer-events: none;
            /* as far in on every side as SELECTED's ::before: slidePick sizes it to the button, so its sides are a transparent
               border its fill and edge stay inside (it touched the bar's inner edge sideways, 3px top and bottom: 2026-10-24) */
            background: var(--highlight-wash); box-shadow: inset 0 0 0 2px var(--line-highlight); opacity: 0;
            border-inline: var(--highlight-inset) solid transparent; background-clip: padding-box; /* after `background`: its shorthand resets the clip */
            transition: transform var(--slide) var(--ease), width var(--slide) var(--ease), opacity var(--fade) var(--ease); }
.seg-pick.on { opacity: 1; }
.seg-pick.snap { transition: opacity var(--fade) var(--ease); }
/* The icon system: ONE way a control draws its glyph — a silhouette in `--i`, masked,
   painted with currentColor, so it inherits the palette, the pressed inversion and
   :disabled. No emoji: an emoji font paints its own colour and stays black after dark. */
.icon {
  display: block; width: 20px; height: 20px; margin: auto;
  background: currentColor;
  mask: var(--i) center / contain no-repeat;
}
.icon-pin { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 2a7 7 0 0 0-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 0 0-7-7zm0 9.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z'/%3E%3C/svg%3E"); }
.icon-poly { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 1.5 22.5 9.1 18.5 21.5h-13L1.5 9.1zM12 5.2 5 10.2l2.7 8.3h8.6l2.7-8.3z'/%3E%3C/svg%3E"); }
.icon-soldier { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.6a2.4 2.4 0 1 1 0 4.8 2.4 2.4 0 0 1 0-4.8zM9.1 7.4h5.8l3.4 2.1-1 1.7-2.4-1.5V13l2.6 8.6h-2.3L13.4 15h-2.8L8.8 21.6H6.5L9.1 13V9.7l-2.4 1.5-1-1.7z'/%3E%3C/svg%3E"); }
.icon-pen { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E"); }
/* route: a start dot, the road along and up, the destination pin (the nav tool, overlays/generic/waypoints/) */
.icon-route { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='6' cy='19' r='2.8'/%3E%3Cpath fill-rule='evenodd' d='M18 2.5a4 4 0 0 0-4 4c0 3 4 7.5 4 7.5s4-4.5 4-7.5a4 4 0 0 0-4-4zm0 2.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3z'/%3E%3Cpath d='M9.4 19H15a3 3 0 0 0 3-3v-.5' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E"); }
.icon-search { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M9.8 2.5a7.3 7.3 0 0 1 5.8 11.7l5.9 5.9-1.6 1.6-5.9-5.9A7.3 7.3 0 1 1 9.8 2.5zm0 2.3a5 5 0 1 0 0 10 5 5 0 0 0 0-10z'/%3E%3C/svg%3E"); }
.icon-clock { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 2.2a7.8 7.8 0 1 1 0 15.6 7.8 7.8 0 0 1 0-15.6zM10.9 6.6h2.2v5.9l4.1 2.4-1.1 1.9-5.2-3z'/%3E%3C/svg%3E"); }
.icon-play { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 4.5 19.5 12 7 19.5z'/%3E%3C/svg%3E"); }
.icon-pause { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6.5 4.5h4v15h-4zM13.5 4.5h4v15h-4z'/%3E%3C/svg%3E"); }
/* the loaders' glyphs (.loader::after): LINE drawings on a 48 box — at 30 px a filled silhouette is a blot */
.icon-map { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='none' stroke='%23000' stroke-width='2.6' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M6 12l12-4 12 4 12-4v28l-12 4-12-4-12 4zM18 8v28M30 12v28'/%3E%3C/svg%3E"); }
.icon-camera { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='none' stroke='%23000' stroke-width='2.6' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M9 16h7l3-4h10l3 4h7a3 3 0 0 1 3 3v16a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V19a3 3 0 0 1 3-3z'/%3E%3Ccircle cx='24' cy='27' r='6'/%3E%3C/svg%3E"); }
.icon-live { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 9a3 3 0 1 1 0 6 3 3 0 0 1 0-6zM6.9 5.2 8.4 6.8a7.2 7.2 0 0 0 0 10.4l-1.5 1.6a9.4 9.4 0 0 1 0-13.6zm10.2 0a9.4 9.4 0 0 1 0 13.6l-1.5-1.6a7.2 7.2 0 0 0 0-10.4z'/%3E%3C/svg%3E"); }
.icon-copy { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M8 3h11a2 2 0 0 1 2 2v11h-2V5H8zM4 7h11a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2zm0 2v10h11V9z'/%3E%3C/svg%3E"); }
.icon-chev { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6.4 8.6 12 14.2l5.6-5.6 1.7 1.7L12 17.6 4.7 10.3z'/%3E%3C/svg%3E"); }
/* layers: a sheet over two edges, the Overlays' stack — the ☰ read as a generic menu */
.icon-layers { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M12 2 21 7 12 12 3 7zM4.6 11.1 3 12l9 5 9-5-1.6-.9M4.6 16.1 3 17l9 5 9-5-1.6-.9'/%3E%3C/svg%3E"); }
/* keep: a bookmark, not a push-pin — .icon-pin is already the PoI */
.icon-keep { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5.5 2h13v20.6L12 16.9 5.5 22.6z'/%3E%3C/svg%3E"); }
.icon-close { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.4 17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z'/%3E%3C/svg%3E"); }
/* pop out: a frame open at its top-right corner, with the arrow leaving through it */
/* the glasses: the AR view sent to a pair of them, or to a second screen (card/popout/) */
.icon-compass { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 1.8a10.2 10.2 0 1 0 0 20.4 10.2 10.2 0 0 0 0-20.4zm0 2.1a8.1 8.1 0 1 1 0 16.2 8.1 8.1 0 0 1 0-16.2zM12 5l2.5 7-2.5 7-2.5-7z'/%3E%3C/svg%3E"); }
.icon-glasses { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M1.5 8h8.2a1.3 1.3 0 0 1 1.3 1.3V10h2v-.7A1.3 1.3 0 0 1 14.3 8h8.2v2.2h-1.2v3.3a3.5 3.5 0 0 1-3.5 3.5h-1.6a3.5 3.5 0 0 1-3.5-3.5V12h-1.4v1.5a3.5 3.5 0 0 1-3.5 3.5H6.2a3.5 3.5 0 0 1-3.5-3.5v-3.3H1.5z'/%3E%3C/svg%3E"); }
/* the phone's pill and ⊕ (shell/mapcontrols/phone.js): where am I and the tools */
.icon-locate { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.5a1 1 0 0 1 1 1V5a7 7 0 0 1 6 6h1.5a1 1 0 1 1 0 2H19a7 7 0 0 1-6 6v1.5a1 1 0 1 1-2 0V19a7 7 0 0 1-6-6H3.5a1 1 0 1 1 0-2H5a7 7 0 0 1 6-6V3.5a1 1 0 0 1 1-1zm0 4.5a5 5 0 1 0 0 10 5 5 0 0 0 0-10zm0 2.6a2.4 2.4 0 1 1 0 4.8 2.4 2.4 0 0 1 0-4.8z'/%3E%3C/svg%3E"); }
.icon-folder { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M2.5 4.5A1.5 1.5 0 0 1 4 3h5.2a1.5 1.5 0 0 1 1.06.44L11.8 5H20a1.5 1.5 0 0 1 1.5 1.5v13A1.5 1.5 0 0 1 20 21H4a1.5 1.5 0 0 1-1.5-1.5zM4.9 9.4v9.2h14.2V9.4z'/%3E%3C/svg%3E"); }
.icon-plus { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10.8 4h2.4v6.8H20v2.4h-6.8V20h-2.4v-6.8H4v-2.4h6.8z'/%3E%3C/svg%3E"); }
.icon-help { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.8 8.6a4.2 4.2 0 1 1 6.2 3.7c-1.3.7-2 1.5-2 3v.5M12 20h.01'/%3E%3C/svg%3E"); }
.icon-sun { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10zm-1-6h2v3.6h-2zm0 19.4h2V24h-2zM0 11h3.6v2H0zm20.4 0H24v2h-3.6zM3.5 4.9 4.9 3.5l2.5 2.5L6 7.4zm13.1 13.1 1.4-1.4 2.5 2.5-1.4 1.4zM19.1 3.5l1.4 1.4-2.5 2.5-1.4-1.4zM3.5 19.1 6 16.6l1.4 1.4-2.5 2.5z'/%3E%3C/svg%3E"); }
.icon-moon { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a9 9 0 1 0 9 9 7 7 0 1 1-9-9z'/%3E%3C/svg%3E"); }
.icon-link { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'/%3E%3C/svg%3E"); }
/* full screen (the phone sheet's button, phone.js): four corners out, and in */
.icon-full { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 10V4h6v2H6v4zm10-6h6v6h-2V6h-4zM4 14h2v4h4v2H4zm14 0h2v6h-6v-2h4z'/%3E%3C/svg%3E"); }
.icon-shrink { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 4v6H4V8h4V4zm4 0h2v4h4v2h-6zM4 14h6v6H8v-4H4zm10 0h6v2h-4v4h-2z'/%3E%3C/svg%3E"); }
/* the two AR view toggles on a narrow card: a folded map, and the disc with its ownship */
.icon-basemap { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M2 6l6-2.5v14L2 20zM9 3.5L15 6v14l-6-2.5zM16 6l6-2.5v14l-6 2.5z'/%3E%3C/svg%3E"); }
/* the sight (T): a lens ring with its spot and the span bar's ticks either side */
.icon-sight { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zm0 2.2a5.8 5.8 0 1 1 0 11.6 5.8 5.8 0 0 1 0-11.6zM12 9.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM1 11h3v2H1zm19 0h3v2h-3z'/%3E%3C/svg%3E"); }
/* look ahead from the aircraft: an arrow over the horizon */
.icon-forward { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.5 18.5 11h-4v4.5h-5V11h-4zM3 17.6h18V20H3z'/%3E%3C/svg%3E"); }
.icon-minimap { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 2.2a7.8 7.8 0 1 1 0 15.6 7.8 7.8 0 0 1 0-15.6zm0 3.3-4 9.2 4-2 4 2z'/%3E%3C/svg%3E"); }
/* the compute graph: one node branching into two — a flowchart, not a chart */
.icon-graph { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.5 2h7v5h-7zM11 7h2v3h-2zM3 10h18v1.6H3zM5 11.6h1.8V14H5zM17.2 11.6H19V14h-1.8zM2 14h7.6v5H2zM14.4 14H22v5h-7.6z'/%3E%3C/svg%3E"); }
/* gnss/'s GNSS fix: a satellite, its two panels and its body, sending */
.icon-accuracy { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 5a7 7 0 1 1 0 14 7 7 0 0 1 0-14zm0 2a5 5 0 1 0 0 10 5 5 0 0 0 0-10z'/%3E%3Cpath d='M11 2h2v7h-2zm0 13h2v7h-2zM2 11h7v2H2zm13 0h7v2h-7z'/%3E%3C/svg%3E"); } /* gnss/'s fix accuracy: a crosshair through a ring, the middle open (an ellipse round a dot read as an eye, the Overlays' show/hide; a ring, not an oval: the owner, 2026-10-25) */
.icon-gnss { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3.5 7.3 7.3 3.5l3.5 3.5L7 10.8zM13.2 17l3.8-3.8 3.5 3.5-3.8 3.8zM8.6 12 12 8.6l3.4 3.4-3.4 3.4zM2 14h2a6 6 0 0 0 6 6v2a8 8 0 0 1-8-8zm3.6 0h2a2.4 2.4 0 0 0 2.4 2.4v2A4.4 4.4 0 0 1 5.6 14z'/%3E%3C/svg%3E"); }
/* applies on reload: a circular arrow, for the dials GL JS took at construction */
.icon-reload { --i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17.65 6.35A8 8 0 1 0 19.73 14h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z'/%3E%3C/svg%3E"); }
/* each tool's pointer is what it is about to put down (Google Earth's Pegman): poi/ aoi/ soldier/
   bake --cursor-<mode> from the theme at setup, because a cursor image cannot read a var */
body[data-mode="poi"] .mapboxgl-canvas-container.mapboxgl-interactive {
  cursor: var(--cursor-poi, crosshair); }
body[data-mode="aoi"] .mapboxgl-canvas-container.mapboxgl-interactive {
  cursor: var(--cursor-aoi, crosshair); }
body[data-mode="soldier"] .mapboxgl-canvas-container.mapboxgl-interactive {
  cursor: var(--cursor-soldier, crosshair); }
body[data-mode="nav"] .mapboxgl-canvas-container.mapboxgl-interactive { cursor: crosshair; } /* a stop wants the exact pixel */
/* a symbol or a pin under the pointer (tooltip.js probes): it answers a click. A class, never an inline
   style, so the armed tools' crosshair above is never shadowed */
.mapboxgl-canvas.over-symbol { cursor: pointer; }
/* an area under the pointer (overlays/generic/aoi/corners.js): a click selects it; its corner a press drags */
.mapboxgl-canvas.over-area { cursor: pointer; }
.mapboxgl-canvas.over-corner { cursor: grab; }
.mapboxgl-canvas.dragging-corner { cursor: grabbing; }
/* the Overlays open: the row slides right of it (20 + its width + 12) */
body:has(#overlays:not(.shut)) #toolbar { left: calc(32px + var(--sidew)); }
.draw-surface { position: absolute; inset: 0; z-index: 4; display: none;
               cursor: var(--cursor-draw, crosshair); touch-action: none; }
body.drawing .draw-surface { display: block; }
/* Mapbox's control groups wear the PillButtonBar (mapcontrols.js adds the class: a white slab was the
   brightest object on a night map). What is GL JS's own stays here: its grey halo off, its buttons' type and
   ink, and `order` to place the where-am-I button — which GL JS builds LATE and so appends LAST — above the compass */
.mapboxgl-ctrl-group:not(:empty) { box-shadow: var(--glass-shadow); } /* GL JS's own ring is 0-2-0 and would win */
.mapboxgl-ctrl-group button { font: 16px/1 var(--font); color: var(--text-ink); }
.mapboxgl-ctrl-group .mapboxgl-ctrl-compass { order: 1; }
/* a real compass: dark north, salmon-red south (GL JS bakes it grey; its night variants outrank a lone class) */
.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23fa7268'/%3E%3C/svg%3E"); }
/* GL JS's own hover is 5 % black, invisible at night — and at (0,3,1) it REPLACED the armed fill
   on a hovered button (measured 2026-09-24: the pressed pan button went transparent under the
   pointer). The wash is the shared one's, in the ink, and the fill outranks it. `:not(:disabled)`
   on both is what puts them level with GL JS's rule; ours loads later and wins. */
.mapboxgl-ctrl-group button:not(:disabled):active {
  background-color: color-mix(in srgb, var(--text-ink) 20%, transparent); }
/* no inversion: nothing in the stack is a toggle — a press acts, and only a glyph may change
   (mapcontrols.js Scars, the owner 2026-10-10) */
/* Mapbox adds a blue box-shadow on focus: the app's ring is enough */
.mapboxgl-ctrl-group button:focus, .mapboxgl-ctrl-group button:focus:focus-visible { box-shadow: none; }
/* the ring INSIDE the capsule's buttons: drawn 2 px outside, the neighbours covered it and a hairline was all a Tab showed */
.mapboxgl-ctrl-group button:focus-visible { outline-offset: -2px; }
.mapboxgl-ctrl-group .icon { width: 16px; height: 16px; margin: 6px auto; }
/* Mapbox's own nav glyphs are baked images: inverted after dark instead — the compass with its
   hue turned back, or its red needle inverts to cyan */
.mapboxgl-ctrl-icon { transition: filter var(--fade); } /* …and cross over with the palette, as the chips do */
body[data-light="night"] .mapboxgl-ctrl-icon { filter: invert(1); }
body[data-light="night"] .mapboxgl-ctrl-compass .mapboxgl-ctrl-icon { filter: invert(1) hue-rotate(180deg); }
/* mid-chase two fingers are the drone's radius (mapcam/), and GL JS's own touch handlers are
   stood down — which drops the touch-action its classes carried, and the browser would take the
   pinch for the page */
body.chase .mapboxgl-canvas-container { touch-action: none; }
/* the satellites' own canvas over the map's (satellites/layer.js): it paints between the map's frames */
.sat-overlay { position: absolute; left: 0; top: 0; pointer-events: none; }
/* gnss/: the satellites' names over the map, a 2D canvas as above */
.gnss-overlay { position: absolute; left: 0; top: 0; pointer-events: none; }
/* touch targets: 44px squares under `pointer: coarse` (the PRIMARY input, so a trackpad laptop
   stays byte-identical); grid, so the glyph centres itself at any size */
/* the scale bar from the palette: Mapbox's fixed #333 on a white wash read nothing at night. A RULER, not a plate:
   GL JS's open bracket is the measure, and it stays as GL JS draws it (glassed for an hour on 2026-10-17; the owner:
   "this entity is supposed to be a map ruler") */
.mapboxgl-ctrl-scale {
  background-color: var(--overlay-scale);
  color: var(--text-ink); border-color: var(--text-ink);
  font-size: 12px; line-height: 16px; /* GL JS ships 10: under the type floor (12) */
}
/* THE F1 CALLOUTS (shell/mapcontrols/callouts.js): a scrim, and beside each thing a card of the keys and
   gestures that act on it now, on a dotted ink leader — black by day, white at night — ending in the ink
   and no head. One grid per card: the caps centred in a column as wide as the widest cap, the text
   centred beside them, a row ONE line — its words after the title, wrapping only when long (the owner, 2026-10-13) */
#callouts { position: fixed; inset: 0; z-index: 60; pointer-events: none;
            font: 12px/16px var(--font); color: var(--text-ink); }
#callouts * { box-sizing: border-box; }
body.callouts .key-hints { visibility: hidden; } /* the pills say the same: one voice while the cards are up */
.co-scrim { position: absolute; inset: 0; background: var(--overlay-backdrop); pointer-events: auto; animation: co-fade var(--fade) both; }
@keyframes co-fade { from { opacity: 0; } }
.co-leaders { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.co-leaders path { fill: none; stroke: var(--text-ink); stroke-width: 2; stroke-dasharray: 0 5; stroke-linecap: round; stroke-linejoin: round; }
.co-card { position: absolute; left: 0; top: 0; max-width: 340px; display: grid; grid-template-columns: auto 1fr; gap: 8px 8px;
           align-items: center; padding: 8px 12px; border-radius: var(--radius-l); color: var(--text-ink); /* its plate: the thick glass (callouts.js) */
           pointer-events: auto; animation: co-pop var(--fade) var(--ease) both; }
@keyframes co-pop { from { opacity: 0; scale: .9; } }
.co-card.stay { animation: none; } /* redrawn in place (callouts.js): it is already there */
/* LEAVING (callouts.js hide): out the way they came in, on --tap and --ease-exit — an exit is shorter than its entrance —
   and the DOM goes only after it; F1 off dropped every card and the scrim in one frame (2026-10-22) */
#callouts.leaving .co-card { animation: co-out var(--tap) var(--ease-exit) both; }
@keyframes co-out { to { opacity: 0; scale: .9; } }
@keyframes co-fade-out { to { opacity: 0; } }
#callouts.leaving :is(.co-scrim, svg) { animation: co-fade-out var(--tap) var(--ease-exit) both; } /* the full-screen leaders fade, they do not shrink */
.co-row { display: contents; } /* the rows share the card's two columns */
.co-head { grid-column: 1 / -1; margin-bottom: -2px; font-size: 12px; line-height: 16px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--text-meta); }
.co-caps { display: inline-flex; align-items: center; gap: 2px; justify-self: center; }
.co-caps kbd.wide { padding: 0 8px 2px; }
.co-what { min-width: 0; line-height: 16px; }
.co-say { font-weight: 700; white-space: nowrap; }
.co-more { color: var(--text-soft); font-weight: 400; }
.co-plus { font-weight: 700; margin: 0 1px; }
/* a mouse cap: the body, the button that acts filled, the gesture's word */
.co-mouse { gap: 2px; padding: 0 4px 2px; }
.co-mouse svg rect, .co-mouse svg path { fill: none; stroke: var(--hud-key-rim); stroke-width: 1.5; }
.co-mouse svg .f { fill: var(--hud-key-rim); stroke: none; }
.co-mouse span { font-size: 12px; line-height: 16px; }
/* a pressed key lights its cap for a moment (callouts.js LIT_MS): the face goes to the rim's ink */
.co-caps kbd { transition: background var(--tap), color var(--tap); }
.co-caps kbd.lit { background: var(--hud-key-rim); color: var(--hud-key-face); }
#overlays-list {
  details { border-bottom: 1px solid var(--line-rule); }
  /* the section headers scroll WITH their rows (sticky from 2026-09 to 2026-10-17: 33 rows of Vessels pushed every
     other feed off screen). On the glass they cannot stay: a pinned header needs a backdrop to cover the rows
     passing under it, and a backdrop — a tint or a blur — INSIDE a translucent plate doubles the plate's tint (the
     compositor blends the filtered copy over the original; only an opaque backdrop, the map under the panel itself,
     is replaced), so every header was a band in a second colour, twice over (the owner, 2026-10-17). */
  /* the right gutter for the count and the show button: reserved width, not floats */
  /* ONE ROW, three kinds — a section's summary, a bare row, a track: a 16 px LEAD column (the fold's triangle, a
     track's tone dot, or nothing), the label, the number, the eye, all in flow. Every label at a level stands on one
     x and every number ends on one edge, 4 px short of the eye (Carbon's tree, Primer's trailing column). The native
     marker was 14.8 px of a font's width, so a bare row's label sat 14.8 left of its summary sibling's, a track's 5.8;
     the summary's count stood absolute at right 26 and slid under a 44 px eye on a phone (2026-10-22) */
  :is(summary, .overlays-row) { display: flex; align-items: center; justify-content: flex-start; min-height: var(--row-h); }
  summary { padding: 4px 0; font-weight: bold; cursor: pointer; list-style: none; }
  summary::-webkit-details-marker { display: none; }
  :is(summary, .overlays-row)::before { content: ''; flex: none; width: 16px; height: 8px; }
  summary::before {
    background: currentColor; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0 7 4 1.5 8z'/%3E%3C/svg%3E") 0 50% / 8px 8px no-repeat;
    transition: rotate var(--tap) var(--ease); transform-origin: 4px 50%; /* the triangle's own centre, open AND shut */
  }
  details[open] > summary::before { rotate: 90deg; }
  button.overlays-row::before { width: 8px; height: 8px; margin-right: 8px; border-radius: 50%; background: var(--tone, transparent); }
  :is(summary, .overlays-row) > .show { order: 1; }
  :is(summary, .overlays-row) > .count, :is(summary, .overlays-row):not(:has(> .count)) > .show { margin-left: auto; }
  :is(summary, .overlays-row) > .count { padding: 0 4px 0 8px; }
  /* the show button: one masked eye silhouette; a 24px box (WCAG 2.5.8) around a 16px glyph */
  .show {
    width: 24px; height: 24px; padding: 0;
    border: none; cursor: pointer; background: var(--text-soft);
    --eye: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 5C6.5 5 2.3 9.7 1 12c1.3 2.3 5.5 7 11 7s9.7-4.7 11-7c-1.3-2.3-5.5-7-11-7zm0 10.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7z'/%3E%3C/svg%3E");
    mask: var(--eye) center / 16px 16px no-repeat;
  }
  /* hidden: the slash is a third evenodd subpath punched through the eye; .65 clears 3:1 on the glass over a road (theme.test) */
  .show.hidden {
    opacity: .65;
    --eye: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M12 5C6.5 5 2.3 9.7 1 12c1.3 2.3 5.5 7 11 7s9.7-4.7 11-7c-1.3-2.3-5.5-7-11-7zm0 10.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7zM3.1 1.7 22.3 20.9l-1.4 1.4L1.7 3.1z'/%3E%3C/svg%3E");
  }
  details > div { padding: 2px 0 6px 16px; }
  /* A SECTION EASES OPEN AND SHUT (the owner's pick, 2026-10-22): its content's height from 0 to its own — `auto`, which
     interpolate-size lets a transition reach — and back, kept rendered until it has folded (content-visibility, discrete),
     where it opened and shut in one frame inside a panel that folds. A browser without ::details-content or
     interpolate-size (Firefox; Safari's height) opens it at once, as before */
  interpolate-size: allow-keywords;
  details::details-content { height: 0; overflow: clip;
    transition: height var(--slide) var(--ease), content-visibility var(--slide) allow-discrete; }
  details[open]::details-content { height: auto; }
  /* an empty list (satellites/ with no filter hits) is not a padded blank strip under its heading. Never the windowed
     .row-list: hidden while still unfilled, its offsetTop read 0, overlaysPanel/'s window started past its end and it was never
     filled — a section opened below the fold showed its count and no rows (2026-10-26); at --n 0 it is 0 tall anyway */
  details > div:empty:not(.row-list) { display: none; }
  /* nested sections: plain rows, not bold bordered sections */
  details details { border-bottom: 0; }
  details details > summary { font-weight: normal; padding: 0; }
  /* the in-view count. Not a flex summary: that loses the disclosure triangle */
  .count {
    color: var(--text-meta); font-size: 12px; line-height: 16px; font-weight: normal; font-variant-numeric: tabular-nums;
  }
  /* a counted row with a show button: the number stands against the eye, not in the middle of the row */
  /* every row stands on one pitch, --row-h (the flex rule above): the eye in flow sets it, so a row needs no padding of
     its own. 14px/1.4 made Roads 23.6 and the eye made Traffic lights 28 (the owner, 2026-10-22) */
  .overlays-row { padding-block: 0; }
  /* never answered AND the last poll threw: '!' with the reason as its title */
  .count.warn { color: var(--text-danger); font-weight: bold; }
  /* answered once and gone quiet: the count stays, greyed, the age on the tooltip */
  .count.stale { color: var(--text-faint); font-style: italic; }
  .count.none { color: var(--text-faint); } /* the world's count: none of it is in view */
  /* out of coverage, or parked by its eye (overlaysPanel/): nothing is fetched, and the row fades whole —
     one muted line says it where six '—' did not */
  :is(details.off > summary, .overlays-row.off) { color: var(--text-faint); }
  /* windowed rows (overlaysPanel/): the list reserves `--n` rows of height and the visible slice is
     placed absolutely inside it, at the pitch --row-h (body) that overlaysPanel/'s rowH() reads. */
  .row-list {
    contain: content; position: relative;
    height: calc(var(--n, 0) * var(--row-h));
  }
  .row-list > .overlays-row { position: absolute; left: 0; right: 0; height: var(--row-h); }
  /* the "nothing in view" line is not a windowed row */
  .row-list > .overlays-row.empty { position: static; height: auto; }
  /* an empty list reserves 0 rows: its one line of explanation was clipped under the next
     sticky header, so every "no coverage" / "waiting for first data" went unread */
  .row-list:has(.empty) { height: auto; }
  /* "Show all" (overlaysPanel/): a control over the list, not a row in it — it sits outside .row-list
     so the windowing arithmetic never sees it, and wears no tone bar so it cannot be mistaken
     for a track. Underlined because it is the one row that does not fly the map somewhere. */
  /* a link in the panel is a 24px target (WCAG 2.5.8): NEPTUN's is a licence term, not a courtesy */
  a { display: inline-flex; align-items: center; min-height: 24px; }
  /* 24px tall (WCAG 2.5.8): the one control that changes what the LIST IS */
  button.overlays-row.more {
    font-size: 12px; line-height: 16px; color: var(--text-meta);
    min-height: 24px; display: flex; align-items: center; text-decoration: underline;
  }
  /* the track list and its Show all hang off the <details> itself, beside the rows' box: the box's indent, so a track's
     label starts where its siblings' do (a margin: the rows are placed absolutely, from the list's padding edge) */
  details > .row-list { margin-left: 16px; }
  details > button.overlays-row.more { padding-left: 16px; }
  :is(button.overlays-row, summary, .show):focus-visible {
    outline: 2px solid var(--text-ink); outline-offset: -2px;
  }
  /* the Splats section's one verb (overlays/generic/splats/): a fancy button the width of the list; it fills while a folder is
     dragged over the map or over it — the drop lands on either */
  .splat-load { width: 100%; margin: 6px 0 8px; }
}

/* display:flex below would beat [hidden]: an empty note row held a blank line open */
.overlays-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 2px 0;
  cursor: pointer;
}

.overlays-row button { /* the delete: the same 24px target as every other verb */
  min-width: 24px; min-height: 24px; padding: 0;
  cursor: pointer;
  color: var(--text-danger);
}

.overlays-row button .icon { width: 12px; height: 12px; }

/* a meta shrinks and ellipsises like a name: held whole, a splat's "105 tiles · 3.8 M splats · all of it · 170.4 MB/s" cut
   its name to nothing and put the ✕ past the panel (the owner, 2026-10-24: "unable to delete splat") */
.overlays-row .meta { color: var(--text-meta); font-size: 12px; line-height: 16px; margin-left: auto; padding: 0 4px 0 8px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* a track row has no eye: its meta ends where a count does, 4 px short of the eye's column */
button.overlays-row > .meta { padding-right: 28px; }

/* with nothing followed the waypoints list is a bare hint (a text node, not a counted row) */
:is(#waypoints-list, #splats-list):not(:has(.overlays-row)) { color: var(--text-faint); font-size: 12px; line-height: 16px; }

/* the other three self-filled lists say so too instead of an 8px blank */
/* a PoI row is two lines, M3's dense two-line item: the name over its coordinates, the ✕ beside both (36 = 20 + 16).
   On one line the coordinates held their width and cut 14 of 17 names to "Command L…"; before that a name wrapped into
   its numbers (2026-10-22). A waypoint's row too: its road and minutes squeezed "WP 1" to nothing and pushed its ✕ off
   the panel (the owner, 2026-10-24: "No WP label … can't delete"). Then a splat's, the same evening: EVERY item (userlist
   row: PoI, waypoint, AoI, splat) is the two-line row, one rule, not a list at a time */
#overlays-list .overlays-row.item { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; }
#overlays-list .overlays-row.item::before { grid-area: 1 / 1 / 3 / 2; }
#overlays-list .overlays-row.item > .lbl { grid-area: 1 / 2; }
#overlays-list .overlays-row.item > .meta { grid-area: 2 / 2; margin: 0; padding: 0; }
#overlays-list .overlays-row.item > button { grid-area: 1 / 3 / 3 / 4; align-self: center; }

:is(#poi-list, #aoi-list, #draw-list):empty::before {
  content: 'nothing drawn yet'; color: var(--text-faint); font-size: 12px; line-height: 16px; }

.overlays-row [contenteditable="true"] { outline: 1px solid var(--text-dim); padding: 0 2px; }

/* the lists features fill themselves (variable height): content-visibility skips the ones
   scrolled out of the panel, contain-intrinsic-size keeps the scrollbar steady */
#settings-list, #poi-list, #aoi-list, #waypoints-list, #draw-list {
  content-visibility: auto;
  contain-intrinsic-size: auto 200px;
}

#aoi-list details { border-bottom: 0; }

#aoi-list summary { font-weight: normal; padding: 0; }

/* ---- track lists (overlaysPanel/) ---- */
:is(#track-filter, #dial-filter) {
  width: 100%; margin-bottom: 8px;
  padding: 4px 8px; font: inherit; font-size: 12px; line-height: 16px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-s); background: var(--surface-card);
}

/* long names ellipsise; min-width:0 is what lets a flex item shrink below its content */
.overlays-row .lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.overlays-row.empty { color: var(--text-faint); font-size: 12px; line-height: 16px; cursor: default; }

/* track rows are <button>s (overlaysPanel/), keyboard-reachable */
button.overlays-row {
  width: 100%; margin: 0;
  font: inherit; color: inherit; text-align: left;
  /* the row wears its pin's fill as the dot in its lead column (#overlays-list); a row with no pin keeps the column */
  padding-inline: 0;
}

.mapboxgl-ctrl-logo { display: none !important; }
/* the callout (details/): a clicked pin morphs into this card, growing from the pin's own
   anchor; transform + opacity only, so compositor work and no layout */
.callout {
  position: relative;
  display: flex; align-items: center;
  /* --dx (details/) steps a card back inside its container; the grow still runs from the pin */
  left: var(--dx, 0px); transform-origin: calc(50% - var(--dx, 0px)) 100%;
  max-width: min(340px, calc(100vw - 40px)); padding: 12px 32px 12px 12px;
  color: var(--text-ink); /* its plate: the thick glass (details.js) */
  font: 14px/20px var(--font);
  /* collapsed = the pin's head (details/ measures both and sets the vars) */
  opacity: 0; border-radius: 50%;
  transform: translateY(var(--ty, -20px)) scale(var(--sx, .16), var(--sy, .6));
  /* CLOSING (this rule's transition): the shrink on --ease — a container folding into its own pin eases like its
     opening, it is not leaving the screen (--ease-exit held it big, then the marker went at 280 ms mid-shrink) — and the
     plate fading over its last --tap: it faded in 130 ms from the first frame, gone before the shrink could be seen
     (2026-10-22). OPENING (.open's): the plate at once, the words after it (below) */
  transition: transform var(--slide) var(--ease), border-radius var(--slide) var(--ease),
              opacity var(--tap) var(--ease-exit) calc(var(--slide) - var(--tap));
  will-change: transform, opacity;
  &.open { opacity: 1; transform: none; border-radius: var(--radius-xl);
           transition: transform var(--slide) var(--ease), border-radius var(--slide) var(--ease), opacity var(--tap) var(--ease); }
  &.open .callout-body, &.open .callout-glyph, &.open .callout-x {
    opacity: 1; transition-delay: calc(var(--slide) - var(--tap));
  }
  b { display: block; font-size: 16px; line-height: 24px; margin-bottom: 2px;
               overflow: hidden; text-overflow: ellipsis; }
  img { display: block; max-width: 100%; border-radius: var(--radius-m); margin-top: 6px; }
  /* the card's gauge (details/gauge): the FILL grades green through red, because an open card is
     one place and one question — the map's gauge wears its disc's colour instead, where colour has
     to say which kind of place it is. The track and rim are the CARD's own ink, mixed, exactly like
     the glyph divider above: the gauge's dark chip from the map went olive over a gold card, and
     this one holds on every fill a feed can hand us, pastel to dark red. */
  .gauge {
    display: block; height: 9px; margin: 6px 0 2px; max-width: 220px;
    background: color-mix(in srgb, currentColor 16%, transparent);
    border: 1px solid color-mix(in srgb, currentColor 45%, transparent);
    border-radius: var(--radius-s); overflow: hidden;
  }
  /* min-width: one free space in four hundred is a cap you can see, not a hairline (badge/'s own
     rule for the gauge on the map) */
  .gauge i { display: block; height: 100%; min-width: 5px; border-radius: var(--radius-s); }
  /* the count rides at the bar's right end: four characters do not earn a line of their own on a
     card this size, and the bar and its figure are one reading */
  .gauge-row { display: flex; align-items: center; gap: 8px; margin: 6px 0 2px; }
  .gauge-row .gauge { flex: 1; margin: 0; }
  .gauge-n { flex: none; font-variant-numeric: tabular-nums; font-size: 12px; line-height: 16px;
                      opacity: .85; }
  /* the row's name, where one bar is not the whole story: three classes queue at a border post
     and a bar with no name in front of it is a colour */
  .gauge-k { flex: none; width: 3.6em; font-size: 12px; line-height: 16px; opacity: .8; }
  /* WHEN, as a state (details/'s `chip`): a tinted dot and two characters, in place of the
     sentence each feed used to spend a line on */
  .chip { display: inline-flex; align-items: center; gap: 4px;
                   font-size: 12px; line-height: 16px; opacity: .85; font-variant-numeric: tabular-nums; }
  .chip i { flex: none; width: 7px; height: 7px; border-radius: 50%; }
  /* small facts side by side, so two of them do not cost two lines on a card this size */
  .row { margin-top: 4px; font-size: 12px; line-height: 16px; overflow: hidden; text-overflow: ellipsis; }
  /* block, not flex, so the text in it can END in an ellipsis; the facts space themselves */
  .row > * + * { margin-left: 8px; }
  .row > *, .sub > * { vertical-align: middle; }
  /* what the symbol on the left already says, in words — APP-6D is not universally read, and a
     screen reader gets nothing at all from the frame. Under the name, at the meta weight — and a
     ROW, because the one state that would otherwise cost a line of its own (the age chip) rides
     at the end of it. */
  .sub { display: flex; align-items: center; gap: 8px;
                  font-weight: 400; font-size: 12px; line-height: 16px; opacity: .75; }
  /* the WORDS give way and the tail (an age, a bar) does not: it is the one reading on the line */
  .sub-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .sub > :not(.sub-t) { flex: none; }
  /* how far through a works is (pins/incident's span), how well a track is placed (ukraine/), at
     the end of that line: as long as the line leaves it, never longer than a reading. The floor is
     on the BAR, not the row: an empty block has no width of its own, and a row with a basis the
     bar cannot fill sized the card without it (measured: "→ Kyiv" cut to "→ …" on a 287 px card) */
  .sub > .gauge-row { flex: 1 1 auto; max-width: 110px; margin: 0; }
  .sub .gauge { min-width: 48px; }
  /* one pip per source that reported the track, tinted by the feed's own confidence word: three
     dots are read without being counted, "high, 3 src" is not */
  .pips { display: inline-flex; gap: 2px; }
  .pips i { width: 6px; height: 6px; border-radius: 50%; }
  /* a grade's price against the local median (fuel/): name, a bar reading OUT FROM ITS CENTRE, the
     figure. The centre tick is the median — the one thing that makes the bar mean anything, so it
     is drawn and not implied. Colour is the direction, length is how far: no words for either. */
  .price { display: flex; align-items: center; gap: 8px; margin: 4px 0;
                    font-variant-numeric: tabular-nums; }
  .price-k { flex: none; width: 3.4em; font-size: 12px; line-height: 16px; opacity: .8; }
  .price-v { flex: none; font-size: 12px; line-height: 16px; }
  .price-bar {
    position: relative; flex: 1; min-width: 104px; height: 7px; border-radius: var(--radius-s);
    background: color-mix(in srgb, currentColor 14%, transparent);
  }
  .price-bar::before { /* the median, ABOVE the segment: it is what the segment is read
       against, so a bar that reaches it must not cover it */
    content: ''; position: absolute; left: 50%; top: -2px; bottom: -2px; width: 1px; z-index: 1;
    background: color-mix(in srgb, currentColor 55%, transparent);
  }
  /* min-width: a pump AT the median still draws a mark on its own tick, or the row reads as a
     grade nobody quoted. SQUARE at the centre and round at the far end, so the eye reads the
     segment as running OUT from the median rather than as a lozenge parked near it */
  .price-bar i { position: absolute; top: 0; bottom: 0; min-width: 3px; }
  .price-bar i[style*="right:50%"] { border-radius: var(--radius-s) 0 0 var(--radius-s); }
  .price-bar i[style*="left:50%"] { border-radius: 0 var(--radius-s) var(--radius-s) 0; }
  /* the tick is NAMED, once, under the bottom bar — a bar read against an unnamed centre is a
     decoration. It hangs off the last row rather than taking a caption line of its own, so the word
     sits on the very thing it names instead of a sentence under the card. */
  .price:last-of-type { margin-bottom: 12px; }
  .price:last-of-type .price-bar::after {
    content: 'median'; position: absolute; left: 50%; top: 9px; transform: translateX(-50%);
    font-size: 12px; line-height: 16px; letter-spacing: .03em; opacity: .55;
  }
  /* the one thing a card offers to DO (details/'s `act`): outlined in the CARD's own ink like the
     gauge's track, never filled — the card's background is the feed's colour and a filled button
     would be a second one on it. Its own line under the facts, full width of what it sits in. */
  .card-act {
    display: block; margin-top: 8px; padding: 4px 8px;
    font: inherit; font-size: 12px; line-height: 16px; cursor: pointer;
    color: inherit; background: color-mix(in srgb, currentColor 10%, transparent);
    border: 1px solid color-mix(in srgb, currentColor 45%, transparent); border-radius: var(--radius-m);
  }
  /* a licence line on a pin-coloured card: the browser's own link blue is a third colour on a
     ground that already has two, and on the darker fills it falls under 3:1. The card's ink,
     underlined — the underline is what says it is a link */
  a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
  /* the tail, painted over the card's bottom border so the outline runs unbroken */
  &::after {
    content: ''; position: absolute; left: 50%; bottom: -7px;
    width: 14px; height: 14px; margin-left: calc(-7px - var(--dx, 0px)); /* stays on the point */
    background: inherit; border-radius: 0 0 var(--radius-s) 0;
    border-right: 1.5px solid var(--line-glyph); border-bottom: 1.5px solid var(--line-glyph);
    transform: rotate(45deg);
  }
}

/* the contents arrive once the shape is most of the way there, and LAND with it */
.callout-body, .callout-glyph, .callout-x {
  opacity: 0; transition: opacity var(--tap) var(--ease);
}

/* NOTHING ON A CARD WRAPS: a title, what it is, a fact — each one line, cut with an ellipsis where
   it does not fit, and details/ hangs the whole of a cut line on its tooltip. A card that grew a
   paragraph of an upstream's prose was a wall of text over the map it was meant to explain. */
.callout-body { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* THE CAMERA CARD (roadcams/camcard.js): the camera's name, a rounded picture, a short stem
   with a gap at each end, standing over the badge. --w is written per frame from metres on the
   third of the map's width; .kept is the card in the cards' column after the 📌, where the
   column lays it out and the stem is gone. The Marker owns the element's transform, so the grow
   is on the card inside it. */
.cam-card {
  display: flex; flex-direction: column; align-items: center;
  width: var(--w, 320px); pointer-events: none;
}

.cam-card > * { transition: opacity var(--fade) var(--ease), transform var(--slide) var(--ease); }

.cam-card:not(.open) > * { opacity: 0; }

.cam-card.kept { width: 100%; flex: none; pointer-events: auto; }

.cam-card.kept :is(.cam-stem, .cam-keep) { display: none; }

.cam-card:not(.open) .cam-frame { transform: translateY(12px) scale(.92); }

.cam-card:not(.open) .cam-stem { transform: scaleY(0); }

.cam-title {
  align-self: flex-start; max-width: 100%; margin: 0 0 6px 4px;
  font: bold 14px/20px var(--font); color: var(--text-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* the halo is the app's glass: white by day, dark at night, so the name reads on any ground */
  text-shadow: 0 0 3px var(--overlay-scrim), 0 0 8px var(--overlay-scrim), 0 0 14px var(--overlay-scrim);
}

.cam-frame {
  position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; isolation: isolate;
  border-radius: var(--radius-xl); border: 1.5px solid var(--line-glyph);
  background: var(--surface-void); box-shadow: 0 8px 24px var(--overlay-shadow);
  pointer-events: auto; transform-origin: 50% 100%;
}

/* the monitor's elements (roadcams/monitor/, the AR card's own classes) arrive when a picture is there */
.cam-frame :is(.ar-video, .ar-yt) { opacity: 0; transition: opacity var(--fade); background: transparent; }

.cam-frame .ar-video { object-fit: cover; }

.cam-frame .ar-yt { pointer-events: none; }

.cam-frame.live :is(.ar-video, .ar-yt) { opacity: 1; }

.cam-x, .cam-keep {
  position: absolute; top: 6px; right: 6px; z-index: 3;
  display: grid; place-items: center; width: 26px; height: 26px; padding: 0;
  border-radius: var(--radius-m); color: var(--text-ink); /* their plate: glass (camcard.js) */
  cursor: pointer;
}

.cam-x .icon { width: 12px; height: 12px; }

.cam-keep { right: 38px; }

.cam-keep .icon { width: 16px; height: 16px; }

.cam-note {
  position: absolute; left: 8px; bottom: 8px; z-index: 2; padding: 2px 8px; border-radius: var(--radius-s);
  color: var(--text-ink); font: 12px/16px var(--font); /* its plate: glass (camcard.js) */
}

.cam-note:empty { display: none; }
.cam-note a { color: inherit; text-decoration: underline; pointer-events: auto; }

/* the stem is INK with the glass as its halo, like the title: a hairline in the frame's grey vanished on the night map */
.cam-stem { width: 2px; height: 28px; margin-top: 8px; border-radius: var(--radius-full); background: var(--text-ink);
            box-shadow: 0 0 0 1.5px var(--overlay-scrim); transform-origin: 50% 0; }

/* the pin's glyph as a column down the left edge — as TALL as the card (align-self: stretch),
   because what is in it is a symbol with a movement leader off one end, and a 36 px square gave
   that drawing a frame smaller than the two words beside it. The drawing letterboxes itself into
   the column, so the widest it can be is the column's 48 px and the tallest is the card. */
.callout-glyph {
  flex: none; align-self: stretch; width: 60px; margin-right: 12px; padding-right: 12px;
  display: flex; align-items: center;
  /* the divider is the CARD's own ink at a quarter: the fill under it runs from a pastel to a
     dark red, and one fixed black hairline vanishes on half of them */
  border-right: 1px solid color-mix(in srgb, currentColor 25%, transparent);
}

/* a SQUARE, whatever the symbol's own box: milsymbol's SVG with a movement leader is three times
   taller than wide (hdg 175: 118 × 257 units), and `height: 100%` let that height set the card's —
   a two-line UAV card stood 158 px tall beside a 71 px unit card. The symbol fits the square.
   `height: auto` is load-bearing: the SVG's own height attribute is a presentation hint, and
   aspect-ratio yields to any height that is not auto. */
.callout-glyph svg, .callout-glyph img { display: block; width: 100%; height: auto; aspect-ratio: 1; object-fit: contain; }

/* …and on the disc it is drawn on, where the glyph is a RASTER with dark ink baked into it (the
   siren, the boom gate): a card cannot re-ink those, and the night card's ground is dark. The
   plate is the symbol's own fill, so the disc arrives in the card as the disc — which also leaves
   the card's body neutral, and a bar in a feed's colour readable on it. */
.callout-glyph.plated {
  align-self: center; width: 48px; height: 48px; /* a disc: its own square, whatever the card's height */
  border-right: none; border-radius: 50%; padding: 4px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--line-glyph) 45%, transparent);
}

/* the ✕ inherits the CARD's ink (a pin-coloured card carries fixed dark ink); .6 stays
   above 4.5:1 on either ground */
.callout-x {
  position: absolute; top: 2px; right: 3px; cursor: pointer;
  color: inherit; opacity: .6;
  min-width: 24px; min-height: 24px; padding: 4px 6px; border-radius: 50%; /* a round wash under the pointer */
}

.callout-x .icon { width: 12px; height: 12px; }


/* one focus ring everywhere, offset outside the control so it survives pressed fills */
:is(button, a, summary, input, select, [tabindex], [contenteditable="true"]):focus-visible {
  outline: 2px solid var(--text-ink); outline-offset: 2px;
}
/* …and ONE hover and ONE press for every control, beside it: THE HIGHLIGHT's washes (10 % hovered, 20 % pressed)
   laid inside its box (the owner, 2026-10-24: one look for hovered, pressed and selected; it was the control's own ink
   at 10/20 %). Not a brightness filter — a filter multiplies the pixels there are, and half of these are a glyph on
   nothing, which brightness cannot touch; the night palette flips the cyan for free. `:where` keeps it at zero specificity, so a control that shades
   its own way (the Compute Graph's node glow, the drive pad's inversion) wins on its own rule.
   GL JS's buttons are washed by background-color above: their focus rules own box-shadow. The wash takes the
   control's SHAPE: a round control's own rule gives it `border-radius: 50%` (the ☰, the chevron, a callout's ×) —
   a 40px square wash inside the ☰'s circle was the owner's note of 2026-10-17. */
:where(#toolbar button, #menu-btn, .mapboxgl-ctrl-scale, .ar-head button, .show,
       button.overlays-row, #overlays-list summary, .overlays-row button, .callout-x, .card-act, .rewind-play, .rewind-shut,
       .rewind-live, .dial-btn, .nato-sw, .nato-chev, .nato-tog, #performance button,
       .cg-fold, .fancy-button) {
  transition: box-shadow var(--tap);
  /* hover is a POINTER's state: a finger that tapped leaves none behind, and the wash stuck to the
     last control touched until the next tap landed somewhere else */
  @media (hover: hover) {
    &:hover { box-shadow: inset 0 0 0 100vmax var(--highlight-hover); }
  }
  &:active { box-shadow: inset 0 0 0 100vmax var(--highlight-wash); }
}
#overlays-head h2 { font-size: inherit; font-weight: inherit; margin: 0; }
#skip-link {
  position: absolute; z-index: 10; top: -100px; left: 20px;
  padding: 8px 12px; border-radius: var(--radius-m);
  color: var(--text-ink); text-decoration: none; /* its plate: the thick glass (index.html) */
}
#skip-link:focus-visible { top: 20px; }
/* the boot failure banner (app.js): the one failure a demo hits was a silent beige desk */
#map-error {
  position: absolute; z-index: 10; top: 72px; left: 50%; transform: translateX(-50%); /* under the tool row, off the search pill */
  max-width: min(560px, calc(100% - 40px));
  padding: 8px 16px; border-radius: var(--radius-l);
  color: var(--text-danger); /* its plate: the thick glass (app.js) */
  font-weight: bold;
}
#map-error.ok { color: var(--text-ink); } /* news, not a fault: a link copied (shell/share/) */
/* THE TOUCH PAD (fleet/drive/touchpad.js): ONE STICK under the left thumb, ONE SLOT under the right,
   nothing in the middle where the road is. The stick's ring rests in its corner until a thumb lands
   anywhere in the zone, then under that thumb; pushed to the rim the car boosts and the man sprints,
   and the ring says so in ink. The slot is the key the stick cannot say:
   a car's handbrake, a gun's fire, a man's pistol and sights, the drone's climb-and-descend (two halves a
   thumb slides between); touchpad.js shows the driven kind's plate alone. Over the map's bottom edge on a
   desk; on a phone, in the map's lower corners (the phone block). */
#drive-pad {
  position: absolute; z-index: 4; left: 0; right: calc(var(--dock, 0px) / var(--ui)); bottom: 24px;
  display: flex; justify-content: space-between; align-items: flex-end; padding: 0 16px;
  pointer-events: none; /* only the stick's zone and the slot take the pointer */
  :is(.slot, .stick-zone) {
    pointer-events: auto; touch-action: none; -webkit-user-select: none; user-select: none; }
  :is(.slot, .stick) { color: var(--text-ink); } /* their plate: glass (touchpad.js) */
  .slot { display: flex; flex-direction: column; border-radius: var(--radius-full); overflow: hidden; }
  /* its own reset, not the shared :where one: the plate rule above outranks zero specificity; a half
     takes no pointer of its own — the plate's decides which half the thumb is on */
  .slot button { width: 64px; height: 64px; border: none; border-radius: 0; background: none; color: inherit;
                 display: grid; place-items: center; pointer-events: none; }
  /* pressed: the app's one selected look, the highlight (the SELECTED rule by the tool row) */
  /* the stick's zone: the left half, up to a thumb's reach; the ring's rest is its corner */
  .stick-zone { position: relative; width: 44%; height: 200px; }
  .stick {
    position: absolute; left: 70px; top: calc(100% - 70px); translate: -50% -50%;
    width: 124px; height: 124px; border-radius: 50%; pointer-events: none;
    opacity: .72; transition: opacity var(--fade); }
  .stick.on { opacity: 1; }
  .stick.sprint { border-color: var(--text-ink); box-shadow: 0 0 0 3px color-mix(in srgb, var(--time-fill) 55%, transparent); }
  .knob {
    position: absolute; inset: 0; margin: auto; width: 52px; height: 52px; border-radius: 50%;
    background: var(--text-ink); }
}

/* controls built by settings/ and fps/, styled HERE so they follow the night swap */
/* THE DEBUG READOUT, bottom-left (shell/overlaysPanel/performance/). Shut: one row — the frame rate, the repaints and
   the worst frame — with a chevron. A plate of the thick glass (performance.js) in the app's ink — a
   READOUT with no rim while shut, a panel with the rim once open. Tabular figures, or a changing
   digit shuffles the row. `pointer-events` is off on the shut box and back ON for the chevron
   alone, so the readout never eats a map drag that starts in the corner. */
#performance { position: absolute; bottom: 8px; left: 8px; z-index: 2; /* under the Overlays */ padding: 2px 8px;
           font: 12px/16px ui-monospace, Menlo, monospace; font-variant-numeric: tabular-nums;
           text-align: left; color: var(--text-ink); border-radius: var(--radius-l); pointer-events: none;
           transition: left var(--slide) var(--ease); /* its size and inset FOLD (performance.js, util/dom.js fold) */
  .top { display: flex; align-items: center; gap: 6px; }
  /* the row's text: the readout while shut, the drawer's title while open — one grid cell, crossfaded */
  .face { display: grid; flex: 1; }
  /* the readout and the title share one cell and never one moment: the one leaving fades on --tap, the one coming waits
     for it — crossfaded on --fade they read as one garbled line for 100 ms (2026-10-22) */
  .face > * { grid-area: 1 / 1; transition: opacity var(--tap) var(--ease) var(--tap); }
  &:not(.open) .title, &.open .rate { transition-delay: 0s; }
  .title { opacity: 0; font-weight: 600; }
  &.open .rate { opacity: 0; }
  &.open .title { opacity: 1; }
  /* the chevron's HIT AREA is 24px (WCAG 2.5.8) and its glyph stays 13 — negative margins keep
     the row the height the text sets, so the readout does not grow to hold a target */
  button { pointer-events: auto; display: grid; place-items: center;
                    color: inherit; cursor: pointer; border-radius: 50%; /* a round wash under the pointer */
                    width: 24px; height: 24px; padding: 0; margin: -6px -6px -6px 0; }
  /* the chevron points where the box will go: up to open the drawer above the row, down to fold it */
  button .icon { width: 12px; height: 12px; transform: rotate(180deg); transition: transform var(--slide) var(--ease); }
  &.open button .icon { transform: none; }
  /* OPEN: the Performance drawer unfolds out of the row — up (its rows track 0fr → 1fr) and right (0 → 300px)
     together, on the panel's own slide, its content fading in behind; folding runs it backwards.
     The inner .perf-drawer keeps its full 300px throughout, so a plot drawn mid-unfold is drawn at its
     real size and only clipped. Open, it takes the pointer: a panel, like the Overlays */
  &.open { z-index: 4; padding: 6px 8px 8px; pointer-events: auto; }
  .more { display: grid; grid-template-rows: 0fr; width: 0; opacity: 0; overflow: hidden; }
  /* folding shut (.morph) the drawer stays whole and the panel's closing edges cover it, as its opening ones uncover it;
     the drawer's own 0fr/width/opacity transitions drifted from the panel's (the width snapped in two frames) */
  &:is(.open, .morph) .more { grid-template-rows: 1fr; width: 300px; opacity: 1; }
  &.morph { overflow: hidden; }
  /* minmax(0, …): a no-wrap line ellipsises inside the column instead of widening it past the box;
     a viewport shorter than the drawer scrolls it, stopping clear of the tool row (a phone's wraps).
     Its top padding comes in with the unfold: a 0fr row still holds its padding, under the readout */
  .perf-drawer { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; min-height: 0;
                    width: 300px; max-height: calc(100dvh - 180px); overflow-y: auto; padding-top: 0;
                    scrollbar-width: thin; scrollbar-color: var(--hud-fps-grid) transparent; }
  &:is(.open, .morph) .perf-drawer { padding-top: 8px; }
  /* the Overlays and the drawer share the left edge: open together on a screen wide enough, the drawer
     steps out beside the panel instead of under it (#map is its own stacking context, so no
     z-index of the drawer's can rise over the panel) */
  @media (min-width: 720px) {
    body:has(#overlays:not(.shut)) &.open { left: calc(var(--sidew) + 20px); }
  }
  p { margin: 0; }
  .dim { opacity: .7; }
  /* a group per question: Smooth? · Cost · Loading · Where the time goes */
  section { display: grid; gap: 6px; }
  section + section { padding-top: 8px; border-top: 1px solid var(--hud-fps-grid); }
  h3 { display: flex; justify-content: space-between; margin: 0; font: inherit; font-weight: 600; opacity: .7; }
  h3 .note { font-weight: 400; }
  canvas { display: block; width: 100%; height: 16px; border-radius: var(--radius-s); background: var(--hud-fps-well); }
  canvas.plot { height: 56px; }
  canvas.strip { height: 20px; }
  canvas.bars { height: 28px; }
  .cap { display: flex; justify-content: space-between; gap: 8px; }
  /* the copy button rides the title row, open only; its glyph is upright, escaping the chevron's half-turn */
  .copy { margin-right: 2px; }
  .copy .icon { transform: none; }
  &:not(.open, .morph) .copy { display: none; }
  /* the three numbers the owner judges by, large, the unit on their line, each with its state in a word */
  .hero { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
  .stat b { margin-right: 4px; font-size: 24px; line-height: 32px; font-weight: 600; }
  .state { display: flex; align-items: center; gap: 4px; min-width: 0; white-space: nowrap; }
  .state i { flex: none; width: 7px; height: 7px; border-radius: 50%; }
  .state i:not(.good, .warn, .bad) { display: none; }
  i.good { background: var(--text-on); }
  i.warn { background: var(--text-warn); }
  i.bad { background: var(--text-danger); }
  /* cost: CPU and GPU a column each — name, a gauge filling toward the budget, the reading — over
     their bars on the same scale */
  .meters { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  .gauge { display: grid; gap: 4px; }
  .gauge p { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 6px; align-items: center; }
  .ram p:first-child { display: flex; align-items: baseline; }
  :is(.gauge, .ram) b { font-size: 14px; line-height: 20px; font-weight: 600; }
  b.over { color: var(--text-danger); }
  .track { display: block; height: 6px; margin-top: 2px; border-radius: var(--radius-s); overflow: hidden;
                    background: var(--hud-fps-well); }
  /* SCALED, NOT SIZED (performance.js writes scaleX): each reading eased a bar's width, a layout on every frame of every
     ease — 275 layouts a second with the drawer open (the perf scan, 2026-10-22); a transform is the compositor's */
  .track i { display: block; height: 100%; border-radius: var(--radius-s); transform-origin: 0 50%;
                      transition: transform var(--fade) var(--ease); }
  i.cpu { background: var(--hud-fps-cpu); }
  i.gpu { background: var(--hud-fps-gpu); }
  .track i.over { background: var(--text-danger); }
  /* the figures with a what-is-counted chip take the keyboard too */
  :is(.gauge, .ram) { border-radius: var(--radius-s); }
  :is(.gauge, .ram):focus-visible { outline: 2px solid var(--text-ink); outline-offset: 2px; }
  .ram { display: grid; gap: 2px; }
  .key { display: inline-flex; align-items: center; gap: 4px; margin-right: 8px; }
  .key i { width: 8px; height: 8px; border-radius: var(--radius-s); }
  /* RAM's title row: the name, its trend beside it, the reading pushed right; the caption is the legend */
  .ram p:first-child { justify-content: flex-start; gap: 8px; }
  .ram b { margin-left: auto; }
  .ram .cap { justify-content: flex-start; white-space: nowrap; }
  .load-plots { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
  .load-plot p { display: flex; justify-content: space-between; gap: 4px; margin-bottom: 2px; }
  .gauge .track { margin: 0; }
  /* where the time goes: the longest frame, tinted by its state, then the top three, each row tinted
     to its share of the first — the name on its own bar, so the row is the chart */
  :is(.rank, .hitch) { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto;
                                gap: 8px; align-items: center; padding: 2px 6px; border-radius: var(--radius-s); }
  .hitch { grid-template-columns: auto minmax(0, 1fr) auto; }
  .hitch.warn { background: color-mix(in srgb, var(--text-warn) 16%, transparent); }
  .hitch.bad { background: color-mix(in srgb, var(--text-danger) 14%, transparent); }
  .rank > i { position: absolute; inset: 0; border-radius: var(--radius-s); transform-origin: 0 50%;
                       background: color-mix(in srgb, var(--hud-fps-cpu) 22%, transparent);
                       transition: transform var(--fade) var(--ease); }
  .rank > span { position: relative; }
  .name .dim { margin-left: .6em; }
  :is(.name, .load-plot p span:last-child) {
    display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

/* the camera readout: a STATUS LINE under the scale bar, bottom-right (mapcontrols.js; Google Earth's place, the owner's
   pick 2026-10-22), a glow for legibility. ONE line — zoom / bearing / pitch / altitude, then the centre — right-aligned
   to the map's edge, reaching left over the map as far as it needs. At the top of the control column it hung in the
   tool row's band, and an open Rewind or search ran under it; it had to fade out of their way. */
/* …and never under the Performance pill (#performance, bottom-left, ~260 px): a narrow map (a card beside it, iPad
   portrait) wraps the line into the corner's width instead of running it under the pill (2026-10-27) */
#map .mapboxgl-ctrl-bottom-right { max-width: calc(100% - 270px); }
.zoom-readout {
  padding-right: 4px; text-align: right;
  /* 10/12: under the 12 floor on purpose — a glance at the camera, not text to read; the nearest standard step to
     its old 9 (Fluent 2's and Calcite's smallest caption), at 12 it crowded the corner (the owner, 2026-10-22) */
  font: bold 10px/12px var(--font); font-variant-numeric: tabular-nums;
  color: var(--text-ink); text-shadow: 0 0 3px var(--surface-panel), 0 0 6px var(--surface-panel);
}
.dial-row { display: flex; align-items: center; gap: 6px; padding: 1px 0; font-size: 12px; line-height: 16px; }
/* THE app's tooltip (theme/tooltip/) — the shell's buttons and the map's entities alike.
   Not `title=`: the native tooltip is suppressed over a dragging range input, it cannot go dark
   with the basemap, and two tooltip looks eight pixels apart is not a design. Above every
   floating control (the tool row, the cards, the promotion bar); a <dialog> is in the top layer
   and outranks any z-index, which is fine — nothing in a dialog carries a chip. */
.tip { position: fixed; z-index: 40; display: none; max-width: 280px; padding: 4px 8px;
       border-radius: var(--radius-m); color: var(--text-ink); /* its plate: the thick glass (tooltip.js) */
       font-size: 12px; line-height: 16px; pointer-events: none; }
/* THE KEY HINTS (parts/tracks/hint.js/): a key cap and its verb over the thing the key acts on,
   the tooltip's own chip with the cap in reverse. Only a pill that DOES something takes a tap */
.key-hints { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 2; }
/* a cluster is the whole display (arview/cluster/cockpit.js): no chip over it. The keys still act */
body.cockpit .key-hints { display: none; }
.key-hint { position: absolute; left: 0; top: 0; display: flex; align-items: center; gap: 6px;
            margin: 0; padding: 0; color: var(--hud-key-text);
            font: inherit; font-size: 12px; font-weight: 700; line-height: 1; white-space: nowrap;
            text-shadow: 0 0 3px var(--hud-key-halo), 0 1px 2px var(--hud-key-halo); pointer-events: none;
            /* a pill arrives on a fade, not in one frame; it follows its anchor by transform, which is not eased */
            transition: opacity var(--fade); @starting-style { opacity: 0; } }
.key-hint[data-run] { pointer-events: auto; cursor: pointer; }
/* a key chip is the shell's furniture standing in the map (zoom 1/--ui), placed by a translate in the map's px: its
   CONTENTS take the shell's zoom, never its box (a zoomed box's translate would scale too) — at 4K it was 1× (UX audit) */
.key-hint > * { zoom: var(--ui); }
/* a KEYCAP, as game prompts and keyboard.css draw one, on no plate: a white face in a dark rim, a
   darker skirt (the inset bottom edge) and the legend lifted onto the face */
.key-caps { display: flex; gap: 2px; }
.key-wasd { display: grid; grid-template-columns: repeat(3, auto); }
.key-wasd kbd:nth-child(1) { grid-area: 1 / 2; }
.key-wasd kbd:nth-child(2) { grid-area: 2 / 1; }
.key-wasd kbd:nth-child(3) { grid-area: 2 / 2; }
.key-wasd kbd:nth-child(4) { grid-area: 2 / 3; }
/* W A S D with Q E: one pair under A S D, no space between (the owner, 2026-10-13) — six half-columns, every cap spans two */
.key-six { grid-template-columns: repeat(6, auto); }
.key-six kbd:nth-child(1) { grid-area: 1 / 3 / 2 / 5; } .key-six kbd:nth-child(2) { grid-area: 2 / 1 / 3 / 3; }
.key-six kbd:nth-child(3) { grid-area: 2 / 3 / 3 / 5; } .key-six kbd:nth-child(4) { grid-area: 2 / 5 / 3 / 7; }
.key-six kbd:nth-child(5) { grid-area: 3 / 2 / 4 / 4; } .key-six kbd:nth-child(6) { grid-area: 3 / 4 / 4 / 6; }
:is(.key-hint, .co-caps) kbd { box-sizing: border-box; min-width: 22px; height: 22px; padding: 0 4px 2px;
                display: inline-flex; align-items: center; justify-content: center;
                border: 1.5px solid var(--hud-key-rim); border-radius: var(--radius-s);
                background: var(--hud-key-face); color: var(--hud-key-rim); text-shadow: none;
                box-shadow: inset 0 -3px 0 color-mix(in srgb, var(--hud-key-face) 72%, var(--hud-key-rim)),
                            0 1px 3px var(--overlay-tip-shadow);
                font: inherit; font-size: 12px; font-weight: 700; line-height: 1; }
.key-wasd kbd { min-width: 18px; height: 18px; padding: 0 2px 2px; font-size: 12px; line-height: 16px;
                box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--hud-key-face) 72%, var(--hud-key-rim)),
                            0 1px 3px var(--overlay-tip-shadow); }
.key-hint[data-run]:active kbd { padding-bottom: 1px; /* pressed: the face sinks onto the skirt */
                box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--hud-key-face) 72%, var(--hud-key-rim)); }
.dial-name { flex: none; width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dial-num { flex: none; width: 62px; font: inherit; font-size: 12px; line-height: 16px; padding: 1px 2px;
            border: 1px solid var(--line-border); border-radius: var(--radius-s); }
/* ONE thumb for every dial slider, single or paired. A range input's track and thumb ship with
   browser defaults that belong to no design system, and the paired track has to draw its own
   rail anyway — two NATIVE tracks each filled from the left is what made one range read as two
   broken sliders. */
.dial-range { flex: 1; min-width: 40px; height: 14px;
              appearance: none; background: none; }
.dial-range::-webkit-slider-runnable-track { height: 4px; border-radius: var(--radius-full);
                                            background: var(--line-border); }
.dial-range::-webkit-slider-thumb { appearance: none;
  width: 12px; height: 12px; margin-top: -4px; border-radius: 50%;
  background: var(--text-ink); border: 1px solid var(--surface-card); cursor: pointer; }
/* the ring goes on the THUMB: an outline round a 100%-wide transparent input says nothing */
.dial-range:focus-visible { outline: none; }
.dial-range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 2px var(--text-ink); }

/* ---- a two-handle range (settings/) ----
   The two numbers ride the label line and the track takes a line of its own: squeezed beside
   them it measured 56 px, where two handles cannot both be grabbed. */
.dial-row-pair { flex-wrap: wrap; padding: 2px 0 4px; }
.dial-row-pair .dial-name { width: auto; flex: 1 1 auto; min-width: 34px; }
.dial-num-pair { width: 52px; }
.dial-pair { position: relative; flex: 0 0 100%; height: 14px; margin-top: 2px; }
.dial-pair-rail { position: absolute; left: 0; right: 0; top: 5px; height: 4px;
                  border-radius: var(--radius-full); background: var(--line-border); }
/* the BAND between the handles is what a range means; a fill from the left says the wrong thing.
   It reads as the value, so it takes the THUMB's family a step quieter, not a line colour:
   line.strong measured 2.6:1 on line.border at night, under the 3:1 floor for a state indicator.
   text.soft measures 5.98:1 at night and 6.06:1 by day, on the same rail. */
.dial-pair-fill { position: absolute; top: 5px; height: 4px; border-radius: var(--radius-full);
                  background: var(--text-soft); }
.dial-pair .dial-range { position: absolute; inset: 0; width: 100%; margin: 0;
                         pointer-events: none; }
.dial-pair .dial-range::-webkit-slider-runnable-track { background: none; }
.dial-pair .dial-range::-webkit-slider-thumb { pointer-events: auto; }
.dial-pair .dial-pair-hi { z-index: 2; }
/* the chase shot drawn from the side (settings/): an EXTRA view of two dials that keep their own
   sliders below it, so nothing here is the only way to set them */
.dial-pad-row { padding: 4px 0; }
.dial-pad { width: 100%; max-width: 190px; height: auto; cursor: crosshair; touch-action: none;
            border: 1px solid var(--line-border); border-radius: var(--radius-m);
            background: var(--surface-card); }
.dial-pad-ground { stroke: var(--line-border); stroke-width: 1; }
.dial-pad-car { fill: var(--text-faint); }
.dial-pad-ray { stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 3 2; }
.dial-pad-eye { fill: var(--text-ink); }
/* the steep half, where every degree of pitch drags the horizon's tile pyramid into frame */
.dial-pad-costly { fill: var(--text-faint); opacity: 0.13; }
/* a named choice (settings/); `color` is already handed back by the form-control rule above */
.dial-select { flex: 1; min-width: 40px; font: inherit; font-size: 12px; line-height: 16px; padding: 1px 2px;
               background: transparent; border: 1px solid var(--line-border); border-radius: var(--radius-s); }
.dial-reload { flex: none; width: 13px; height: 13px; margin: 0; color: var(--text-faint); }
.dial-btn { padding: 2px 8px; font: inherit; font-size: 12px; line-height: 16px;
            cursor: pointer; border: 1px solid var(--line-border); border-radius: var(--radius-m);
            background: var(--surface-card); }
/* Copy beside Reset, at the TOP: under 254 rows a button is thousands of px from the click */
.dial-bar { display: flex; gap: 6px; margin: 0 0 6px; }

/* ── The promotion bar (parts/nato/nato.js) ────────────────────────────────────────────────
   Two objects side by side, not one: the FIELD, with its dropdown hanging under it, and the four
   disposition frames floating clear of the pill. The pill is therefore only part of the bar's
   width, which is why nothing here is centre-anchored — place() aligns the COLUMN to the place
   and lets the frames sit off to one side. It lives in the map's container, so it is positioned
   in the same pixels map.project() speaks. */
/* THE PROMOTION BAR — one pill: a question, a hairline, and its four answers. The frames sat
   OUTSIDE the pill until 2026-09-07, which made the bar two objects to keep in line, gave the
   pill a width that changed as you typed, and needed a tail to say which of the two pointed at
   the building. One pill needs none of that. It wore an OPAQUE plate until 2026-10-17 — the frames
   are read as COLOURS and a building through a translucent panel shifts every one of them, the cost
   the owner accepted having seen it on four prototypes — and is now the LIQUID GLASS (`.glass`,
   theme/glass), with the card of names INSIDE it: one shape that grows down over the building
   (0fr → 1fr, as the amplifier drawer does) and eases its radius, not a second object hung under. */
/* --nato-w: 340 for a mouse, stepped up under a thumb (the coarse block) so four 44px frames leave the field room */
.nato-bar { position: absolute; z-index: 20; width: calc(var(--nato-w, 340px) * var(--ui)); display: flex; flex-direction: column; }
.nato-pill { position: relative; border-radius: calc(48px / 2); overflow: hidden; /* half its row: it eases to xl as the list opens */ display: flex; flex-direction: column;
             transition: border-radius var(--slide) var(--ease), border-color var(--tap); }
.nato-pill:has(.nato-list:not([hidden])) { border-radius: var(--radius-xl); }
.nato-row { display: flex; align-items: center; gap: 8px; height: 48px; padding: 0 8px 0 16px; }
.nato-q { flex: 1; min-width: 0;
          font: 14px/1 var(--font); color: var(--text-ink); padding: 0;
          caret-color: var(--text-ink); }
.nato-q::placeholder { color: var(--text-meta); opacity: 1; }
/* The FOCUS RING IS THE PILL, the way #searchbar's is the pill: the app's global rule boxes any
   focused input, and a rectangle inside a rounded pill that is itself the focused object read as
   a second, smaller widget. The indication is not dropped — it moves outward, to the glass's rim. */
.nato-q:focus-visible { outline: none; } /* the pill's rim goes to ink instead: `.glass:focus-within` */
.nato-div { flex: none; width: 1px; height: 22px; background: var(--line-border); }

/* the four frames, INSIDE the pill: gap, not a grid, because they are four different SHAPES
   whose natural widths differ by a third — boxed in one fixed square the widest (the friendly
   rectangle) overflowed and ate the gap beside it, and the row read as 2 + 2. The armed one
   stands forward and the rest recede; NO MARK EVER TOUCHES A FRAME, because a bar under an
   APP-6D symbol is its STATUS field (yellow damaged, red destroyed) and an underline would have
   been saying something. */
.nato-swatches { display: flex; align-items: center; gap: 6px; flex: none; }
.nato-sw { flex: none; width: auto; height: 30px; padding: 0;
           cursor: pointer; display: grid; place-items: center; opacity: .58;
           transition: opacity var(--tap) var(--ease), transform var(--tap) var(--ease); }
.nato-sw[data-armed] { opacity: 1; transform: scale(1.12); }
.nato-sw img { display: block; height: 26px; width: auto; pointer-events: none; }

/* the amplifier drawer's handle, quiet at the end of the row — the concept has nothing here, and the three
   amplifiers are for the people who need them */
.nato-chev { flex: none; width: 26px; height: 26px; border-radius: var(--radius-m);
             cursor: pointer; color: var(--text-meta); display: grid;
             place-items: center; padding: 0; opacity: .5;
             transition: opacity var(--tap) var(--ease), color var(--tap) var(--ease), box-shadow var(--tap); }
.nato-chev .icon { flex: none; width: 16px; height: 16px;
                   transition: transform var(--slide) var(--ease); }
.nato-pill.expanded .nato-chev { opacity: 1; color: var(--text-ink); }
.nato-pill.expanded .nato-chev .icon { transform: rotate(180deg); }

/* the amplifier drawer (nato/), inside the same pill — one row, the controls carrying their own labels */
/* 0fr → 1fr, not max-height: the amplifier drawer opens to the height its ONE row actually is, so the
   56px that used to be written here — a guess at that height — cannot clip the row the day it
   gains a control or a longer label. `min-height: 0` on the row is what lets the grid crush it;
   `overflow: hidden` is what hides it while crushed. nato.js's transitionend keys on this
   property name, so the two move together.
   AND IT IS `minmax(0, 0fr)`, NOT `0fr`. A bare `fr` track is `minmax(auto, Nfr)`, so it keeps
   an AUTO minimum — the row's own min-content, which its 21px of padding sets whatever
   `min-height: 0` says. Measured: the shut amplifier drawer stood 21px open, the padding showing as a
   bare strip under the pill. The explicit 0 minimum is the only part of this that is not
   obvious, and it is the part that does the collapsing. */
.nato-amp { display: grid; grid-template-rows: minmax(0, 0fr); overflow: hidden;
            border-top: 0 solid var(--line-rule);
            transition: grid-template-rows var(--slide) var(--ease); }
.nato-pill.expanded .nato-amp { grid-template-rows: minmax(0, 1fr); border-top-width: 1px; }
.nato-amp-row { min-height: 0; display: flex; align-items: center; gap: 6px; padding: 8px 12px 12px; }
.nato-amp select { flex: 1 1 0; min-width: 0; font: 12px/1 var(--font); color: var(--text-ink);
                   background: var(--overlay-glass); border: 2px solid var(--overlay-rim);
                   border-radius: var(--radius-m); padding: 6px 4px; cursor: pointer; }
/* HQ and Task force are the app's ARMED control, not a UA checkbox: on, they wear the highlight every
   selected control on the map wears (the SELECTED rule by the tool row) */
.nato-tog { flex: none; font: 12px/1 var(--font); color: var(--text-ink); cursor: pointer;
            background: var(--overlay-glass); border: 2px solid var(--overlay-rim);
            border-radius: var(--radius-m); padding: 6px 8px;
            transition: background var(--tap) var(--ease), color var(--tap) var(--ease), box-shadow var(--tap); }

/* THE CARD OF NAMES, inside the pill under the row and the drawer: crushed to 0fr while nothing is
   typed and unfolded to its rows when something is. `[hidden]` keeps its meaning (nato.js writes it)
   and the GRID, not display, is what hides it, so the unfolding can animate. No symbol in the row:
   the frames are two centimetres away in the pill and a 36 px symbol per row turned a list of five
   into a wall nobody scanned. */
.nato-list { display: grid; grid-template-rows: minmax(0, 0fr); overflow: hidden; opacity: 0;
             transition: grid-template-rows var(--slide) var(--ease), opacity var(--fade); }
/* !important, or the one [hidden] rule's display: none wins and the list snaps open and shut (it did, under a pill
   easing its corners round it, until 2026-10-22); `visibility` takes the folded list out of the tab order and the
   accessibility tree as `hidden` means, once the fold has played */
.nato-list[hidden] { display: grid !important; visibility: hidden;
                     transition: grid-template-rows var(--slide) var(--ease), opacity var(--fade), visibility 0s var(--slide); }
.nato-list:not([hidden]) { grid-template-rows: minmax(0, 1fr); opacity: 1; }
/* 5.5 rows, not 5: the sixth peeking over the edge is the whole scroll affordance, and place()
   lowers the cap when the place does not leave that much room */
.nato-hits { min-height: 0; overflow-y: auto; max-height: var(--nato-hits, 276px); overscroll-behavior: contain;
             scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
/* the rule over the first name is inset like the rows' own, and scrolls with them as the first row's top */
.nato-hits::before { content: ''; display: block; height: 1px; margin: 0 16px; background: var(--line-rule); }
/* BLOCK, not flex: a flex container's own text never ellipsises, so the longest entity names
   (2525D has one at 120 characters) were sliced off mid-word with no mark to say so */
.nato-hit { position: relative; height: 46px; padding: 0 16px; cursor: pointer;
            font: 14px/44px var(--font); color: var(--text-ink);
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* the rule is INSET, not a border: a hairline running into the card's rounded corner is the one
   detail that gives a stack of names away as a table */
.nato-hit::after { content: ''; position: absolute; left: 18px; right: 18px; bottom: 0;
                   height: 1px; background: var(--line-rule); }
.nato-hit:last-child::after { display: none; }
.nato-hit.cur { background: var(--highlight-wash); } /* the current row: the highlight's wash, day and night (a grey band before 2026-10-24; the veil read as a dark slab at night) */
/* only on the rows that COLLIDE in this list — see render() */
.nato-hit i { font: 12px/1 var(--font); font-style: normal; color: var(--text-meta);
              margin-left: 6px; }
.nato-none { padding: 16px 16px; color: var(--text-meta); font-size: 12px; line-height: 16px; }

/* ── ONE phone-width block: the panes, the pill, the ⊕, the sheet, the strip, the pad and the promotion bar
   together, so a breakpoint is one place to read. The NATO rules come LAST here, after the bar's
   own base rules above, which is why the block sits in this section and not with the shell's.
   THE SHAPE (the owner's sketch, 2026-10-11; edge to edge since 2026-10-15): the two PICTURES fill the
   screen — the map from the top edge down, the AR view the pane under it (16:9 of the width), 8 px of
   desk between them, no frame, no inset, under the notch and the home indicator alike — and the CHROME
   clears them: one pill (shell/mapcontrols/phone.js) where the tool row and the stack were, one ⊕ in the
   thumb corner, the Overlays as a sheet, Rewind's bar as a strip, the stick and the
   slot over the map. env(safe-area-inset-*) keeps every control off the notch and the iPhone home
   indicator; --homeb is that bottom inset only while the map's bottom edge IS the screen's. */
/* THE PHONE: a phone's width, or a phone's HEIGHT under a thumb — a phone on its side is still a phone,
   not a short desk (shell/mapcontrols/phone.js asks the same query and builds the shell) */
@media (max-width: 560px), ((max-height: 560px) and (any-pointer: coarse)) {
  /* the AR pane is 16:9 of its width — the pop-out's shape, so the picture is the same picture in the
     app and on the glasses — plus whatever the home indicator covers (the picture runs under it); the map
     takes the rest */
  body { --sheeth: calc(100vw * 9 / 16); --homeb: env(safe-area-inset-bottom, 0px); }
  body.cards {
    --dock: 0px;                             /* the map keeps its full width */
    --sheet: calc(var(--sheeth) + 8px + env(safe-area-inset-bottom, 0px)); /* the pane, which runs under the home indicator, + 8 px of desk */
    --homeb: 0px;                            /* the pane stands under the map's edge: nothing there to clear */
  }
  body.cards.nocard { --sheet: 0px; --homeb: env(safe-area-inset-bottom, 0px); } /* the picture put away (H, the pane's ✕ #phone-hide): the map takes its height back */
  /* the map and its loader wear one box: the screen less the pane, no frame — under the status bar and the home indicator */
  #map, #map + .loader { top: 0; left: 0; right: var(--dock, 0px); bottom: var(--sheet, 0px); border: 0; border-radius: 0; }
  #map { transition: right var(--slide) var(--ease), bottom var(--slide) var(--ease); }
  /* what stands on the map's bottom edge (the scale bar, the readout) clears the home indicator by --homeb, and the notch's band on its side */
  #map .mapboxgl-ctrl-bottom-left, #map .mapboxgl-ctrl-bottom-right { bottom: var(--homeb, 0px); }
  #map .mapboxgl-ctrl-bottom-left { left: env(safe-area-inset-left, 0px); }
  #map .mapboxgl-ctrl-bottom-right { right: env(safe-area-inset-right, 0px); }
  #performance { bottom: calc(8px + var(--homeb, 0px)); left: calc(8px + env(safe-area-inset-left, 0px)); }
  /* the AR pane: a horizontal snap strip, the live card leftmost, each card the whole pane */
  #ar-cards {
    top: auto; left: 0; right: 0; width: auto;
    height: calc(var(--sheeth) + env(safe-area-inset-bottom, 0px)); /* the picture runs under the home indicator; its chrome lifts (.ar-head, .ar-minimap-box) */
    bottom: 0;
    flex-direction: row;
    overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory;
    --away: translateY(calc(100% + 20px)); /* away is DOWN here */
  }
  body.cards #ar-cards { transform: none; }
  .ar-card { position: relative; width: 100%; height: 100%; max-height: 100%; scroll-snap-align: start; align-self: stretch; }
  .ar-frame { flex: 1 1 auto; min-height: 0; aspect-ratio: auto; border: 0; border-radius: 0; }
  #ar-seam, body.cards #ar-seam { display: none; } /* upright, the 8 px of desk is the gap and nothing to drag (the owner, 2026-10-15); on its side it is the handle (below) */
  .ar-minimap-box { translate: 0 calc(0px - env(safe-area-inset-bottom, 0px)); } /* the 2D minimap placed the minimap in frame px; the frame's foot is under the home indicator */
  /* THE STACK and the row's clock leave: the pill has ☰, search and locate, the sheet has the clock and
     day/night, and two fingers turn the map (no compass: the owner). The scale bar keeps the map's
     bottom-right corner, under the ⊕ (the owner, 2026-10-13); the readout keeps the bottom-left (below). */
  .mapboxgl-ctrl-top-right, #rewind-open { display: none; }
  .zoom-readout { display: none; } /* it left with the stack until it moved down by the ruler (2026-10-22): the corner is the ruler's and the ⊕'s */
  /* THE PILL: fixed over the map pane's top, the app's one bar look; its search box loses its own
     capsule (it IS the pill's middle) and keeps its field open — the desk folds it to a circle */
  #phone-pill {
    position: fixed; z-index: 5; top: calc(22px + env(safe-area-inset-top, 0px));
    left: calc(22px + env(safe-area-inset-left, 0px)); right: calc(22px + env(safe-area-inset-right, 0px));
    height: 48px; align-items: center; padding: 0 2px;
    overflow: visible; /* the pill class clips to its capsule — and clipped the search list hanging under it */ }
  /* the suggestion list hangs under the WHOLE pill: the field and its capsule stop being its anchor —
     so the magnifier, absolute in the capsule on the desk, stands in the flow here (or it lands on the ☰) */
  #phone-pill :is(#searchbar, .search-box) { position: static; }
  #phone-pill .search-box { display: flex; align-items: center; }
  #phone-pill .search-box > .icon { position: static; margin: 0 0 0 12px; flex: none; }
  /* a native feel under a thumb: no grey flash on a tap, no double-tap-to-zoom wait on a control */
  :is(#phone-pill, #phone-plus, #phone-dots, #toolbar, #overlays, #drive-pad, .ar-head, .rewind-bar) :is(button, summary, a, input), #phone-plus, .key-hint[data-run] {
    -webkit-tap-highlight-color: transparent; }
  :is(#phone-pill, #phone-plus, #phone-dots, #toolbar, #overlays, .ar-head, .rewind-bar) :is(button, summary, a), #phone-plus, .key-hint[data-run] {
    touch-action: manipulation; }
  /* flex: none — the search box's intrinsic width shrank its neighbours to 43px; the pill's own inset
     divider stays on the left edge (the UA's other three borders go), the first child has none */
  #phone-pill > button { flex: none; width: 44px; height: 44px; border-width: 0 0 0 1px; border-radius: 50%; background: none; color: inherit; display: grid; place-items: center; cursor: pointer; position: relative; }
  #phone-pill > :first-child { border-left: 0; }
  #phone-pill #searchbar { flex: 1 1 auto; min-width: 0; height: 44px; }
  #phone-pill .search-box, #phone-pill .search-box:focus-within { border: 0; background: none; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; height: 44px; border-radius: 0; }
  #phone-pill #searchbar input, #phone-pill #searchbar:not(:focus-within) input { font-size: 16px; line-height: 24px; padding: 0 8px; height: 44px; min-width: 0; }
  #phone-pill #searchbar:not(:focus-within) .search-box > .icon { color: var(--text-meta); }
  #phone-menu.warn::after { content: ''; position: absolute; top: 4px; right: 4px; width: 8px; height: 8px; border-radius: 50%; background: var(--text-danger); }
  /* with a track up the middle is its name and speed, and locate becomes ✕ */
  #phone-pill[data-track] :is(#searchbar, #phone-locate, #phone-compass), #phone-pill:not([data-track]) :is(.phone-track, #phone-close) { display: none; }
  #phone-compass .icon { rotate: var(--north, 0deg); } /* phone.js writes --north off the map's bearing */
  /* the track line is a BUTTON (phone.js): with the picture put away (its ✕, body.nocard) a tap brings it back and
     the chevron at its end says so (up: the picture rises). The pill's round-button rule above is undone here */
  #phone-pill > .phone-track { flex: 1 1 auto; width: auto; min-width: 0; height: 44px; display: flex; align-items: center; gap: 8px;
                 padding: 0 0 0 8px; border: 0; border-radius: 0; background: none; color: inherit; cursor: pointer; text-align: left;
                 font: inherit; font-size: 16px; line-height: 24px; font-weight: 700; white-space: nowrap; overflow: hidden;
                 font-variant-numeric: tabular-nums; }
  .phone-track::before { content: ''; flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--text-danger); }
  .phone-track .phone-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .phone-track .icon { display: none; flex: none; width: 20px; height: 20px; margin-right: 8px; rotate: 180deg; }
  body.nocard .phone-track .icon { display: block; }
  /* THE STRIP'S DOTS (phone.js): over the AR pane's top-left corner, under the heading band — a 44px
     target each around an 8px dot, the shown card's filled */
  #phone-dots { position: fixed; z-index: 5; left: 18px; display: flex;
                bottom: calc(env(safe-area-inset-bottom, 0px) + var(--sheeth) - 80px); }
  #phone-dots button { width: 28px; height: 44px; padding: 0; border: 0; background: none; display: grid; place-items: center; cursor: pointer; }
  #phone-dots button::before { content: ''; width: 9px; height: 9px; border-radius: 50%; background: var(--overlay-glass);
                               border: 2px solid var(--overlay-rim); box-shadow: var(--glass-shadow); } /* a ::before: the glass by its two values */
  #phone-dots button.on::before { background: var(--text-ink); border-color: var(--text-ink); }
  body.nocard #phone-dots { display: none; }
  /* THE PANE'S ✕ (phone.js #phone-hide): H — the picture slides away and the drive goes on on the map; the pill's
     name brings it back. 44px round over the picture's top-right corner, 10px in from the pane's edges; it goes
     with no card, with the picture away, and in mirror mode (the pill's ✕ is the way back there) */
  #phone-hide { position: fixed; z-index: 5; right: 20px; bottom: calc(env(safe-area-inset-bottom, 0px) + var(--sheeth) - 82px); /* 82: under the heading band, whose NE it covered at 54 */
                width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; cursor: pointer;
                color: var(--text-ink); /* its plate: glass (phone.js) */ }
  #phone-hide .icon { width: 20px; height: 20px; }
  body:not(.cards) #phone-hide, body.nocard #phone-hide, body.mirror #phone-hide { display: none; }
  /* QUIET (phone.js): a few touchless seconds into a drive the chrome drops to a whisper — the pill, the picture's ✕
     and bar, the dots, the ruler, the shut readout — and any touch wakes it at once (no transition on the way back);
     the stick, the Exit chip and the pictures keep their ink. Mirror mode's own fade is the same idea */
  body.quiet :is(#phone-pill, #phone-hide, #phone-dots, .mapboxgl-ctrl-scale, .ar-head, #performance:not(.open)) {
    opacity: .3; transition: opacity var(--fade); }
  /* THE READOUT (shell/overlaysPanel/performance/) keeps the map's bottom-left, under the stick's zone: the whole row is the tap (fps.js —
     the chevron is a mouse target), 44px tall through a ::before so the box stays the desk's; the Performance drawer unfolds as
     on the desk, 300px in a 390px screen, and the pad waits while it is open (the pad block) */
  #performance .top { position: relative; pointer-events: auto; }
  #performance:not(.open) .top::before { content: ''; position: absolute; inset: -12px -8px; }
  /* the drawer stands INSIDE the map pane: with a card up the desk's 100dvh − 180 ran its title row and chevron off
     the pane's top, behind the pill — the AR pane under it (--sheet) and the pill over it leave this much */
  #performance .perf-drawer { max-height: calc(100dvh - var(--sheet, 0px) - 200px); }
  /* THE ⊕ in the thumb corner, standing over the scale bar (the owner: the bar right under it); it leaves with a
     track up — the picture's chrome is enough */
  #phone-plus {
    position: fixed; z-index: 5; right: 20px; bottom: calc(64px + env(safe-area-inset-bottom, 0px)); /* 64: at 56 it sat 6 px into the ruler */
    width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; cursor: pointer;
    color: var(--text-ink); } /* its plate: glass (phone.js) */
  #phone-plus .icon { width: 24px; height: 24px; }
  body.cards #phone-plus { display: none; }
  /* …but a DRIVABLE one up keeps it, lifted over the picture (--sheet): its fan holds the nav tool, a touch screen's only
     way to waypoints (overlays/generic/waypoints/placing.js marks body.drivable). Not while chased — the pad's slot (a man's
     pistol, the drone's plate) stands in that corner — unless the tool is armed, whose second tap sends the order */
  :is(body.cards.drivable:not(.chase), body.cards[data-mode="nav"]) #phone-plus {
    display: grid; bottom: calc(var(--sheet, 0px) + var(--homeb, 0px) + 64px); }
  body.cards.drivable #toolbar { bottom: calc(var(--sheet, 0px) + var(--homeb, 0px) + 128px); }
  /* the tool row IS the fan: the five tools stand in a column above the ⊕, each with its name beside
     it, and only while the ⊕ is open; an armed one wears SELECTED's capsule (its list names the fan), never the
     desk's sliding pick, which is a row's: top to bottom of the bar, it banded the whole column (Scars) */
  #toolbar { position: fixed; z-index: 5; top: auto; left: auto; right: 24px; bottom: calc(128px + env(safe-area-inset-bottom, 0px));
             width: auto; flex-direction: column; align-items: flex-end; container-type: normal; }
  body:not(.tools) #toolbar { display: none; }
  #toolbar .seg-pick { display: none; }
  #toolbar .seg { flex-direction: column-reverse; gap: 12px; overflow: visible; /* no plate: each circle is its own glass */
                  background: none; border: 0; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
  #toolbar .seg > button { width: 48px; height: 48px; border-radius: 50%; position: relative; border-top: 0;
    background: var(--overlay-glass); -webkit-backdrop-filter: var(--glass-filter); backdrop-filter: var(--glass-filter);
    border: 2px solid var(--overlay-rim); box-shadow: var(--glass-shadow); }
  /* THE SELECTED VEHICLE'S BAR on a phone (phone.js puts the slot on <body>): a column of circles over the ⊕, read up
     from it as the fan is — nav first, nearest the thumb, then dome and accuracy; out of the way while the ⊕'s fan
     stands there. It grows by its scale alone: there is no row to make room in */
  #vehicle-slot { position: fixed; z-index: 5; right: 24px; margin: 0; width: auto; transition: none;
    bottom: calc(var(--sheet, 0px) + var(--homeb, 0px) + 128px); } /* the inset is in --homeb or --sheet already, as #toolbar's */
  body.tools #vehicle-slot { display: none; }
  #vehicle-bar { flex-direction: column-reverse; gap: 12px; overflow: visible; transform-origin: 50% 100%;
    background: none; border: 0; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
  #vehicle-bar > button { width: 48px; height: 48px; border-radius: 50%; position: relative; border-top: 0;
    background: var(--overlay-glass); -webkit-backdrop-filter: var(--glass-filter); backdrop-filter: var(--glass-filter);
    border: 2px solid var(--overlay-rim); box-shadow: var(--glass-shadow); }
  #vehicle-bar #nav-btn { width: 48px; padding: 0; } /* a circle: its said step is the tip, not words inside (two ids: over [data-say]) */
  #vehicle-bar #nav-btn > .say { display: none; }
  #vehicle-bar #nav-btn > .icon { margin: auto; }
  #toolbar .seg > button::after {
    content: attr(aria-label); position: absolute; right: 58px; top: 50%; translate: 0 -50%; white-space: nowrap;
    padding: 6px 8px; border-radius: var(--radius-m); color: var(--text-ink); font: 600 14px/1 var(--font);
    background: var(--glass-thick); -webkit-backdrop-filter: var(--glass-filter); backdrop-filter: var(--glass-filter);
    border: 2px solid transparent; box-shadow: var(--glass-shadow); } /* an ::after: the thick glass by its values, a readout: no rim */
  /* THE SHEET: the Overlays rises from the bottom, half the screen, a grabber at its head; shut
     it is nothing (the pill's ☰ opens it). Its first row is Rewind, its head carries day/night */
  #overlays { top: auto; left: 0; right: 0; bottom: 0; width: auto; --sidew: 100vw; max-height: 56dvh;
             z-index: 7; /* over the AR pane (3), the pad and the ⊕ (5): a sheet is the top of the stack */
             border-radius: var(--radius-xl) var(--radius-xl) 0 0; box-shadow: 0 -6px 24px var(--overlay-shadow);
             padding-bottom: env(safe-area-inset-bottom, 0px); }
  #overlays.shut { display: none; }
  /* TWO SNAP HEIGHTS (phone.js, the grabber): half, or full up to the pill, 16px short of it; the step between them
     eases, under the thumb it follows (the desk's width/radius glide is not this one's). `translate` too: a pull let go
     short of the snap jumped back, and a dismiss slides the rest of the way out before it shuts (2026-10-22) */
  #overlays { transition: max-height var(--slide) var(--ease), translate var(--slide) var(--ease); }
  #overlays.full { max-height: calc(100dvh - 86px - env(safe-area-inset-top, 0px)); }
  #overlays.dragging { transition: none; }
  #overlays-box { width: auto; }
  /* the head stands OUTSIDE the scrolling box (phone.js moves it): the grabber and the ☰ stay under the
     thumb however far the list is scrolled, and the pull is the head's (touch-action) */
  #overlays-head { margin: 12px 0 0; padding: 0 12px; touch-action: none; } /* the box's side inset: the ☰ over the rows */
  /* iOS zooms the page on the focus of a field under 16px and stays zoomed until a pinch: the track filter and a
     rename field were 12 and 14px (the pill's search already 16) (2026-10-13 review) */
  #track-filter, [contenteditable="true"] { font-size: 16px; line-height: 24px; } /* not [contenteditable="false"]: a renamed row goes back to false and must size down again */
  /* the AR pane takes no gestures of its own (interactive: false), so a double tap or a pinch on the picture was
     the browser's page zoom; pan-x keeps the strip's swipe between a kept card and the live one */
  #ar-cards { touch-action: pan-x; }
  #overlays-head::before { content: ''; position: absolute; top: 8px; left: 50%; translate: -50% 0; width: 36px; height: 5px;
                        border-radius: var(--radius-full); background: color-mix(in srgb, var(--text-ink) 22%, transparent); }
  #phone-light, #phone-share, #phone-full { width: 44px; height: 44px; display: grid; place-items: center; border: 0; background: none; color: inherit; cursor: pointer; }
  #phone-light { margin-left: auto; } /* day/night, then full screen (phone.js builds it where the browser has one), at the head's right */
  #phone-tm { display: flex; align-items: center; gap: 8px; width: 100%; height: 52px; margin-bottom: 4px; padding: 0 6px;
              border: 0; border-top: 1px solid var(--line-rule); border-bottom: 1px solid var(--line-rule);
              background: none; color: inherit; font: inherit; font-size: 16px; line-height: 24px; font-weight: 600; cursor: pointer; }
  #phone-tm .icon { width: 16px; height: 16px; margin: 0; }
  #phone-tm small { margin-left: auto; font-size: 12px; line-height: 16px; font-weight: 400; color: var(--text-meta); }
  /* the sheet over everything else of the phone's: the fan, the pad and the pill's search wait */
  body:has(#overlays:not(.shut)) :is(#toolbar, #drive-pad, #phone-plus) { visibility: hidden; }
  /* THE STRIP: Rewind's bar at the bottom edge, under the map, its row on its own line
     (the desk's row-container rules do not reach it here: it hangs off <body>) */
  body.phone #rewind .rewind-bar {
    position: fixed; z-index: 6; left: 0; right: 0; bottom: 0; height: auto; border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px)); flex-wrap: wrap; row-gap: 8px;
    animation: none; } /* its plate: the glass it wears on the desk */
  /* with a card up the strip stands ABOVE the AR pane, a floating bar over the map's foot: the picture it
     scrubs stays in view */
  body.phone.cards #rewind .rewind-bar { bottom: calc(var(--sheet, 0px) + 10px); left: 10px; right: 10px; border-radius: var(--radius-xl); padding-bottom: 12px; }
  body.phone #rewind .rewind-row { flex: 1 1 100%; order: -1; }
  body.phone #rewind .rewind-shut { margin-left: auto; }
  /* the strip stands where the ⊕ and the scale bar stand: they wait */
  body:has(#rewind .rewind-bar:not([hidden])) :is(#phone-plus, .mapboxgl-ctrl-bottom-right) { display: none; }
  /* THE PAD rides the MAP (the owner, 2026-10-13: the picture below keeps its own chrome): the stick's zone in the
     map's lower-left, standing on the readout, 140px wide so the F · Exit chip hung under a centred car is never its
     (the chips live inside #map, under this fixed pad); the slot — a man's pistol, the drone's plate, a car none — in
     the lower-right over the scale bar. The pad's box IS the map's, so a put-away picture (--sheet 0) grows it too */
  #drive-pad { position: fixed; z-index: 5; display: block; padding: 0; height: auto;
               left: env(safe-area-inset-left, 0px); right: env(safe-area-inset-right, 0px); top: 0;
               bottom: calc(var(--sheet, 0px) + var(--homeb, 0px));
               transition: bottom var(--slide) var(--ease); }
  #drive-pad .stick-zone { position: absolute; left: 0; bottom: 40px; width: 140px; height: min(300px, 55%); }
  #drive-pad .stick { left: 80px; top: calc(100% - 100px); }
  #drive-pad .slot { position: absolute; right: 12px; bottom: 56px; }
  body:has(#performance.open) #drive-pad { visibility: hidden; } /* the Performance drawer unfolds under the zone: the pad waits, as under the sheet */
  /* the keycap hints (parts/tracks/hint.js/): a keycap means nothing to a thumb. One that RUNS something (F · Drive,
     F · Exit) is a tappable chip with its word alone; one that only names a key (W A S D · Move) goes */
  .key-hint:not([data-run]) { display: none; }
  .key-hint[data-run] { height: 44px; padding: 0 16px; border-radius: var(--radius-full); font-size: 14px; line-height: 20px; text-shadow: none; color: var(--text-ink);
    background: var(--overlay-glass); -webkit-backdrop-filter: var(--glass-filter); backdrop-filter: var(--glass-filter);
    border: 2px solid var(--overlay-rim); box-shadow: var(--glass-shadow); } /* a CSS-only plate: the glass by its values — a chip here, a cap on the desk */
  .key-hint[data-run] .key-caps { display: none; }
  /* Under 560px the promotion bar is a bottom SHEET, the move the AR view cards make: there is no room beside
  a place on a phone. This is the one arrangement where the card CANNOT hang under the pill —
  below it is the bottom of the screen — so it stacks upward. place() writes no inline left/top
  here, or these rules never win. */
  .nato-bar { position: fixed; left: 0; right: 0; bottom: calc(var(--sheet, 0px) + var(--homeb, 0px)); top: auto; width: auto; /* above the AR sheet, off the home indicator */
    flex-direction: column-reverse; padding: 0 8px 8px;
    filter: drop-shadow(0 -6px 24px var(--overlay-shadow)); }
  .nato-pill { flex-direction: column-reverse; } /* the names over the drawer over the row: the thumb's row stays at the foot */
  .nato-row { height: 52px; }
  .nato-hits { max-height: 40vh; }
  /* ON ITS SIDE: the AR pane stands BESIDE the map, each half the width and the whole height — a 45dvh
     pane under a 390px-high map is two letterboxes. The 8 px seam between them is the HANDLE (arview/'s
     own drag, by delta): equal halves at rest, the thumb moves the split for the session, 260 px to 60 %
     of the screen. The pill keeps to the map's half, the pad rides the map, the sheet rises higher (it
     has less to rise into) */
  @media (orientation: landscape) {
    body { --fw: calc(50vw - 4px); --homeb: env(safe-area-inset-bottom, 0px); } /* the pane's width: half, less half the seam; the map's foot is the screen's */
    body.cards { --dock: calc(var(--fw) + 8px); --sheet: 0px; --homeb: env(safe-area-inset-bottom, 0px); }
    #ar-cards {
      top: 0; bottom: 0; left: auto; right: 0; width: var(--fw); height: auto;
      /* away is still DOWN: parked off the right edge it widened the page, and a phone's layout viewport
         grows to fit a wide page — the pill and the ⊕ measured 1261 px of a 844 px screen */
    }
    /* body.cards on both, and margin: 0 — the coarse block below (later, the desk's thumb seam: 24 px, pulled 7 px
       over the column, the line inset 11) wins on order otherwise: the line computed 0 px wide and the box lay 7 px
       over the picture (2026-10-16 review) */
    body.cards #ar-seam { display: block; top: 0; bottom: 0; right: var(--fw); width: 8px; margin: 0; }
    body.cards #ar-seam::after { inset: 0 3px; } /* the desk's 2 px line, centred in the 8 */
    #ar-seam::before { content: ''; position: absolute; inset: 0 -18px; } /* a thumb's 44 px over an 8 px bar */
    body.cards #phone-pill { right: calc(var(--dock) + 22px); } /* the map's half */
    /* …the ⊕ and its fan too: at the screen's edge they stood on the picture, the ⊕ over its minimap, the fan's top tool
       over the band's NE (2026-10-26). --dock is 0 with the picture put away (H), so they go back to the edge */
    body.cards #phone-plus { right: calc(var(--dock) + 20px); }
    body.cards #toolbar { right: calc(var(--dock) + 24px); }
    /* 390 px has no room for the fan under the pill (its column is 228 px, 200 between the pill and the ⊕): open, it
       stands OVER the pill's end — under it, its top tool (Drop a soldier) took no tap (2026-10-26) */
    #toolbar { z-index: 6; }
    #phone-dots { left: calc(100vw - var(--fw) + 18px); bottom: auto; top: calc(46px + env(safe-area-inset-top, 0px)); }
    body.cards:not(.nocard) #drive-pad { right: var(--dock); } /* the map's half */
    #phone-hide { bottom: auto; top: calc(20px + env(safe-area-inset-top, 0px)); right: calc(20px + env(safe-area-inset-right, 0px)); }
    /* the SHEET is a SIDE PANEL here, the whole height at the left with the map beside it (ATAK's drawer, a maps app's
       landscape sheet): a bottom sheet in 390 px gave its head, filter and Rewind row 270 px and showed two rows (UX
       audit, 2026-10-22). Its pull is LEFT (phone.js), so no grabber bar; ‹ shuts it too */
    #overlays, #overlays.full { top: 0; bottom: 0; left: 0; right: auto; --sidew: min(360px, 50vw); width: var(--sidew); max-height: none;
      border-radius: 0 var(--radius-xl) var(--radius-xl) 0; box-shadow: 6px 0 24px var(--overlay-shadow);
      padding: env(safe-area-inset-top, 0px) 0 env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px); }
    #overlays-head::before { display: none; }
    body.phone.cards #rewind .rewind-bar { right: calc(var(--dock) + 10px); } /* the map's half (the phone block's own specificity): it spanned both and lay over the pane's foot, the stick zone */
  }
}

/* ── A CARD'S HEAD ON A PHONE, AND IN MIRROR MODE: its pill alone, short, in the picture's BOTTOM-right corner (the owner,
   2026-10-13: where the handbrake stood, once the stick and the slot moved to the map) — the glasses first, then
   the background map and the forward/ground view while the card has them (card.js un-hides those two per track).
   Nothing else: the name and ✕ are the phone pill's, the picture's own ✕ is the shell's (#phone-hide, top-right);
   minimap, sight, graph and keep are the desk's. Clear of the heading band at the top and of a flight's altitude
   box at mid-height; the minimap disc keeps the lower-LEFT. Keyed on body.phone (shell/mapcontrols/phone.js, the
   width query) OR body.mirror: a phone on its side leaves the width query, and in mirror mode this pill is the way
   back when there is no other (there it stays top-right, under the band: the pill is at the bottom). */
body:is(.phone, .mirror) .ar-head { position: absolute; z-index: 3; right: calc(12px + env(safe-area-inset-right, 0px)); top: auto;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px)); /* the picture's foot is under the home indicator on a phone */ margin: 0; min-height: 0; }
body:is(.phone, .mirror) .ar-head :is(.ar-label, .ar-info, .ar-close, .ar-minimap, .ar-sight, .ar-graph, .ar-keep) { display: none; }
/* …but a car's card has its gauges in both bottom corners (arview/cluster/cluster.js `corners`), so its pill stands between them */
body:is(.phone, .mirror) .ar-card:has(.cluster:not(.bare)) .ar-head { right: 50%; translate: 50% 0; }

/* ── MIRROR MODE (card/popout/ mirror(), the glasses button on a phone or tablet): the live card's
   picture IS the screen, which a pair of USB-C glasses shows as is — sideways once the page has
   gone full screen and locked landscape, in whatever orientation otherwise. The map and the ⊕
   stand down; the pill drops to the bottom between the thumbs and fades until touched; the stick
   and the slot stay, faded the same. Keyed on body.mirror, not the phone block: a tablet mirrors too. */
body.mirror :is(#map, #phone-plus, #phone-dots, #toolbar, #performance, .mapboxgl-control-container) { visibility: hidden; }
body.mirror #ar-cards { position: fixed; inset: 0; width: auto; height: auto; z-index: 8; transform: none; visibility: visible; overflow: hidden; }
body.mirror .ar-card { position: relative; width: 100%; height: 100%; max-height: 100%; margin: 0; }
body.mirror .ar-card:not(.mirrored) { display: none; } /* the card whose glasses button was pressed (popout.js mirror), not the first in the column: a kept card's press showed the live card, and an idle live card beside a kept one showed nothing (2026-10-13 review) */
body.mirror .ar-frame { flex: 1 1 auto; min-height: 0; aspect-ratio: auto; border: 0; border-radius: 0; }
body.mirror .ar-head { position: absolute; z-index: 3; right: calc(12px + env(safe-area-inset-right, 0px)); top: calc(48px + env(safe-area-inset-top, 0px)); bottom: auto; }
body.mirror #phone-pill { z-index: 9; top: auto; bottom: calc(22px + env(safe-area-inset-bottom, 0px)); left: 50%; right: auto; width: min(360px, 92vw);
                          translate: -50% 0; opacity: .3; transition: opacity var(--fade) var(--wake); }
body.mirror #drive-pad { position: fixed; z-index: 9; display: block; top: auto; height: 220px; padding: 0;
                         left: calc(10px + env(safe-area-inset-left, 0px)); right: calc(10px + env(safe-area-inset-right, 0px));
                         bottom: calc(8px + env(safe-area-inset-bottom, 0px)); }
body.mirror #drive-pad { height: 300px; width: auto; } /* width: auto — the sideways block's half-width pad is not this one */
/* sideways the disc grows with the frame (a 150px disc 120px in from the corner): the zone reaches past
   it and the ring rests to its right, clear of the pill under it */
body.mirror #drive-pad .stick-zone { position: absolute; left: 0; bottom: 0; width: 440px; height: 300px; }
body.mirror #drive-pad .stick { left: 350px; top: calc(100% - 150px); }
body.mirror #drive-pad .slot { position: absolute; right: 12px; bottom: 30px; }
body.mirror :is(#drive-pad .stick:not(.on), #drive-pad .slot, .ar-head) { opacity: .3; transition: opacity var(--fade) var(--wake); }
body.mirror :is(#phone-pill, .ar-head):is(:hover, :focus-within, :active), body.mirror #drive-pad .slot:active { opacity: 1; transition: none; }
/* an iPhone's Safari has no element full screen and no orientation lock (WebKit, won't fix), and the
   glasses mirror the phone AS HELD — upright, a strip down the middle of their frame. The one thing
   the page can do is say so, until the phone is turned */
@media (orientation: portrait) {
  /* upright, the pill sits under the hint, off the minimap disc in the picture's lower-left; the card's
     own pill under that, clear of the hint and of the heading band */
  body.mirror #phone-pill { top: calc(108px + env(safe-area-inset-top, 0px)); bottom: auto; }
  body.mirror .ar-head { top: calc(168px + env(safe-area-inset-top, 0px)); } /* under the pill: upright, the band is three times its size */
  body.mirror::after {
    content: 'Turn the phone sideways'; position: fixed; z-index: 9; top: calc(60px + env(safe-area-inset-top, 0px)); left: 50%; translate: -50% 0;
    padding: 8px 12px; border-radius: var(--radius-full); color: var(--text-ink); /* an ::after: the glass by its values, a readout: no rim */
    background: var(--overlay-glass); -webkit-backdrop-filter: var(--glass-filter); backdrop-filter: var(--glass-filter);
    border: 2px solid transparent; box-shadow: var(--glass-shadow); font: 600 14px/1 var(--font); white-space: nowrap; }
}

/* ── REWIND (shell/overlaysPanel/rewind.js/) ────────────────────────────────────
   In the row with the tools: a circle until it is opened, then the whole control. The group
   is `display: contents`, so the button and the bar are the ROW's own items: on a wide row the
   bar takes the circle's place and the row's slack, and the search pill takes the rest — no
   width is written here; on a narrow row (the container block below) the circle stays, pressed,
   and the bar takes the line under the row. */
#rewind { display: contents;
  /* the open button is one of the row's circles (#toolbar > button's rule names it) and must not
     be squeezed by the pill beside it; open on a wide row, the bar stands in its place */
  .rewind-open { flex: none; }
  .rewind-bar {
    height: 44px; border-radius: var(--radius-full); color: var(--text-ink); } /* its plate: glass (rewind.js) */
  .rewind-open:disabled { opacity: .45; cursor: not-allowed; }
  .rewind-bar {
    flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; padding: 0 6px 0 6px; }
  /* the row: the track and the shape of the record share one box, so the band is the thing
     being scrubbed rather than a chart that happens to sit near it */
  .rewind-row { position: relative; flex: 1; min-width: 80px; height: 26px; }
  /* the instant under the pointer, before committing to it */
  .rewind-tip {
    position: absolute; bottom: 26px; translate: -50% 0; z-index: 2; pointer-events: none;
    padding: 2px 6px; border-radius: var(--radius-s); white-space: nowrap; background: var(--glass-thick); /* a chip INSIDE the glass bar: the thick tint alone, no blur (a plate in a plate doubles the tint) */
    font-size: 12px; line-height: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
  /* the track: a tube with rounded caps, filled to the cursor. --v is set by the module, so the
     fill is the SAME number the thumb sits on — one source, never two that drift apart. */
  .rewind-track {
    position: absolute; left: 0; right: 0; bottom: 0; width: 100%; height: 22px; margin: 0;
    appearance: none; background: none; cursor: ew-resize; }
  /* the history already played is the blue, the rest of the tube the rule */
  .rewind-track::-webkit-slider-runnable-track {
    height: 10px; border-radius: var(--radius-full);
    background: linear-gradient(90deg, var(--time-fill) var(--v, 100%), var(--line-rule) var(--v, 100%)); }
  .rewind-track::-webkit-slider-thumb {
    appearance: none; width: 18px; height: 18px; margin-top: -4px;
    border-radius: 50%; background: var(--time-thumb);
    border: 2.5px solid var(--time-mark); box-shadow: 0 1px 3px var(--overlay-shadow); }
  .rewind-play, .rewind-shut {
    width: 30px; height: 30px; flex: none; display: grid; place-items: center;
    border-radius: 50%; color: var(--text-ink); cursor: pointer; }
  .rewind-shut { color: var(--text-meta); }
  /* the wall clock is the readout; how far back is the small print under it */
  .rewind-clock {
    flex: none; display: flex; flex-direction: column; min-width: 7.2em; }
  .rewind-at { font-size: 14px; line-height: 16px; font-variant-numeric: tabular-nums; }
  .rewind-ago {
    font-size: 12px; line-height: 12px; color: var(--text-meta); font-variant-numeric: tabular-nums; }
  &[data-scrubbed] .rewind-at { color: var(--text-danger); }
  &[data-scrubbed] .rewind-ago { color: var(--text-danger); opacity: .8; }
  .rewind-live {
    flex: none; display: flex; align-items: center; gap: 4px; padding: 4px 8px 4px 8px;
    border: 1px solid var(--line-border); border-radius: var(--radius-full); cursor: pointer;
    background: none; color: var(--text-soft); font: inherit; font-size: 12px; line-height: 16px;
    font-weight: 700; }
  &:not([data-scrubbed]) .rewind-live {
    color: var(--text-on); border-color: var(--text-on); }
  /* in the past it is THE button: solid green, the one that brings now back */
  &[data-scrubbed] .rewind-live {
    background: var(--text-on); color: var(--surface-panel); border-color: var(--text-on); }
  .rewind-live .icon, .rewind-play .icon, .rewind-shut .icon { width: 16px; height: 16px; }
  /* on a wide row the bar FOLDS out of the clock it stands in for and back into it (rewind.js, util/dom.js fold): its
     max-width eased between the circle and its own width, the search pill riding its edge — it jumped 852 px each
     way — and folding shut it stays in, clipped, the clock held back until it lands (2026-10-22) */
  .rewind-bar.morph { overflow: hidden; }
  .rewind-bar.morph[hidden] { display: flex !important; }
  &:has(.rewind-bar.morph) .rewind-open { display: none; }
  .rewind-track::-webkit-slider-thumb { transition: transform var(--tap) var(--ease); }
  .rewind-track:active::-webkit-slider-thumb { transform: scale(1.3); }
  &[data-scrubbed] .rewind-live .icon { animation: rewind-pulse var(--handover) ease-in-out infinite; }
}

#rewind-open[aria-expanded="true"] { display: none; }

/* THE PAST, framed: a hairline of the clock's own red round the whole page while the map shows
   the past, so any corner of the screen says this is not now. Under every click. */
.rewind-frame { position: fixed; inset: 0; z-index: 20; pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--text-danger); }

/* ── the search, with Rewind open ──────────────────────────────────────
   The track takes the row's slack, so the search circle opened has nowhere to go: it takes the
   row's whole width and lays over the control it displaced. Not on a phone: there the open
   pill already answers to the screen (the 560px block), and this rule's left/right fought its
   width and pushed the pill off the right edge. */
@media (min-width: 561px) {
  body:has(#rewind .rewind-bar:not([hidden])) #searchbar:focus-within {
    position: absolute; left: 0; right: 0; z-index: 4; flex: 0 0 44px; }
}

/* ── polish ───────────────────────────────────────────────────────────────────
   The bar grows out of the button it replaced rather than appearing; the knob answers the
   pointer. Both stand down under prefers-reduced-motion (the global rule at the end). */
@keyframes rewind-in { from { opacity: 0; transform: scaleX(.94); transform-origin: left center; } }

/* live is a state, not a button you are being asked to press: it only pulses when it is NOT
   where you are, so a live map has nothing blinking on it */
@keyframes rewind-pulse { 50% { opacity: .45; } }

/* A NARROW ROW — a phone, or a card open beside a laptop's map — has no width left beside
   five tools: the OPEN Rewind takes a line of its own rather than shrinking to nothing
   (shut, it is a circle and stays in the row). The ROW's own width decides (#toolbar is the
   query container), so the two cases are one rule. This has to sit AFTER the rules above,
   not up in the shared phone block — same specificity, and source order decides. */
@container (max-width: 700px) {
  /* `order`: laid out after the search circle, so the row above the bar stays whole */
  #rewind .rewind-bar { flex: 1 0 100%; order: 1; animation: rewind-in var(--fade) var(--ease) both; } /* a line of its own: it fades in, nothing to fold from */
  /* the circle stays in the row, pressed: the bar is UNDER it, not in its place */
  #rewind-open[aria-expanded="true"] { display: block; }
  /* even a whole line is ~100 px short of the control's min-content, so it wraps into two:
     the track and its readout on top, the transport under them. Left to overflow instead, the
     live and close buttons walked off the map's right edge onto the page. */
  /* wrapped to two or three lines it is a card, not a capsule: a 999 px corner on a 118 px-tall bar made it an oval
     with the live chip and the ✕ in its curve (the audit, 2026-10-22) */
  #rewind .rewind-bar { flex-wrap: wrap; height: auto; padding: 6px; row-gap: 8px; border-radius: var(--radius-xl); }
  #rewind .rewind-row { flex: 1 1 140px; }
  #rewind .rewind-clock { order: 1; }
  #rewind .rewind-play { order: 2; }
  #rewind .rewind-live { order: 4; }
  #rewind .rewind-shut { order: 5; margin-left: auto; }
}
/* A ROW NARROWER THAN THE OPEN SEARCH PILL (a phone): the box pops out UNDER the row at the
   row's full width, and the circle it left stays in the row, pressed — Rewind's own
   pattern. Absolute to the toolbar: `top: 100%` is under every line of it, the open Time
   Machine's too. A tap on the pressed circle blurs the input, which is how it closes. */
@container (max-width: 500px) {
  #searchbar:focus-within { flex: 0 0 44px; height: 44px; display: grid; place-items: center;
    border-radius: 50%; background: var(--highlight-wash); box-shadow: inset 0 0 0 2px var(--line-highlight); } /* pressed: the SELECTED rule's highlight */
  #searchbar:focus-within > .icon { display: block; }
  #searchbar:focus-within .search-box {
    position: absolute; left: 0; right: 0; top: calc(100% + 10px);
    animation: rewind-in var(--fade) var(--ease) both; }
}

/* ── COCKPIT MODE (arview/cluster/cockpit.js) ──────────────────────────────────────────────
   The instrument cluster the MAIN MAP wears. It is the whole display, not a panel on one: the
   map goes edge to edge, every floating control comes off, and the module forces the night
   basemap — so this ground is the only ground these colours are measured against, and `cockpit`
   in theme.js has no NIGHT twin.
   AN ARC IS AN SVG PATH, not a conic-gradient: a gradient stop is a hard edge, so a sweep drawn
   that way can never have a rounded end. The path carries `pathLength=100`, so a reading is a
   dash length in PER CENT and a composition changes the geometry without touching JS.
   THE ARC IS SETTLED: ONE width for the channel and the run inside it — a recess is read from
   its colour, never from a margin of ground left showing either side. Both gauges fill from the
   bottom of the sweep, and each reads in one direction: km/h on the left, the rpm of a simulated
   gearbox on the right.
   Decoration only: nothing takes the pointer, and the chase's own gestures reach the canvas
   untouched. */
body.chase:is(.cockpit, .eye) { --dock: 0px; } /* outranks body.cards: the map takes the cards' width back */
/* A CLUSTER FILLS ITS SCREEN: no desk, no frame, no radius. GL JS resizes itself. */
/* …in ONE STEP, under the cards, which slide away over it; and on the way out it stays
   full-bleed until the cards have slid back over it (`ck-leaving`), so no frame shows the page's
   ground. NEVER TRANSITIONED: each frame an edge moves, GL JS clears its canvas and redraws a
   frame later, so a map whose box is moving shows a BLANK map for the whole run (Scars) */
body.chase:is(.cockpit, .eye) #map, body.ck-leaving #map { inset: 0; border: none; border-radius: 0; transition: none; }
/* every piece of the app's own furniture, by the one rule. KEYFRAMES, NOT A TRANSITION: the
   Overlays, the tool row and body.relight (which fires on the same frame, from day) each own
   `transition` on these already, and a second rule would replace theirs. It fades in the first
   30% of the wake and comes back in the last 40% of the exit, after the cluster has gone —
   `ck-leaving` is cockpit.js's, for exactly that window */
body.chase.cockpit :is(#overlays, #toolbar, #performance, .mapboxgl-control-container, #ar-seam) {
  animation: ck-shell-out calc(var(--wake) * .3) var(--ease) forwards;
}
body.ck-leaving :is(#overlays, #toolbar, #performance, .mapboxgl-control-container, #ar-seam) {
  animation: ck-shell-in calc(var(--wake) * .24) var(--ease) calc(var(--wake) * .36) both;
}
@keyframes ck-shell-out { to { opacity: 0; visibility: hidden; } }
@keyframes ck-shell-in { from { opacity: 0; visibility: hidden; } }
/* The column keeps `visibility` rather than display: a display:none map measures 0×0 and throws
   its tiles away (see #ar-cards). It SLIDES OUT with the map's right edge, on the same clock:
   hidden at once, it left the page's own ground showing where it stood until the map caught up */
body.chase.cockpit #ar-cards {
  transform: var(--away); visibility: hidden;
  transition: transform var(--slide) var(--ease), visibility 0s var(--slide);
}
/* …and on the way out it comes back with the shell, not over a cluster still on the glass: it
   lands just before the exit ends, where the map steps back under it */
body.ck-leaving #ar-cards {
  transition: transform var(--slide) var(--ease) calc(var(--wake) * .3),
              visibility 0s calc(var(--wake) * .3);
}

/* EYE MODE (maps/mapcam/, arview/eye.js): the MAIN map is his eye, edge to edge. The shell and the
   cards step away as they do under the cluster, in one step (a moving map box draws blank frames,
   the cockpit's Scars); the Performance readout stays, it is the owner's verdict. His AR card is parked. The map's edge
   and --dock are the cockpit's two rules (above, `:is(.cockpit, .eye)`). */
body.chase.eye :is(#overlays, #toolbar, .mapboxgl-control-container, #ar-seam) {
  animation: ck-shell-out calc(var(--wake) * .3) var(--ease) forwards;
}
/* …and back on the way OUT of his eye, over the camera's own blend (mapcam.js wears `eye-leaving`
   for its length): the cluster's return, so the shell does not pop while the picture is still moving */
body.eye-leaving :is(#overlays, #toolbar, .mapboxgl-control-container, #ar-seam) {
  animation: ck-shell-in calc(var(--wake) * .24) var(--ease) calc(var(--wake) * .36) both;
}
body.chase.eye #ar-cards { transform: var(--away); visibility: hidden; transition: none; }
body.eye .key-hints { display: none; }
/* THE HUD FRAME in a driver's seat (maps/mapcam/hudframe.js): the HUD's virtual image over his picture, empty, in the FoV's pen —
   the fancy button's style by day (its cyan edge, its 20 % fill: theme.js fov), the yellow after dark; placed per frame inline */
.hud-frame {
  position: absolute; z-index: 1; pointer-events: none; box-sizing: border-box; border-radius: var(--radius-s);
  border: 2px solid var(--fov-line-day); background: color-mix(in srgb, var(--fov-line-day) 20%, transparent);
}
body[data-light="night"] .hud-frame { border-color: var(--fov-line); background: color-mix(in srgb, var(--fov-line) 14%, transparent); }
.hud-frame-said {
  position: absolute; left: -2px; bottom: calc(100% + 4px); white-space: nowrap;
  font-size: 12px; line-height: 16px; font-weight: 600; color: var(--fov-line-day); text-shadow: 0 0 3px var(--overlay-scrim);
}
body[data-light="night"] .hud-frame-said { color: var(--fov-line); }
/* H on the map: the AR view cards put away and the map full width (the cards park, card.js show).
   The column SLIDES away with the map's edge, as it does when the last card empties — arview.js
   holds GL for it the same way. display:none took it off in one frame, 280 ms before the edge */
body.cards.nocard { --dock: 0px; }
body.nocard #ar-seam { display: none; }
body.nocard #ar-cards {
  transform: var(--away); visibility: hidden;
  transition: transform var(--slide) var(--ease), visibility 0s var(--slide);
}
.eye-frame { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; }
body:not(.eye) .eye-frame, .eye-frame > .ar-hud { display: none; }

/* A WEARER'S VISOR DISPLAY (arview/eye.js): the frame is his eye, and the cluster lives in this rectangle, level and
   unclipped; eye.js writes its place and size per frame */
.ar-display { position: absolute; z-index: 1; pointer-events: none; }

/* ── THE CLUSTER (arview/cluster/cluster.js): ONE OVERLAY for every driving view — the main map's cockpit (#cockpit, below)
   and a car's AR view card (.ar-frame > .cluster) wear the same instruments in the same places, and the only
   difference is the map under it (the owner, 2026-10-25). Left the speedometer with the posted limit, right the pod
   (the minimap under M, else the tachometer), the compass band across the top, the BRAKE badge, the rail under it all.
   EVERY SIZE IS THE OVERLAY'S OWN: a container, so `cqh`/`cqw` lay it out at a dashboard's size and a card's alike,
   and a floor of 12 px (the design system's) keeps a card's small print legible ── */
.cluster {
  position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden;
  container-type: size; font-variant-numeric: tabular-nums;
  color: var(--cluster-ink);
  --g: min(42cqh, 26cqw, 400px);   /* the gauge's own box */
  --w: 3.44;                       /* the channel AND the run in it, as a share of that box */
  --glow: 7px;                     /* the bloom around the reading, never around the channel */
  --num: .294;                     /* the big numeral, as a share of the box */
  --edge: .18;                     /* how far in from the edge a gauge stands, in gauges */
  /* the sweep: 137° of a radius-41 circle centred in the box, 0 at the bottom end. The ends of
     the scale stand at its own end points, set out to radius 48 — never on the arc */
  --d: path("M 31.39 86.53 A 41 41 0 0 1 38.70 10.59");
  --lo-x: 28.2%; --lo-y: 92.8%; --hi-x: 36.8%; --hi-y: 3.9%;
}
.cluster > * { position: absolute; }
/* an emptied card: nothing followed, no instruments (a clicked camera closes with it, card/pip/) */
.cluster.bare { display: none; }
/* AN AR VIEW (`corners`, cluster.js): glasses or a HUD, so the picture stays the picture — both gauges in its two bottom
   corners, each the size the card's minimap always was and at its height (--g and --in, below), a clicked camera over the right one, and no rail (the owner, 2026-10-25) */
/* the corners' two numbers, off the picture's width: a disc 18 % of it, never under 84 px (the pod is the disc over its slot's
   88 %), its centre 14.1 % in — one rule for a card and a visor, as the dashboard's are one rule (the owner, 2026-10-26) */
.cluster.corners { --g: max(20.45cqw, 95.45px); --in: 14.1cqw; }
.cluster.corners :is(.ck-gauge, .ar-cam) { top: auto; transform: none; bottom: calc(var(--in) - var(--g) / 2); }
.cluster.corners .ck-speed { left: calc(var(--in) - var(--g) / 2); }
.cluster.corners :is(.ck-rpm, .ar-cam) { right: calc(var(--in) - var(--g) / 2); }
/* …and its compass band across the picture's top: never under 43 px, the height band.js letters at 1× (BAND_UNITS) */
.cluster.corners .ck-compass { top: 0; width: 100%; height: max(43px, 17.2cqh); }
.cluster.corners :is(.ck-gear, .ck-batt) { display: none; }
/* …its rail is a face's facts alone, centred between the two corners: a soldier's grid, a plane's squawk — in the day's ink
   on a card's day picture, as the figures are (.ck-val) */
.cluster.corners .ck-facts { margin: 0 auto; }
:where(body:not([data-light="night"]) :not(.blank) > .cluster.corners) .ck-facts { color: var(--text-ink); }
/* A SIDE WITH NOTHING ON IT (cluster.js `none`): a side the face neither dials nor paints (a man on foot has no
   speedometer, a bus no tachometer) — it draws nothing, and its pod's slot still holds the minimap */
.ck-gauge.none > :not(.ck-minimap-slot) { display: none; }
/* THE WAKE'S STAGES (#cockpit, below) are registered at 1: anywhere the cockpit is not waking them — a card — every
   stage is whole */
@property --s-glass { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --s-house { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --s-draw { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --s-fill { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --s-read { syntax: '<number>'; inherits: true; initial-value: 1; }
@property --s-rail { syntax: '<number>'; inherits: true; initial-value: 1; }

/* ── the two gauges: one arc, two readings ── */
.ck-gauge {
  width: var(--g); height: var(--g); top: 50%; transform: translateY(-50%);
  display: grid; place-content: center; text-align: center;
}
/* the number sits on the arc's OWN CENTRE, which is what ties the reading to the sweep around it */
.ck-speed { left: calc(var(--g) * var(--edge)); }
.ck-rpm { right: calc(var(--g) * var(--edge)); }
/* NO `vector-effect: non-scaling-stroke`: it measures dashes in screen pixels against a path
   measured in user units, so the pattern repeated 1.8× and every gauge read as two lit segments
   with a gap (Scars). Stroke weight is therefore a share of the gauge box, which is the right
   unit for a gauge anyway: the ring thickens with the instrument. */
.ck-arc { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.ck-arc-rpm { transform: scaleX(-1); } /* mirrored, so both sweeps open toward the road */
.ck-arc path { d: var(--d); fill: none; stroke-linecap: round; stroke-width: var(--w); }
/* the channel is cut in the ground colour with a little of the track mixed back in, or a groove
   over dark buildings is a groove over nothing */
.ck-track { stroke: color-mix(in srgb, var(--cluster-ground) 52%, var(--cluster-track)); }
/* THE CHANNEL DRAWS IN from the bottom of the sweep while the cluster wakes; faded under a
   pixel for the reason the lit run is (below): a dash of length zero is a dot */
.ck-track {
  stroke-dasharray: calc(var(--s-draw) * 100px) 100px;
  opacity: min(1, calc(var(--s-draw) * 400));
}
/* A ZERO READING DRAWS NOTHING. `stroke-linecap: round` puts a round cap on each end of every
   dash, and a dash of length zero is two caps back to back — a dot sitting at the bottom of an
   empty gauge. There is no conditional cap in CSS, so the run is faded out instead, and the
   multiplier is large enough that any reading a pixel wide is already fully opaque. */
.ck-lit { filter: drop-shadow(0 0 var(--glow) currentcolor); }
.ck-arc-speed .ck-lit { opacity: min(1, calc(var(--speed, 0) * var(--s-fill) * 400)); }
.ck-arc-rpm .ck-lit { opacity: min(1, calc(var(--rpm, 0) * var(--s-fill) * 400)); }
/* EVERY DASH LENGTH CARRIES px. `pathLength=100` makes one px one per cent of the sweep, but a
   BARE calc() in stroke-dasharray serialises as a percentage, and a percentage there is measured
   against the viewport diagonal rather than the path — which repeated the pattern (Scars) */
.ck-arc-speed .ck-lit {
  stroke: var(--cluster-speed); color: var(--cluster-speed);
  /* × the wake's fill: the run FILLS TO THE READING, never past it — no self-test sweep, which
     would show a car doing 50 a moment of 160 */
  stroke-dasharray: calc(var(--speed, 0) * var(--s-fill) * 100px) 100px;
}
/* THE REDLINE IS PAINT, NOT A READING: it lies in the channel over the last 8% of the sweep —
   where the simulated box changes up — the run passes over it, and it never blooms. Red is
   reserved for faults everywhere else in this cluster; a tachometer's redline is the one
   exception every car on the road already makes. */
.ck-arc-rpm .ck-band {
  stroke: var(--cluster-redline); filter: none;
  stroke-dasharray: 8px 100px; stroke-dashoffset: -92px;
  /* it lies in the last 8% of the channel, so it arrives as the channel's draw reaches it */
  opacity: clamp(0, (var(--s-draw) - .92) / .08, 1);
}
.ck-arc-speed .ck-band, .ck-rpm:not(.redline) .ck-band { display: none; } /* the redline: a face's that says so (a car's) */
.ck-arc-rpm .ck-lit {
  stroke: var(--cluster-rpm); color: var(--cluster-rpm);
  stroke-dasharray: calc(var(--rpm, 0) * var(--s-fill) * 100px) 100px;
}
/* the tachometer's four figures against the speed's two: the numeral is sized off the gauge box,
   so the wider reading is narrowed rather than allowed to run out over the sweep */
.ck-val {
  /* SF Pro Rounded at 300, chosen from six the owner walked: at a glance, in the corner of an
     eye, a rounded terminal reads as a figure a fraction sooner than a cut one. A system face —
     this app ships no fonts and has no build step — with the grotesk behind it for a machine
     that has no rounded face at all */
  font: 300 calc(var(--g) * var(--num)) / .9 ui-rounded, 'SF Pro Rounded', system-ui, sans-serif;
  letter-spacing: -.01em;
  /* THE TWO FIGURES ALONE wear a glow, the one the card's old speedometer wore (the owner, 2026-10-26: "the shadow …
     was only for the speed number and RPM number … make it look like that"): after dark — the cockpit is always — the
     light figure on a dark glow; by day on a card's picture the dark figure on a light one, below */
  text-shadow: 0 0 4px var(--anno-night-glow), 0 0 9px var(--anno-night-glow);
}
:where(body:not([data-light="night"]) :not(.blank) > .cluster.corners) .ck-val {
  color: var(--text-ink); text-shadow: 0 0 4px var(--text-on-dark), 0 0 8px var(--text-on-dark);
}
/* THE NUMERALS ARE INK, both of them: the owner tried them lit in their gauge's own colour and
   put them back. The colour belongs to the SWEEP — a numeral is read, an arc is glanced at, and
   two big coloured figures either side of the road is the screen shouting. */
/* …except for a tenth of a second at an upshift, which is what a shift light IS: the tach blooms
   and its figure takes the gauge's colour. Nothing moves and nothing is added to the screen. */
.ck-rpm.shift .ck-val {
  color: var(--cluster-rpm); text-shadow: 0 0 26px var(--cluster-rpm);
}
.ck-rpm.shift .ck-lit { filter: drop-shadow(0 0 calc(var(--glow) * 2.8) currentcolor); }
.ck-rpm .ck-val { transition: color var(--fade) linear, text-shadow var(--fade) linear; }
/* the tachometer's unit, under its figure where the speed's roundel hangs (cluster.js `per`): the figure is in hundreds, so
   both numerals are two digits at one size (the owner, 2026-10-26); a face's own dial carries its own unit in its cap.
   FLUSH under the figure on a 12 px line: hung a roundel's gap lower it shared a row with the scale's 0 at the arc's end,
   ink 2.7 px apart on a card's 95 px gauge, and read "×1000" (2026-10-26) */
.ck-per { position: absolute; left: 50%; top: 100%; translate: -50%; line-height: 1;
  font-size: 12px; letter-spacing: .1em; color: var(--cluster-soft); white-space: nowrap; }
.ck-per:empty { display: none; } /* a face's own dial carries its unit in its cap */
.ck-cap, .ck-lo, .ck-hi {
  font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--cluster-soft);
}
.ck-cap { margin-bottom: 5px; }
/* the figures come up once the channels are drawn; the pod's disc and camera with them */
:is(.ck-cap, .ck-read, .ck-lo, .ck-hi, .ck-minimap-slot) { opacity: var(--s-read); }
.ck-lo, .ck-hi { position: absolute; transform: translate(-50%, -50%); }
.ck-lo { left: var(--lo-x); top: var(--lo-y); }
.ck-hi { left: var(--hi-x); top: var(--hi-y); }
.ck-rpm .ck-lo { left: calc(100% - var(--lo-x)); }
.ck-rpm .ck-hi { left: calc(100% - var(--hi-x)); }

/* ── the posted limit, in the roundel a European driver reads before anything else. It RIDES THE
   NUMERAL, because the speed you are doing and the speed you may are one reading ── */
/* THE ROUNDEL HANGS UNDER THE NUMERAL rather than sharing a row or a column with it: either
   would re-centre the pair and push the speed off the arc's own centre, which is the thing that
   ties the reading to the sweep around it. Absolute, so the numeral does not move when the sign
   comes and goes. A sixth of the gauge, never under 26 px: on a card's gauge a sixth is an unreadable dot */
.ck-read { position: relative; display: grid; place-items: center; }
.ck-sign {
  --sz: max(calc(var(--g) * .17), 26px);
  position: absolute; left: 50%; top: 100%; translate: -50% calc(var(--g) * .065);
  width: var(--sz); height: var(--sz);
  display: grid; place-items: center; border-radius: 50%;
  background: var(--cluster-ink); color: var(--cluster-ground);
  box-shadow: inset 0 0 0 calc(var(--sz) * .13) var(--cluster-sign);
  font: 700 max(calc(var(--sz) * .44), 12px) / 1 system-ui, sans-serif; font-variant-numeric: tabular-nums;
  transition: transform var(--reveal-open) var(--ease), opacity var(--tap) linear;
}
.ck-sign.gone { transform: scale(.3); opacity: 0;
  transition: transform var(--reveal-shut) var(--ease-exit), opacity var(--tap) linear; }
/* over the limit, the sign PULSES — no bloom. A glow around a regulatory roundel is the one piece
   of decoration that makes it read as ours rather than as the road's */
.ck-sign.over { animation: ck-over calc(var(--handover) / 2) ease-in-out infinite; }
@keyframes ck-over { 50% { transform: scale(1.13); } }

/* ── M: the minimap, and it TAKES THE WHOLE RING — the rpm reading, its caption, the scale ends and
   the sweep itself all go ── */
/* THE SLOT IS THE SIZE, and the disc fills it: minimap3d/ builds its GL JS map at the size its
   frame reports and watches nothing after, so a stylesheet that grew the disc instead left the
   roads drawn in one corner of it and the scale label lying by 2× (the 2D minimap's Scars, scratch/labs/minimap2d/, inherited) */
.ck-minimap-slot {
  position: absolute; left: 6%; bottom: 6%; width: 88%; aspect-ratio: 1; display: none;
}
.cluster.minimap .ck-minimap-slot { display: block; }
.cluster.minimap .ck-rpm :is(.ck-read, .ck-cap, .ck-lo, .ck-hi) { display: none; } /* .ck-read: the figure and its ×100 */
/* the whole sweep goes, not just the lit runs: at 88% the disc covers the channel anyway, and a
   ring drawn under an opaque disc is a ring nobody sees */
.cluster.minimap .ck-arc-rpm { display: none; }

/* ── N: the compass band (arview/cluster/band.js), in the theme's ink, folded to N's one switch as a card's is (it
   draws nothing folded, so it is never hidden here). It runs nearly edge to edge; `ppd` is width/120, so a wider band
   spreads the same 120° of heading further apart rather than showing more of it. Its lettering grows
   with its height (band.js BAND_UNITS) ── */
.ck-compass {
  top: calc(var(--f-band, 0px) + 1.5cqh); left: 50%; transform: translateX(-50%);
  width: calc(100% - clamp(16px, 2.8cqw, 40px)); height: clamp(28px, 8.7cqh, 78px);
}
/* the periphery arrives last and goes first, as a cluster's navigation and state do */
.ck-compass, .ck-rail { opacity: var(--s-rail); }
.ck-hud { width: 100%; height: 100%; display: block; }

/* ── the status rail: the charge, the gearbox. SIZED FOR A DASHBOARD at a dashboard's size: read at arm's
   length on a real cluster, the first set was "barely legible", so every figure here is about a third larger
   than a screen would ask for — and on a card the same rail at the 12 px floor. Everything on it is in its own em ── */
.ck-rail {
  left: 0; right: 0; bottom: var(--f-band, 0px); height: clamp(28px, 8.4cqh, 76px);
  padding: 0 clamp(8px, 2.1cqw, 30px) calc(env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: .77em;
  font-size: clamp(12px, 2.9cqh, 26px);
}
/* the charge: a bar that is the reading, with the number beside it rather than inside it. It
   takes the right-hand END of the rail, where a cluster carries its one number about the car */
.ck-batt { display: flex; align-items: center; gap: .42em; }
.ck-batt-bar {
  width: 2.3em; height: .65em; border-radius: 4px; position: relative;
  box-shadow: inset 0 0 0 2px var(--cluster-track);
}
.ck-batt-bar::after {
  content: ''; position: absolute; inset: 4px; width: auto; border-radius: 2px;
  background: linear-gradient(90deg, var(--cluster-speed) 0 var(--pct, 100%),
                              transparent var(--pct, 100%));
}
.ck-batt.low .ck-batt-bar::after { background: linear-gradient(90deg, var(--cluster-brake) 0 var(--pct, 100%), transparent var(--pct, 100%)); }
.ck-batt-pct { font: 500 max(.77em, 12px)/1 system-ui, sans-serif; }
.ck-gear { margin-left: auto; display: flex; gap: .73em; font: 700 1em/1 system-ui, sans-serif; }
/* A FACE'S FACTS (track.js `cluster` `rail`: a plane's callsign and squawk, a man's grid) take the gear's end of the rail;
   PRND is a face's that has a gear (a car's, cluster.js), the pack a face's with a charge */
.ck-facts { margin-left: auto; font: 500 max(.85em, 12px)/1 system-ui, sans-serif;
  letter-spacing: .02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ck-facts:empty { display: none; }
/* THE GLASS: a face that paints its own instrument (a PFD, an ND, an orbit display) takes the
   gauge's whole box, and the dial it stands in for goes. Wider than the arc's box, as a display
   is: the arc needed a square, a PFD needs its tapes either side of the ball */
.ck-glass { position: absolute; inset: -12%; width: 124%; height: 124%; display: none;
  opacity: var(--s-read); }
.ck-gauge.painted .ck-glass { display: block; } /* `painted`, not `glass`: that class is theme/glass's plate, and wore every face in a frosted square for a day (2026-10-17) */
.ck-gauge.painted :is(.ck-arc, .ck-cap, .ck-read, .ck-lo, .ck-hi) { display: none; }
.cluster.minimap .ck-rpm .ck-glass { display: none; } /* M: the minimap takes the pod, as over the tach */
.ck-g { color: var(--cluster-track); transition: color var(--fade) linear; }
.ck-g.on { color: var(--cluster-ink); }
/* a dash of colour on the one that is selected: the gear you are IN is worth a hue */
.ck-g.on:nth-child(4) { color: var(--cluster-speed); text-shadow: 0 0 14px var(--cluster-speed); }
.ck-g.on:nth-child(2) { color: var(--cluster-brake); text-shadow: 0 0 14px var(--cluster-brake); }
/* ── the brake badge: an OUTLINED pill on the ground, so it adds light without hiding road. It
   pops on the springloaded pair M and N share (.ar-minimap-box). THE WRAPPER POSITIONS AND THE WORD IS
   THE PILL: a wrapper doing both is a containing block for its own pseudo-element, and its
   `translate` fights the `transform` the pop animates ── */
.ck-brake { left: 0; right: 0; top: 44%; display: grid; place-items: center; }
.ck-brake-word {
  padding: .53em 1.35em; border-radius: .6em;
  background: color-mix(in srgb, var(--cluster-ground) 82%, transparent);
  color: var(--cluster-brake);
  font: 700 clamp(12px, 3.8cqh, 34px)/1 system-ui, sans-serif; letter-spacing: .26em; text-indent: .26em;
  box-shadow: inset 0 0 0 2.5px var(--cluster-brake),
    0 0 40px color-mix(in srgb, var(--cluster-brake) 40%, transparent);
  transition: transform var(--reveal-open) var(--ease), opacity var(--tap) linear;
}
/* the right pod's screen, a roadside camera clicked in a card's picture (card/pip/): over the pod, at the
   picture's own shape, in the cluster's housing and the speed sweep's glow */
.ar-cam {
  height: var(--g); right: calc(var(--g) * var(--edge)); top: 50%; transform: translateY(-50%);
  aspect-ratio: 16 / 9; overflow: hidden; border-radius: calc(var(--g) * .06);
  background: var(--surface-void); pointer-events: auto;
  box-shadow: 0 0 0 3px var(--cluster-housing),
              0 0 calc(var(--g) * .16) color-mix(in srgb, var(--cluster-speed) 42%, transparent);
}
.ar-cam .ar-video { object-fit: cover; }
.ar-cam-close {
  position: absolute; top: 3px; right: 3px; z-index: 2; pointer-events: auto;
  display: grid; place-items: center; width: 24px; height: 24px; padding: 0;
  border-radius: var(--radius-s); color: var(--text-ink); /* its plate: glass (pip.js) */
  cursor: pointer;
}
.ar-cam-close .icon { width: 12px; height: 12px; }

/* ── COCKPIT MODE'S OWN (arview/cluster/cockpit.js): the cluster over the main map, in its housing ──
   THE WAKE: ONE CLOCK, and every stage of the cluster setting into its dash is a slice of it.
   `--wake-t` runs 0 → 1 over --wake and back over 60% of it; the readings (`--speed`, `--rpm`)
   are never eased — they are multiplied by a stage, so a needle is never late (round 11: a
   delayed figure reads as lag). Entry order, which the exit reverses for free: the glass darkens
   the whole way, the housing sets in over the first half, the channels draw, the runs fill to the
   reading, the figures come up, and the compass and the rail arrive last — as a production
   cluster wakes. VISIBILITY, not display: an element going display:none → block has no `from`. */
@property --wake-t { syntax: '<number>'; inherits: true; initial-value: 0; }
#cockpit {
  visibility: hidden;
  --wake-t: 0;
  transition: --wake-t calc(var(--wake) * .6) ease-in-out, visibility 0s calc(var(--wake) * .6);
  --s-glass: var(--wake-t);
  --s-house: clamp(0, var(--wake-t) / .5, 1);
  --s-draw: clamp(0, (var(--wake-t) - .15) / .45, 1);
  --s-fill: clamp(0, (var(--wake-t) - .4) / .5, 1);
  --s-read: clamp(0, (var(--wake-t) - .45) / .35, 1);
  --s-rail: clamp(0, (var(--wake-t) - .6) / .4, 1);
  --f-max: 10%;                    /* the housing above and below the window, each */
  --f-band: calc(var(--f-max) * var(--s-house)); /* what it is at this moment of the wake */
}
body.chase.cockpit #cockpit {
  visibility: visible; --wake-t: 1;
  transition: --wake-t var(--wake) ease-in-out, visibility 0s;
}
/* the GL slot steps back out of the cockpit's --ui zoom (the card's frame already has, .ar-frame) */
#cockpit .ck-minimap-slot { zoom: calc(1 / var(--ui)); }

/* THE VIGNETTE IS THE BEZEL: everything outside the working rectangle is glass falling away, so
   it comes in deep on all four sides and the corners go to the ground colour outright. Four
   layers — a tight ellipse for the falloff, a square-edged wash on each axis (one ellipse cannot
   square a corner), and a foot that carries the status rail out of the road. */
.ck-vignette {
  /* SCREEN COORDINATES, ALWAYS, and never clipped to the housing's window: the badge patch below
     is measured at 18% of the SCREEN, where the map's own canvas parks the sign, and the housing
     is painted over this sheet anyway — clipping it only let raw sky through under a shallow
     frame, which is the one thing a bezel exists to stop */
  inset: 0; opacity: calc(.92 * var(--s-glass)); /* how dark the corners go: the map reads as seen THROUGH a cluster */
  background:
    radial-gradient(ellipse 74% 66% at 50% 54%, /* the shade starts 38 % out; 100 % turns it off */
      transparent 38%, var(--cluster-ground) 104%),
    linear-gradient(var(--cluster-ground), transparent 26% 68%, var(--cluster-ground)),
    linear-gradient(90deg, var(--cluster-ground), transparent 21% 79%, var(--cluster-ground)),
    radial-gradient(ellipse 120% 40% at 50% 100%,
      color-mix(in srgb, var(--cluster-ground) 85%, transparent), transparent 70%);
  /* …and the ONE INSTRUMENT THIS SHEET IS NOT ALLOWED TO DIM: the turn sign is drawn INSIDE the
     map's canvas (overlays/generic/waypoints/turnsign.js/), so it can never be lifted over a DOM layer —
     the shade has to move instead. THINNED, NOT CUT: the patch is the parked badge's own box
     (18% of the height, centred on turnsign's `park`) and it keeps HALF its shade there, so the
     sign reads without a clearing around it. The compass band above it is outside the ellipse
     and keeps the bezel whole, and the falloff runs the patch's full radius — no edge to catch. */
  mask: radial-gradient(ellipse 19% 9% at 50% 23%, rgba(0, 0, 0, .5) 0 26%, #000 100%);
}

/* ── THE HOUSING ──────────────────────────────────────────────────────────────
   A cluster is a PANEL SET IN A DASH, not a screen filling a wall: a 12.3-inch cluster is about
   2.2:1, and the picture in it is shorter than the display it stands on. So the frame here is
   not ornament around the edge — it is the WINDOW, and the rail, the compass and the shading are
   measured from its edges. ONE BAND, top and bottom, because the owner measured the first cut
   and the two were not the same: symmetry is the property, not two numbers that happen to agree.
   Chosen from ten (cowl, slab, curve, hex, pods, brow, wings, float, shelf); the rest are gone.
   TWO LIMITS, both from things drawn outside this stylesheet: the gold instruction parks at 22%
   of the SCREEN's height inside the map's own canvas (turnsign.js), and the car itself sits at
   78% (chase.headroom) — a band past ~18% takes one of them. The card wears no housing: the
   instruments are shared, the frame is the cockpit's (the owner, 2026-10-25). */
.ck-frame {
  /* the housing IS the shadow: a spread this large paints every pixel outside the window and not
     one inside it, so one element is the whole bezel */
  inset: var(--f-band) 0;
  box-shadow: 0 0 0 100vmax var(--cluster-housing);
}

/* ── AN AREA'S RADIAL (annotations/aoi/radial.js): its meaning, a ring of clear wedges over ONE glass disc round the area's
   middle, the middle open so the area shows through; the picked wedge wears THE HIGHLIGHT, the full name of the one under the
   pointer stands over the disc. Mounted shut and opened a frame later, so the fold-out runs every time (the AoI prototype,
   2026-10-24). The script cuts each wedge's clip-path; 168 px across (RADIAL.R 84), open 22 px in the middle ── */
.aoi-radial { position: absolute; left: 0; top: 0; z-index: 4; pointer-events: none; }
/* sized by the dial's radii (theme/radial: --R, --r; the area picker's 84 and 22, the ping wheel's 128 and 56) */
.aoi-radial-disc, .aoi-radial-grooves, .aoi-wedge { position: absolute; left: calc(-1 * var(--R)); top: calc(-1 * var(--R)); width: calc(2 * var(--R)); height: calc(2 * var(--R)); }
.aoi-radial-disc { border-radius: 50%; mask: radial-gradient(circle, transparent calc(var(--r) - 3px), #000 calc(var(--r) - 2px)); opacity: 0; transform: scale(.72);
  transition: opacity var(--fade) var(--ease), transform var(--slide) var(--ease); }
.aoi-radial-grooves { pointer-events: none; stroke: var(--line-border); stroke-width: 2; opacity: 0; transition: opacity var(--fade) var(--ease); }
.aoi-radial.open :is(.aoi-radial-disc, .aoi-radial-grooves) { opacity: 1; transform: none; }
.aoi-wedge { border: 0; padding: 0; margin: 0; background: none; cursor: pointer; color: var(--text-ink); pointer-events: auto; }
.aoi-wedge.danger { color: var(--text-danger); }
.aoi-wedge-hl { position: absolute; left: 0; top: 0; pointer-events: none; }
.aoi-wedge-hl path { fill: transparent; stroke: transparent; stroke-width: 2; transition: fill var(--tap) var(--ease), stroke var(--tap) var(--ease); }
.aoi-wedge:focus-visible { outline: none; }
.aoi-wedge:focus-visible .aoi-wedge-hl path { stroke: var(--text-ink); } /* the one focus ring: 2 px of ink, on the wedge's own edge */
.aoi-wedge[aria-checked="true"] .aoi-wedge-hl path { fill: var(--highlight-wash); stroke: var(--line-highlight); }
.aoi-wedge-c { position: absolute; width: 72px; display: flex; flex-direction: column; align-items: center; gap: 2px; pointer-events: none;
  opacity: 0; transform: translate(-50%, -50%) scale(.7); transition: opacity var(--fade) var(--ease), transform var(--slide) var(--ease); }
.aoi-radial.open .aoi-wedge-c { opacity: 1; transform: translate(-50%, -50%); }
.aoi-wedge-c .icon { margin: 0; }
.aoi-wedge-c .glyph svg { display: block; height: 28px; width: auto; } /* an APP-6E frame (the ping wheel's), in its .ink or not */
body[data-light="night"] .aoi-wedge-c .ink { filter: invert(1); } /* a control measure is milsymbol's black line art */
.aoi-wedge-c b { font: 600 12px/16px var(--font); text-align: center; }
.aoi-radial-cap { position: absolute; left: 0; top: calc(-1 * var(--R) - 8px); transform: translate(-50%, -100%); white-space: nowrap; padding: 4px 12px;
  border-radius: var(--radius-full); font: 600 12px/16px var(--font); color: var(--text-ink); pointer-events: none; opacity: 0;
  transition: opacity var(--tap) var(--ease); }
.aoi-radial-cap.on { opacity: 1; }
.aoi-radial.below .aoi-radial-cap { top: calc(var(--R) + 8px); transform: translate(-50%, 0); }
/* THE AUDIENCE (overlays/generic/aoi/): Police | NATO over the Areas' list — the presets a NEUTRAL area's radial offers. Inside the
   panel, so a hairline capsule and never a plate; the pressed one wears SELECTED's capsule (the rule by the tool row) */
.aoi-audience { display: flex; width: max-content; margin: 4px 0 8px 16px; border: 1px solid var(--line-border); border-radius: var(--radius-full); }
.aoi-audience > button { width: auto; height: 36px; padding: 0 16px; font: 600 12px/16px var(--font); border-radius: var(--radius-full); }
/* AN AREA'S ALERT (overlays/generic/aoi/alerts.js): the engine's newest (rule 3) in ONE pill at the foot of the map, its Show flying
   there; an urgent one (a minefield, a hot zone entered) rimmed red with its word */
.aoi-alert { position: absolute; left: 50%; bottom: 72px; transform: translateX(-50%); z-index: 5; display: flex; align-items: center; gap: 12px;
  max-width: calc(100% - 32px); padding: 4px 4px 4px 16px; border-radius: var(--radius-full); font: 500 14px/20px var(--font); color: var(--text-ink); }
.aoi-alert > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.aoi-alert.urgent { border-color: var(--text-danger); padding-left: 6px; }
.aoi-alert > .urg { font: 700 12px/16px var(--font); color: var(--text-on); background: var(--text-danger); border-radius: var(--radius-full); padding: 2px 8px; }

/* HOVER IS A POINTER'S STATE (the guard on the shared wash, above): a finger that tapped leaves :hover stuck on the
   last control until the next tap lands elsewhere, so every :hover in this file lives here, under the one query, each
   rule as it stood in its own section (rules.test guards it). Mirror mode's wake is the one :hover outside: it rides
   with :active and :focus-within on purpose. */
@media (hover: hover) {
  #ar-seam:hover::after { opacity: .55; }
  .m3-rig:hover .m3-ctl { opacity: 1; }
  .search-opt:hover { background: var(--highlight-hover); }
  .m3-ctl .btn:hover .face { fill: var(--line-highlight); fill-opacity: .1; } /* the one wash on a no-fill bar: the highlight's hover, as the shell's buttons */
  /* HOVER lights the WHOLE node, contents and all — not the title strip. :has() hands the glow
     DOWN: the innermost node under the cursor is the one that lights, and its parents stand back. */
  .cg-node:hover:not(:has(.cg-node:hover)) > .cg-pill {
    background: color-mix(in srgb, var(--cg-tone) 26%, transparent);
    border-color: color-mix(in srgb, var(--cg-tone) 80%, transparent);
  }
  .cg-node:not([data-open]):hover:not(:has(.cg-node:hover)) > .cg-pill {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--cg-tone) 16%, transparent);
  }
  .cg-node[data-open]:hover:not(:has(.cg-node:hover)) {
    border-color: color-mix(in srgb, var(--cg-tone) 70%, transparent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--cg-tone) 16%, transparent);
  }
  .cg-node.cg-fork:hover > .cg-segs {
    border-color: color-mix(in srgb, var(--line-strong) 85%, transparent);
    box-shadow: 0 0 0 3px var(--overlay-veil);
  }
  .glass:hover > .shine { opacity: 1; }
  .mapboxgl-ctrl-group button:not(:disabled):hover {
    background-color: var(--highlight-hover); }
  .callout-x:hover { opacity: 1; }
  .key-hint[data-run]:hover { text-decoration: underline; text-underline-offset: 3px; } /* no plate to wash: its word says it is a verb */
  .nato-sw:hover { opacity: 1; }
  .nato-chev:hover { opacity: 1; } /* a reveal, not a shade: the shared wash is the shade */
  .nato-amp select:hover { border-color: var(--line-strong); }
  .rewind-track:hover::-webkit-slider-thumb { transform: scale(1.15); }
  .aoi-wedge:hover .aoi-wedge-hl path { fill: var(--highlight-hover); }
  .aoi-wedge[aria-checked="true"]:hover .aoi-wedge-hl path { fill: var(--highlight-wash); }
}

/* ── THE LAST WORD: the three blocks below are re-declarations of selectors written above, at
   the same specificity, so they are LAST — the cascade is order, and each of them lost to a later
   rule once (Scars). rules.test.js guards their position: nothing after the last of them but the Scars. */
/* ONE block, every clock: the two infinite pulses stop outright, because a loop at 1ms strobes */
@media (prefers-reduced-motion: reduce) {
  body { --slide: 1ms; --fade: 1ms; --tap: 1ms; --handover: 1ms; --reveal-open: 1ms; --reveal-shut: 1ms; --wake: 1ms; }
  #rewind[data-scrubbed] .rewind-live .icon, .ck-sign.over, .loader::before, .loader::after { animation: none; }
}
/* touch: every target grows to a thumb */
@media (pointer: coarse) {
  .mapboxgl-ctrl-group button { width: 44px; height: 44px; display: grid; place-items: center; }
  /* the drive pill: a thumb's 44 px on a tablet too, where the chip is 22 px tall (2026-10-26); the phone's is a 44 px plate */
  body:not(.phone) .key-hint[data-run]::before { content: ''; position: absolute; inset: -11px -4px; }
  .mapboxgl-ctrl-group .icon { margin: 0; }
  /* the where-am-I button is GL JS-shaped but ours, and the scale bar is a unit switch: both were 24px chips, the
     two smallest controls on the map. A thumb's 44px square with them: the Overlays' eyes; a row's ✕, as tall as its
     row and no taller (taller, it would take the neighbour's tap, and it is the one destructive verb in the panel);
     the promotion bar's frames and chevron (below) */
  .mapboxgl-ctrl-top-right button, .mapboxgl-ctrl-bottom-right button, #overlays-list .show, #overlays .overlays-row button,
  .nato-sw, .nato-chev { min-width: 44px; min-height: 44px; }
  .mapboxgl-ctrl-scale { min-height: 30px; line-height: 28px; padding: 0 8px; }
  /* A CARD'S OWN VERBS are 44px squares in their pill at every pointer; under a thumb the pill (eight
     at most, 352px) left "Cruiser …" of the name on a 380px card, so the NAME takes the first line and
     the pill the second — and the chrome under the picture is 16 (name) + 6 + 44 (the pill) + 6 (the
     head's margin); the command bar's 58 went 2026-10-07. */
  .ar-head { flex-wrap: wrap; }
  .ar-label { flex: 1 0 100%; line-height: 16px; }
  body { --chrome-h: 72px; }
  /* the time cursor: a 16px rail is not a drag target */
  #rewind .rewind-row { height: 34px; }
  #rewind .rewind-track { height: 32px; }
  #rewind .rewind-track::-webkit-slider-thumb { width: 22px; height: 22px; margin-top: -6px; }
  #rewind :is(.rewind-play, .rewind-shut) { width: 40px; height: 40px; position: relative; }
  #rewind :is(.rewind-play, .rewind-shut)::before { content: ''; position: absolute; inset: -2px; } /* 44 to a thumb, the bar unchanged */
  /* live is 44 tall inside the 44px bar */
  #rewind .rewind-live { min-height: 44px; }
  /* the Overlays's rows and their eyes: 205 tab stops, all of them 24px high, stacked touching —
     the one place where 2.5.8's spacing rule cannot save an under-size target. The thumb's 44px, as
     every other target here: they stood at 36, a size under the rest of the shell (the owner, 2026-10-21) */
  #overlays-list .overlays-row, #overlays-list details > summary { min-height: 44px; }
  body { --row-h: 44px; } /* windowed rows are PLACED at the pitch: a min-height alone overlapped them */
  #overlays #overlays-list a { min-height: 44px; }
  #performance button { width: 36px; height: 36px; margin: -6px -6px -6px 0; position: relative; }
  #performance button::before { content: ''; position: absolute; inset: -4px; } /* 44 to a thumb, the row unchanged */
  #track-filter { min-height: 44px; } /* 34 under a thumb: the one field in the sheet */
  /* the ☰ beside the panel's title, the same square it is when the panel is shut */
  #overlays:not(.shut) #menu-btn { width: 44px; height: 44px; }
  /* the ✕ on a callout and the teardrop on an AR view: a HIT AREA around a glyph
     that has to stay small, so the card's own layout never grows for it */
  :is(.callout-x, .ar-pin)::before { content: ''; position: absolute; }
  .callout-x::before { inset: -10px; }
  .ar-pin::before { inset: -12px -14px; }
  /* the promotion bar's frames and chevron: 44px squares in a bar widened to hold them */
  body { --nato-w: 400px; }
  /* Settings: a slider thumb you can put a thumb on (#overlays-list, because the dials' own rules come
     later in this file and would win a tie) */
  #overlays-list :is(.dial-range, .dial-pair) { height: 22px; }
  #overlays-list :is(.dial-pair-rail, .dial-pair-fill) { top: 9px; }
  #overlays-list .dial-range::-webkit-slider-thumb { width: 22px; height: 22px; margin-top: -9px; }
  /* the seam: 24px to grab, standing 7px over each picture's edge; the hairline stays 2 */
  #ar-seam { width: 24px; margin-right: -7px; }
  #ar-seam::after { inset: 0 11px; }
}
/* a phone on its side: the control column stood taller than the map. After the coarse block,
   which the same selectors would otherwise win on order */
@media (max-height: 500px) {
  .zoom-readout { display: none; }
  .mapboxgl-ctrl-group button, .mapboxgl-ctrl-top-right button, .mapboxgl-ctrl-bottom-right button {
    width: 36px; height: 36px; min-width: 36px; min-height: 36px; display: grid; place-items: center; }
  .mapboxgl-ctrl-group .icon { margin: 0; }
}

/* ── Scars ──
   · Cockpit mode first opened the map's frame out over --slide: filmed at a tenth of the speed the map was BLANK the whole
     run — every frame the box moved, our per-container ResizeObserver (maps.js; GL JS 3.32 tracks window resize only)
     cleared the canvas and the redraw landed a frame later → the box steps once, under something that moves by
     transform. The column's own `right` transition has the same flaw, still open.
   · The column FLOATED on the map as tall as its cards for an afternoon (2026-10-22: one card left 888 of 1,064 px of
     column bare) → the owner: "go back to the old style" — the map beside the cards, the column the window's height.
     Don't re-propose a column over the map; the bare desk under one card is the price of the map never being covered.
   · A full-window map CLIPPED to the desk (clip-path, the frame as ::after, the camera's padding following the cards)
     cost frame rate, repaints and Δt the owner saw at once (the canvas drew under the cards, the padding tween read
     computed styles on every body mutation). Reverted: "not worth it".
   · Everything under the picture is ONE token: the sheet wrote .ar-head's 24px + margin as a bare 30, and a 44px thumb
     head pushed Close 14px off a 390×844 phone → --headh (arview.js usableFw() reads it off <body>). It forgot the
     command bar (two touch cards in a 748px column measured 864px) → --chrome-h, stepped whole in the coarse block.
   · The time cursor was absolute to the SCREEN's bottom, on a phone INSIDE the bottom sheet, its 16px thumb 26px from
     Close: anything absolute to the viewport's bottom collides with the sheet → #rewind in the tool row. The coarse
     block still grows its rail and thumb: 16px is not a thumb target wherever it lives.
   · 4K at 100 %: `zoom` on <body> alone fails twice — dvh scales with it (a 3780px body on a 2160 screen) and GL JS sizes
     every canvas off getBoundingClientRect, which zoom inflates (a 6659px canvas) → the pictures zoom back to 1 and the
     furniture zooms; a JS writer of client px (tooltip, promotion bar, details card) divides by the same --ui, read off
     <body> because `zoom` does not inherit.
   · The tool row wrapped by a 760px MEDIA query: a card open beside a 1000px map never wrapped, and the shut Rewind was
     forced onto its own line → the row is a query container.
   · The shared hover was `filter: brightness(k)`: a filter multiplies the pixels an element paints, and half the controls
     paint a glyph on nothing (☰, the eyes, every ✕, the transport), so their hover was invisible → an inset wash of
     currentColor: one number, no night twin.
   · Coarse-pointer rules sat at the same specificity as rules further DOWN this file and lost on order (a 24px ✕ where 44
     was written) → a coarse override of something declared later carries the panel's id, or the base rule reads a
     variable the block sets.
   · H took the column off with display:none while the map's edge eased 280 ms: the ground showed where the column stood
     and the canvas resized every frame (2026-10-04) → it slides away on the clock and GL hold; `--away` is the one
     spelling of "off the screen".
   · The tool row eased its left edge for the Overlays and snapped its right for the cards → both ease on --slide.
   · The search field had `outline: none` under the pill's own :focus-visible ring: a text input matches :focus-visible
     on a click, and the later global input rule outranked the none → two rings. Border ink on focus, no ring
     (2026-10-08).
   · Sixteen :hover rules sat outside the one (hover: hover) guard: on a phone each stayed lit after a tap until the next
     tap landed elsewhere (2026-10-21) → one block under the one query, before the last-word blocks; rules.test fails on
     a :hover outside it. Mirror mode's wake keeps its :hover by name.
   · The boot loader's caption went (the owner, 2026-10-25: "Just the loader ring icon on top is enough"). It had sat in
     a phone's lower half: a place-items: center grid stretches its two implicit rows over the box (align-content packs).
   · Every slider rule's ::-moz-range-* twin went (2026-10-23, the owner's tick): the app meets Chrome and WebKit only, and
     a Firefox visitor's slider falls back to its own look and still works.
   · The two pictures stood 10 px in with the desk's 2 px frame and 22 px radii: on a 390 px phone 44 px of desk across —
     "wasted space on top, bottom, left and right" (the owner, 2026-10-15) → edge to edge, the 8 px gap the only seam;
     --homeb lifts the map's foot off the home indicator only when the map's foot is the screen's.
   · The Rewind's strip as `#rewind .rewind-bar` lost to the later row-container block (the same selector): one 44px line,
     the lane 168px. Chrome answers a size query with no container ancestor as if it matched → the strip's rules carry
     body.phone.
   · The desk's capture of the map for the phone prototypes carried the desk's 2px frame inside the pane's → every pane is
     the picture's edge, and the picture is whatever the pane's shape is (no aspect-ratio).
   · The search pill floated centred on the map, kept clear of the row and the control stack by hand-written pixel sums
     (234/304/492/562) a fourth button or a resized column broke → a flex item of #toolbar: the row bounds it.
   · #performance's chevron pointed where the box came from; the owner: it points where the box will go. The owner also
     put the hero unit on its number's line and each cost gauge between its name and its reading.
   · #performance shut stood 8px taller below its readout: the folded drawer's padding-top held its 0fr row open → the
     padding comes in with .open.
   · The owner (2026-10-13): the stick on the map's lower-left with the readout under it, the scale bar bottom-right under
     the ⊕, the handbrake cut (the stick pulled back brakes), the card's bar to the picture's lower-right, and a ✕ on the
     picture that is H — the drive goes on on the map. The same round's two kept ideas: the sheet snaps half/full and
     remembers, the chrome goes quiet through a drive.
   · The drive pad was five round buttons for a car only → the racing game's layout (a steering plate whose pointer picks
     the half, pedals shaped like pedals) and a stick for the man; with it up on a phone the control column goes, or the
     throttle sat on its locate button.
   · The SELECTED capsule's `position: relative` carried the :is() list's top specificity: the fixed ⊕ went relative when
     its fan opened, dropped to (-20, -56) and could not fold (2026-10-18) → the capsule positions a host at :where(&).
   · The desk's sliding pick (theme/glass slidePick) assumes a ROW: in the phone's fan (a column) an armed tool drew one
     dark band behind the whole column (2026-10-24) → the phone puts the pick away and the armed circle joins SELECTED's
     list inside :where(); the bar's `background: none` still left its glass blur and shadow over the 48×288 column → no
     plate.
   · SELECTED was a lifted white capsule while the AoI board wore the highlight; the owner (2026-10-24): "standardize to
     the whole app, so that it's uniform everywhere" → selected, hovered, pressed and a list's current row read
     line.highlight's washes, the map's selection glow its lit cyan; a new control never paints a pick of its own. Its
     capsule stood 3px inside the rim; the owner (2026-10-25): "less padding than what we have in the prototype" →
     --highlight-inset 5px, the board's 30px ring 7px inside the outer edge.
   · `html { overflow: clip }` is the VIEWPORT's `hidden` on the root, which code still scrolls, and body's own clip
     missed the absolute boxes hung off the root (the parked pane, 1083 px of an 844 px phone): a scrollIntoView lifted
     the whole app 239 px (2026-10-26) → body is `position: relative`, their containing block, and clips them.
   · The phone's vehicle column added env(safe-area-inset-bottom) to --sheet + --homeb, which carry it already: on a notched
     iPhone it stood ~34 px over the ⊕ fan it lines up with (2026-10-27, the live run, read off the CSS) → #toolbar's sum.
   · The camera readout ran nowrap under the Performance pill on a narrow map (iPad portrait with a card: 456 px of map, the
     line clipped to "…40° / 62° / 1.3 km…"; 2026-10-27, the live run) → the bottom-right corner keeps 270 px clear and the
     line wraps there; a wide map keeps it on one line (laptop measured 12 px tall). */
