/* App shell — THE SECTIONAL, day & night (docs/design-direction.md).
   The chrome is chart marginalia: paper panels, ink rules, mono data.

   ── The custom-properties layer (adsb-dgb.7) ────────────────────────
   Every colour the chrome uses is a variable below, defined twice: the
   day edition on :root, the night edition under prefers-color-scheme.
   This is the CSS half of the two-edition mechanism; the MapLibre half
   (basemap + aircraft ink) lives in adsb.map.theme / adsb.map.basemap /
   adsb.map.style, keyed off the SAME media query. The hexes mirror the
   §2 palette table — change them together, in both places.

   ── Typography & scale — SETTLED (§5, adsb-dgb.10) ──────────────────
   The crowned mix, promoted verbatim from the #/preview fitting room:

     typography  THE ANNOTATION — one hand for everything written on
                 the chart, the plotter's: Space Mono (OFL, self-hosted
                 below; licensing in fonts/LICENSE.md). Hierarchy is
                 weight and case, never a change of voice — bold stamps
                 for titles (the header title carries the 2px magenta
                 pen underline), caps-tracked bold for block headings.
                 Mono numbers are inherently tabular.
     captions    PRINTED (§5, bead adsb-fon): the smallest labels —
                 fact labels, stats labels, count units, shelf labels —
                 print in Space Grotesk Medium (OFL; Space Mono's own
                 proportional derivative — one hand at two widths),
                 upright, uppercase, tracked 0.12em, at --t-2. Grotesk
                 stays in exactly this lane: never titles, body, or
                 data. The system's original italic strained at panel
                 sizes (Overseer finding) and is retired from small
                 labels.
     scale       major third, 1.25 @ 13px — tokens --t-2 … --t2.
                 Deliberately shallow; the size is spent on the DATA.
     spacing     compact, 4px base — tokens --s1 … --s5, header 36px.
                 The chrome defers to the map; the sky is the whitespace.
     palette     the wine pen — §2's hue relationships with the PEN
                 pressed deeper (--magenta / --aero below).

   CSP note: production needs `font-src 'self'` (adsb.http.security,
   tracked as adsb-kh4.7) before the faces load behind the strict policy;
   the shadow dev server on :8080 serves them without ceremony. */

@font-face {
  font-family: "Space Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/space-mono-400.woff2") format("woff2");
}

@font-face {
  font-family: "Space Mono";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/space-mono-400i.woff2") format("woff2");
}

@font-face {
  font-family: "Space Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/space-mono-700.woff2") format("woff2");
}

/* The caption voice (§5 captions): Space Mono's proportional sibling. */
@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/space-grotesk-500.woff2") format("woff2");
}

:root {
  /* §5 type tokens — major third, 1.25 @ 13px. */
  --t-2: 8.5px;
  --t-1: 10.5px;
  --t0:  13px;
  --t1:  16px;
  --t2:  20px;

  /* §5 spacing tokens — compact, 4px base. */
  --s1: 2px;
  --s2: 4px;
  --s3: 8px;
  --s4: 12px;
  --s5: 16px;
  --header-h: 36px;

  /* The Stack's footprint on the map edge (right on desktop, bottom on
     phone) — the header, ribbon, and attribution all clear it by this. */
  --stack-w: 76px;

  --paper:        #F5EFDF; /* map paper (also the pre-tile backdrop) */
  --paper-chrome: #FBF6E8; /* panels, header — a lighter leaf */
  --paper-veil:   rgba(251, 246, 232, 0.92); /* panel over the chart */
  --paper-halo:   rgba(251, 246, 232, 0.7);  /* hairline halos on paper */
  --ink:          #2C2A24;
  --faded-ink:    #6E6A58; /* tuned from §2's #8B8471 for caption contrast */
  --contour:      #D9C99F;
  --magenta:      #A83A63; /* the wine pen (§5) */
  --aero:         #36547E;
  --emergency:    #CE2029;
  --on-emergency: #FBF3E4; /* text on the emergency red */
  --alt-ground:   #8A8374; /* aircraft states — mirror adsb.map.style */
  --alt-unknown:  #9A937F;
  --ok:           #55722F; /* connection: live (contrast-tuned) */
  --warn:         #8F6318; /* connection: reconnecting (contrast-tuned) */
  --rule:         rgba(44, 42, 36, 0.3);   /* hairline ink rules */
  --rule-strong:  rgba(44, 42, 36, 0.5);
  --rule-faint:   rgba(44, 42, 36, 0.08);

  /* One hand (§5): every family variable is the plotter's mono. The
     three names survive for the #/preview fitting room, which re-points
     them per candidate. */
  --mono:  "Space Mono", ui-monospace, Menlo, Consolas, monospace;
  --serif: var(--mono);
  --sans:  var(--mono);
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:        #151B26;
    --paper-chrome: #1B2330;
    --paper-veil:   rgba(27, 35, 48, 0.92);
    --paper-halo:   rgba(27, 35, 48, 0.7);
    --ink:          #E9E2CE;
    --faded-ink:    #8D96A8;
    --contour:      #2E3A49;
    --magenta:      #E77E9B; /* the wine pen, night print (§5) */
    --aero:         #8BA9D6;
    --emergency:    #FF5A4D;
    --on-emergency: #1C1210;
    --alt-ground:   #6E7686;
    --alt-unknown:  #7C8494;
    --ok:           #8FBF6F;
    --warn:         #D9A648;
    --rule:         rgba(233, 226, 206, 0.3);
    --rule-strong:  rgba(233, 226, 206, 0.5);
    --rule-faint:   rgba(233, 226, 206, 0.08);
  }
}

/* ── Motion (§6) ──────────────────────────────────────────────────────
   The chrome breathes, quietly: panels settle in under 200ms ease-out,
   the live dots breathe (never blink), the selection ring draws itself
   in. The EMERGENCY treatment spends zero AMBIENT motion (§7) — nothing
   under .adsb-alerts or any -emergency class animates, ever. The one
   sanctioned exception is §7's own commission: the red-pen ellipse
   (.adsb-mayday) DRAWS ITSELF IN once — iteration-count 1, set inline
   by adsb.map.emergency so the suite can prove it — and then holds.
   Ink never blinks. */

@keyframes adsb-settle {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
}

@keyframes adsb-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}

/* The compass-pencil ring drawing itself in (§4): the dashes grow from
   nothing (both dasharrays sum to 7 of the circle's pathLength 70, so
   ten dashes press into place) while the offset sweeps them a step
   around — a pencil dragged once along a compass. Ink stays put after. */
@keyframes adsb-ring-draw {
  from {
    stroke-dasharray: 0 7;
    stroke-dashoffset: 10;
    opacity: 0.2;
  }
  to {
    stroke-dasharray: 3.2 3.8;
    stroke-dashoffset: 0;
    opacity: 1;
  }
}

/* The red pen circling an emergency (§7): the dash sweeps from the full
   pathLength 100 to the ellipse's settled dashoffset 0 — one pass of the
   grease pencil, pressed in and left. Duration, delay, and the PROVABLE
   iteration-count 1 are inline longhands set by adsb.map.emergency. */
@keyframes adsb-mayday-draw {
  from {
    stroke-dashoffset: 100;
  }
}

@media (prefers-reduced-motion: reduce) {
  .adsb-panel,
  .adsb-selection-ring circle,
  .adsb-conn-live .adsb-conn-dot,
  .adsb-feeder-ok .adsb-feeder-dot {
    animation: none;
  }

  /* !important because the draw-in rides inline longhands (see above);
     reduced motion must still win, and the ellipse is simply already
     drawn — the held state IS the design. */
  .adsb-mayday ellipse {
    animation-name: none !important;
  }

  .adsb-stack-tick {
    transition: none;
  }
}

html,
body,
#app {
  margin: 0;
  height: 100%;
}

body {
  font-family: var(--mono);
  font-size: var(--t0);
  background: var(--paper); /* the paper shows before the first tile */
  color: var(--ink);
}

/* The shell fills the viewport; the map is its full-bleed background. */
.adsb-shell {
  position: relative;
  height: 100vh;
  overflow: hidden;
}

.adsb-map {
  position: absolute;
  inset: 0;
  background: var(--paper); /* the chart's own paper while tiles load */
}

/* ── The header — the chart's title block ─────────────────────────────
   36px (--header-h) of instrument: the title stamped in bold under the
   magenta pen underline, mono vitals, the two health chips pinned right
   so the map's health is always in the same place. One ink rule below. */
.adsb-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--s4);
  height: var(--header-h);
  padding: 0 var(--s4);
  background: var(--paper-chrome);
  border-bottom: 1px solid var(--ink);
  color: var(--ink);
  font-size: var(--t0);
  box-sizing: border-box;
}

.adsb-title {
  font-size: var(--t1);
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  border-bottom: 2px solid var(--magenta); /* the plotter's pen (§5) */
  padding-bottom: 1px;
}

/* Live vital signs. The counts sit next to the title; the two health
   chips are pushed to the far right. */
.adsb-counts {
  color: var(--faded-ink);
  font-size: var(--t-1);
}

.adsb-count-total,
.adsb-count-positioned {
  font-weight: 700;
  color: var(--ink);
}

/* .adsb-count-unit takes the printed caption voice — the grouped rule
   at the end of the chrome section owns it. */

.adsb-count-sep {
  opacity: 0.6;
}

.adsb-clock {
  color: var(--faded-ink);
  font-size: var(--t-1);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

/* Health chips — stream and feeder, each distinct per state (a dot
   colour AND a text label; colour alone is not accessible). Drawn as
   stamped chart notes: squared, hairline-ruled, mono. :down is a filled
   red alarm so a frozen map cannot read as a live one. */
.adsb-conn,
.adsb-feeder {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s1) var(--s3);
  border-radius: 2px;
  font-weight: 700;
  line-height: 1.4;
  border: 1px solid transparent;
  font-size: var(--t-1);
  letter-spacing: 0.04em;
}

.adsb-conn {
  margin-left: auto;
}

.adsb-conn-dot,
.adsb-feeder-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  border: 1px solid var(--rule-strong);
  box-sizing: border-box;
  flex: none;
}

/* The healthy dots breathe — ambient life, never a blink (§6). */
.adsb-conn-live .adsb-conn-dot,
.adsb-feeder-ok .adsb-feeder-dot {
  animation: adsb-breathe 3.2s ease-in-out infinite;
}

.adsb-conn-live {
  color: var(--ok);
  border-color: var(--rule);
}

.adsb-conn-reconnecting {
  color: var(--warn);
  border-color: var(--warn);
}

.adsb-conn-down {
  color: var(--on-emergency);
  background: var(--emergency);
  border-color: var(--emergency);
}

.adsb-feeder-ok {
  color: var(--ok);
  border-color: var(--rule);
}

.adsb-feeder-starting {
  color: var(--warn);
  border-color: var(--warn);
}

.adsb-feeder-unknown {
  color: var(--faded-ink);
  border-color: var(--rule);
}

.adsb-feeder-down {
  color: var(--on-emergency);
  background: var(--emergency);
  border-color: var(--emergency);
}

/* ── The margin column — legend and stats, chart-key marginalia ──────
   Tucked bottom-left, out of the header's way: the session stats chip
   over the map key, both on paper veils. */
.adsb-margin {
  position: absolute;
  left: var(--s5);
  bottom: var(--s5);
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
  max-width: calc(100vw - var(--stack-w) - 2 * var(--s5));
}

/* The legend — a chart margin table. Every swatch is painted from the
   adsb.map.style palette of the CURRENT edition (adsb.ui.legend derefs
   the theme ratom). */
.adsb-legend {
  display: flex;
  gap: var(--s5);
  padding: var(--s3) var(--s4);
  background: var(--paper-veil);
  color: var(--ink);
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  font-size: var(--t-1);
  box-sizing: border-box;
}

/* Block headings print in the pen: caps-tracked bold magenta (§5). */
.adsb-legend-heading {
  margin: 0 0 var(--s2);
  font-size: var(--t-2);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--magenta);
}

.adsb-legend-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.adsb-legend-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  line-height: 1.7;
  font-size: var(--t-1);
}

.adsb-legend-swatch {
  width: 13px;
  height: 13px;
  border-radius: 2px;
  flex: none;
  box-shadow: inset 0 0 0 1px var(--rule);
}

.adsb-legend-label {
  white-space: nowrap;
}

/* The session stats readout — two scalars on a small paper chip. */
.adsb-stats {
  display: flex;
  gap: var(--s4);
  padding: var(--s1) var(--s3);
  background: var(--paper-veil);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 2px;
  font-size: var(--t-1);
  box-sizing: border-box;
}

.adsb-stats-row {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}

.adsb-stats-label {
  /* voice: the printed caption rule below */
  color: var(--faded-ink);
}

.adsb-stats-value {
  font-variant-numeric: tabular-nums;
}

/* ── The NOTAM strip (§7) — the loudest chrome the app owns ──────────
   Sits directly under the header, spanning the shell, while ANY
   aircraft squawks distress. Every emergency STACKS as its own row
   rather than cycling behind a timer. Grave, drawn once: red field,
   plain mono language, a stamped NOTAM tab — and ZERO motion. Ink does
   not blink. */
.adsb-alerts {
  position: absolute;
  top: var(--header-h);
  left: 0;
  right: var(--stack-w);      /* clear of the Stack on the right edge */
  z-index: 3;                 /* above the map, the panel, the Stack */
  display: flex;
  align-items: stretch;
  background: var(--emergency);
  color: var(--on-emergency);
  border-bottom: 1px solid var(--rule-strong);
  max-width: 100%;
}

.adsb-alert-stamp {
  align-self: center;
  flex: none;
  margin: 0 0 0 var(--s4);
  padding: var(--s1) var(--s2);
  border: 1px solid var(--on-emergency);
  border-radius: 1px;
  font-size: var(--t-2);
  font-weight: 700;
  letter-spacing: 0.14em;
}

.adsb-alert-rows {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}

.adsb-alert {
  display: flex;
  align-items: baseline;
  gap: var(--s4);
  width: 100%;
  padding: var(--s3) var(--s4);
  border: none;
  border-bottom: 1px solid color-mix(in srgb, var(--on-emergency) 30%, transparent);
  background: transparent;
  color: var(--on-emergency);
  font: inherit;
  font-size: var(--t0);
  text-align: left;
  cursor: pointer;
  box-sizing: border-box;
}

.adsb-alert:last-child {
  border-bottom: none;
}

.adsb-alert:hover {
  filter: brightness(1.08);
}

.adsb-alert:focus-visible {
  outline: 2px solid var(--on-emergency);
  outline-offset: -3px;
}

.adsb-alert-name {
  font-weight: 700;
  letter-spacing: 0.02em;
}

.adsb-alert-squawk {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.adsb-alert-meaning {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--t-1);
  opacity: 0.9;
}

/* ── The detail panel — the index card (§4) ──────────────────────────
   Chart marginalia for one selected aircraft: a paper card under the
   header, clear of the Stack — ink rule under the title, italic fact
   labels, mono data, em-dashes for facts the sky never reported. It
   settles in (§6); it never crowds the plot. */
.adsb-panel {
  position: absolute;
  top: calc(var(--header-h) + var(--s3));
  right: calc(var(--stack-w) + var(--s3));
  z-index: 2;
  width: 288px;
  background: var(--paper-veil);
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  box-shadow: 2px 2px 0 var(--rule-faint);
  color: var(--ink);
  box-sizing: border-box;
  animation: adsb-settle 180ms ease-out;
}

.adsb-panel-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
  padding: var(--s3) var(--s3) var(--s2);
  border-bottom: 1px solid var(--ink);
}

.adsb-panel-title {
  font-size: var(--t1);
  font-weight: 700;
  letter-spacing: 0.04em;
  overflow-wrap: anywhere; /* a hostile callsign must not shear the card */
}

.adsb-panel-close {
  background: none;
  border: none;
  padding: 0 var(--s1);
  color: var(--faded-ink);
  font: inherit;
  font-size: var(--t1);
  line-height: 1;
  cursor: pointer;
}

.adsb-panel-close:hover {
  color: var(--ink);
}

.adsb-panel-close:focus-visible {
  outline: 2px solid var(--magenta);
  outline-offset: 1px;
}

.adsb-panel-badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  padding: var(--s2) var(--s3) 0;
}

.adsb-panel-facts {
  padding: var(--s2) var(--s3) var(--s3);
}

.adsb-fact {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s1) 0;
  border-bottom: 1px dotted var(--rule-faint);
}

.adsb-fact:last-child {
  border-bottom: none;
}

.adsb-fact-label {
  /* voice: the printed caption rule below */
  color: var(--faded-ink);
}

.adsb-fact-value {
  text-align: right;
  font-size: var(--t-1);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* Status badges — small flags on rows and in the detail panel. The
   emergency badge is the only one with a committed colour, because it
   is the only one that means someone is in danger. */
.adsb-badge {
  display: inline-block;
  padding: 1px var(--s2);
  border-radius: 2px;
  font-size: var(--t-1);
  font-weight: 700;
  white-space: nowrap;
  background: transparent;
  border: 1px solid var(--rule);
  color: var(--faded-ink);
}

.adsb-badge-emergency {
  background: var(--emergency);
  border-color: var(--emergency);
  color: var(--on-emergency);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* ── The selection mark — the compass-pencil ring (§4) ───────────────
   A dashed magenta ring around the selected aircraft, drawn as a map
   marker (adsb.map.selection) so it rides the chart, not the screen.
   It draws itself in once per selection and then holds still. */
.adsb-selection-ring {
  width: 44px;
  height: 44px;
  pointer-events: none; /* the plane beneath stays clickable */
}

.adsb-selection-ring svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.adsb-selection-ring circle {
  fill: none;
  stroke: var(--magenta);
  stroke-width: 1.6px;
  stroke-linecap: round;
  stroke-dasharray: 3.2 3.8; /* of pathLength 70 — ten pencil dashes */
  animation: adsb-ring-draw 420ms ease-out;
}

/* ── The §7 map annotations (adsb.map.emergency) ─────────────────────
   The chart's own emergency marks, beside the NOTAM strip's chrome:
   the red-pen double ellipse + MAYDAY stamp on a squawking aircraft in
   view, the edge arrow when it is beyond the frame. Both print in the
   edition's emergency red via var(--emergency) — §2's two reds, one
   variable. The ellipse's ONE-TIME draw-in is inline (see the §6
   motion note); everything in this block is still ink. */
.adsb-mayday {
  width: 84px;  /* mirror adsb.map.emergency/ellipse-box-px */
  height: 84px;
  pointer-events: none; /* the plane beneath stays clickable */
}

.adsb-mayday svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.adsb-mayday ellipse {
  fill: none;
  stroke: var(--emergency);
  stroke-width: 2px;
  stroke-linecap: round;
}

/* The MAYDAY stamp: a small rotated annotation on the paper beside the
   ellipse, in the plotter's hand — red ink, pressed slightly askew.
   Placement (left/top) is chosen once by adsb.map.emergency from the
   track at placement; only its DATA ever changes after. */
.adsb-mayday-stamp {
  position: absolute;
  transform: translate(-50%, -50%) rotate(-4deg);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  width: max-content;
  max-width: 168px;
  padding: var(--s2) var(--s3);
  background: var(--paper-veil);
  border: 1.5px solid var(--emergency);
  border-radius: 1px;
  box-shadow: 2px 2px 0 var(--rule-faint);
  color: var(--emergency);
  font-size: var(--t-1);
  line-height: 1.25;
  text-align: left;
}

.adsb-mayday-word {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  border-bottom: 1px solid var(--emergency);
  padding-bottom: var(--s1);
}

.adsb-mayday-callsign {
  font-weight: 700;
  letter-spacing: 0.02em;
  overflow-wrap: anywhere; /* a hostile callsign must not shear the stamp */
}

.adsb-mayday-facts {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  white-space: nowrap;
}

.adsb-mayday-label {
  /* voice: the printed caption rule below */
  opacity: 0.75;
}

.adsb-mayday-value {
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}

/* The edge arrow (Q13c): pinned just inside the frame on the bearing
   toward an off-screen emergency. A button — it OFFERS selection; the
   camera is never hijacked. Zero animation of any kind. */
.adsb-edge-arrow {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s1) var(--s3) var(--s1) var(--s2);
  background: var(--paper-veil);
  border: 1.5px solid var(--emergency);
  border-radius: 1px;
  box-shadow: 2px 2px 0 var(--rule-faint);
  color: var(--emergency);
  font: inherit;
  font-size: var(--t-1);
  font-weight: 700;
  cursor: pointer;
}

.adsb-edge-arrow:hover {
  filter: brightness(1.08);
}

.adsb-edge-arrow:focus-visible {
  outline: 2px solid var(--emergency);
  outline-offset: 2px;
}

.adsb-edge-arrow-glyph {
  display: block;
  width: 14px;
  height: 14px;
  fill: var(--emergency);
}

.adsb-edge-arrow-callsign {
  letter-spacing: 0.02em;
  overflow-wrap: anywhere;
}

.adsb-edge-arrow-distance {
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}

/* =====================================================================
   THE STACK (adsb-dgb.9) — BEGIN BOUNDED SECTION
   The live flight-level ruler on the map's edge (design direction §9).
   The ruler's FILL is painted inline by adsb.ui.stack from the same
   adsb.map.style altitude stops the map uses — never duplicated here.
   Each tick/graduation carries its place on the ALTITUDE AXIS as the
   unitless --alt-pct custom property; this stylesheet maps that axis to
   `bottom` (desktop, vertical) or `left` (phone, horizontal) — same DOM,
   same semantics, rotated geometry (Q9c). Ink/paper come from the
   custom-properties layer above, so the Stack prints in both editions
   (adsb-dgb.7) without knowing the theme exists.
   ===================================================================== */

.adsb-stack {
  position: absolute;
  top: var(--header-h);
  right: 0;
  bottom: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: var(--stack-w);
  padding: var(--s4) var(--s3) var(--s3);
  background: var(--paper-veil);
  border-left: 1px solid var(--rule);
  color: var(--ink);
  overflow: visible;                       /* labels spill onto the chart */
  box-sizing: border-box;
}

/* The ruler column — the gradient fill (inline style) with the airborne
   ticks and flight-level graduations positioned along it. */
.adsb-stack-ruler {
  position: relative;
  flex: 1;
  width: 22px;
  margin-left: auto;                       /* labels live to its left */
  border: 1px solid var(--rule-strong);
}

/* Flight-level graduations — dashed ink rules across the fill. */
.adsb-stack-grad {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--alt-pct, 0) * 1%);
  border-top: 1px dashed var(--rule-strong);
  pointer-events: none;
}

.adsb-stack-grad-label {
  position: absolute;
  right: calc(100% + 5px);
  transform: translateY(-50%);
  font-size: var(--t-2);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--faded-ink);
  white-space: nowrap;
}

/* A tick — one aircraft, at its true altitude. `bottom` (and `left` on
   phone) TRANSITIONS at just under the 1 Hz feeder cadence, so a
   climbing aircraft's tick drifts up the ruler instead of jumping. */
.adsb-stack-tick {
  cursor: pointer;
  transition: bottom 0.9s linear, left 0.9s linear;
}

.adsb-stack-tick:focus-visible {
  outline: 2px solid var(--magenta);
  outline-offset: 1px;
}

/* Airborne: an ink bar across the ruler with a hairline paper halo so it
   survives every band of the gradient. Centered on its altitude. */
.adsb-stack-ruler .adsb-stack-tick {
  position: absolute;
  left: -3px;
  right: -3px;
  height: 3px;
  bottom: calc(var(--alt-pct, 0) * 1%);
  transform: translateY(50%);
  background: var(--ink);
  box-shadow: 0 0 0 1px var(--paper-halo);
}

.adsb-stack-ruler .adsb-stack-tick:hover {
  height: 5px;
  z-index: 2;
}

/* Selected: the magenta accent, prominent. */
.adsb-stack-tick-selected {
  z-index: 2;
}

.adsb-stack-ruler .adsb-stack-tick-selected {
  height: 5px;
  background: var(--magenta);
  box-shadow: 0 0 0 1px var(--paper-veil);
}

/* Emergency: red, hatched, pinned prominent — ink that never blinks (§7). */
.adsb-stack-tick-emergency {
  z-index: 3;
}

.adsb-stack-ruler .adsb-stack-tick-emergency {
  height: 7px;
  background: repeating-linear-gradient(
    45deg,
    var(--emergency) 0,
    var(--emergency) 3px,
    var(--paper-veil) 3px,
    var(--paper-veil) 5px
  );
  box-shadow: 0 0 0 1px var(--emergency);
}

/* The name beside a tick — shown on hover, selection, and always for an
   emergency. Identity lives on the Stack; the map stays glyphs-only. */
.adsb-stack-tick-label {
  position: absolute;
  right: calc(100% + 8px);
  top: 50%;
  transform: translateY(-50%);
  padding: 1px var(--s2);
  background: var(--paper-chrome);
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  font-family: var(--mono);
  font-size: var(--t-1);
  line-height: 1.5;
  color: var(--ink);
  white-space: nowrap;
  pointer-events: none;
}

.adsb-stack-tick-selected .adsb-stack-tick-label {
  border-color: var(--magenta);
  color: var(--magenta);
  font-weight: 700;
}

.adsb-stack-tick-emergency .adsb-stack-tick-label {
  background: var(--emergency);
  border-color: var(--emergency);
  color: var(--on-emergency);
  font-weight: 700;
}

/* The shelves at the ruler's foot: the surface band (ground cluster) and
   the small holding area for aircraft that never reported an altitude —
   absent is not zero, so they are placed NOWHERE on the scale. */
.adsb-stack-shelf {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 3px;
  min-height: 16px;
  margin-top: var(--s3);
  padding: var(--s2) var(--s2) var(--s2) var(--s2);
  border: 1px solid var(--rule);
  border-radius: 2px;
}

.adsb-stack-shelf-label {
  /* voice: the printed caption rule below */
  color: var(--faded-ink);
  margin-right: var(--s1);
}

/* MapLibre pins its attribution to the map's bottom-right — under the
   Stack. Attribution must stay readable (OpenFreeMap's terms flow through
   it), so the control corner shifts clear of the ruler instead of hiding
   beneath it; the phone rule below lifts it above the recumbent Stack. */
.maplibregl-ctrl-bottom-right {
  right: var(--stack-w);
}

/* Shelf ticks cluster as dots — grouped, never ranked. */
.adsb-stack-shelf .adsb-stack-tick {
  position: relative;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--alt-ground);
  box-shadow: 0 0 0 1px var(--paper-halo);
}

.adsb-stack-unknown .adsb-stack-tick {
  background: var(--alt-unknown);
}

.adsb-stack-shelf .adsb-stack-tick-selected {
  background: var(--magenta);
}

.adsb-stack-shelf .adsb-stack-tick-emergency {
  background: var(--emergency);
  box-shadow: 0 0 0 1px var(--emergency), 0 0 0 2px var(--paper-veil);
}

.adsb-stack-shelf .adsb-stack-tick-label {
  right: auto;
  top: auto;
  left: 50%;
  bottom: calc(100% + 6px);
  transform: translateX(-50%);
}

/* Phone: the ruler lies down along the bottom edge — identical semantics,
   rotated geometry. --stack-axis flips the gradient; --alt-pct maps to
   `left` instead of `bottom`. Neither stage degrades (Q9c). */
@media (max-width: 640px) {
  .adsb-stack {
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    height: var(--stack-w);
    flex-direction: row;
    align-items: stretch;
    padding: 22px 10px var(--s3);
    border-left: none;
    border-top: 1px solid var(--rule);
  }

  .adsb-stack-ruler {
    --stack-axis: to right;
    flex: 1;
    width: auto;
    height: 22px;
    margin-left: 0;
    align-self: flex-end;
  }

  .adsb-stack-grad {
    left: calc(var(--alt-pct, 0) * 1%);
    right: auto;
    top: 0;
    bottom: 0;
    border-top: none;
    border-left: 1px dashed var(--rule-strong);
  }

  .adsb-stack-grad-label {
    right: auto;
    left: 0;
    top: auto;
    bottom: calc(100% + 3px);
    transform: none;
  }

  .adsb-stack-ruler .adsb-stack-tick {
    left: calc(var(--alt-pct, 0) * 1%);
    right: auto;
    top: -3px;
    bottom: -3px;
    width: 3px;
    height: auto;
    transform: translateX(-50%);
  }

  .adsb-stack-ruler .adsb-stack-tick:hover {
    height: auto;
    width: 5px;
  }

  .adsb-stack-ruler .adsb-stack-tick-selected {
    height: auto;
    width: 5px;
  }

  .adsb-stack-ruler .adsb-stack-tick-emergency {
    height: auto;
    width: 7px;
  }

  .adsb-stack-tick-label {
    right: auto;
    top: auto;
    left: 50%;
    bottom: calc(100% + 6px);
    transform: translateX(-50%);
  }

  .adsb-stack-shelf {
    margin-top: 0;
    margin-left: var(--s3);
    min-width: 34px;
    align-content: center;
  }

  /* The bottom edge is the Stack's now: the map attribution and the
     margin column both step up out of its lane. */
  .maplibregl-ctrl-bottom-right {
    right: 0;
    bottom: var(--stack-w);
  }
}

/* =====================================================================
   THE STACK — END BOUNDED SECTION
   ===================================================================== */

/* ── The printed caption voice (§5 captions, bead adsb-fon) ──────────
   One rule, one voice: every small label on the chart prints in Space
   Grotesk — Space Mono's proportional sibling — upright, uppercase,
   tracked, at --t-2. The individual blocks above keep only their own
   colour and geometry; the VOICE lives here and nowhere else. Placed
   after them on purpose: equal specificity, later wins. */
.adsb-count-unit,
.adsb-stats-label,
.adsb-fact-label,
.adsb-mayday-label,
.adsb-stack-shelf-label {
  font-family: "Space Grotesk", system-ui, sans-serif;
  font-style: normal;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: var(--t-2);
}

/* ── Phone (Q9c: a designed stance, not a degradation) ───────────────
   The header holds its 36px: the clock and the counts' unit words step
   aside (the numbers stay), the chips keep their labels. The NOTAM
   strip spans the full width (the Stack is on the bottom edge now); the
   index card spans the width under the header; the margin column steps
   above the recumbent Stack and the lifted attribution. */
@media (max-width: 640px) {
  .adsb-header {
    gap: var(--s3);
    padding: 0 var(--s3);
  }

  .adsb-clock,
  .adsb-count-unit {
    display: none;
  }

  .adsb-alerts {
    right: 0;
  }

  .adsb-panel {
    left: var(--s3);
    right: var(--s3);
    width: auto;
  }

  .adsb-margin {
    left: var(--s3);
    /* above the recumbent Stack AND the lifted attribution, which wraps
       to two lines at phone widths */
    bottom: calc(var(--stack-w) + 56px + var(--s2));
    max-width: calc(100vw - 2 * var(--s3));
  }

  .adsb-legend {
    gap: var(--s4);
    padding: var(--s2) var(--s3);
  }
}
