/* SUPNPABLO WEATHER - "Signal Desk" design system
   Adapted from willcodexquotareset.com: near-black canvas with a faint grid
   texture and one big orange glow, hairline-bordered flat panels, a single
   loud accent (#ff5033) with violet/peach secondaries, tiny monospace
   eyebrows, and oversized tight-tracked display headings.
   Light mode is the paper edition of the same language. */

:root {
  color-scheme: dark;
  --bg: #101010;
  --bg-soft: #0c0c0c;
  --surface: #161616;
  --surface-2: #1b1b1a;
  --surface-3: #262625;
  --border: #343432;
  --border-strong: #4a4846;
  --text: #faf8f7;
  --text-soft: #c9c5c2;
  --muted: #aaa7a5;
  --accent: #ff5033;
  --accent-2: #ffa273;
  --violet: #a78bfa;
  --accent-soft: rgba(255, 80, 51, 0.12);
  --cyan-soft: rgba(255, 80, 51, 0.07);
  --success: #6dd39a;
  --success-soft: rgba(109, 211, 154, 0.10);
  --warn: #e0a83c;
  --warn-soft: rgba(224, 168, 60, 0.10);
  --danger: #ff4d6a;
  --danger-soft: rgba(255, 77, 106, 0.10);
  --info-soft: rgba(167, 139, 250, 0.10);
  /* Aliases the JS chart/badge code reads; kept in step with the tokens
     above so a future palette edit cannot leave the charts stranded. */
  --ok: var(--success);
  --info: var(--violet);
  --shadow: 0 10px 28px rgba(0, 0, 0, 0.55);
  --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.4);
  --radius: 4px;
  --radius-sm: 3px;
  --topbar-h: 60px;
  --font: "Inter", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-head: "Inter", ui-sans-serif, -apple-system, "Segoe UI", sans-serif;
  /* Type scale.  Seven sizes lived between 9px and 12.5px (9, 10, 10.5, 11,
     11.5, 12, 12.5) across 120 rules.  Steps that close together do not read
     as hierarchy -- the eye cannot tell 11px from 11.5px, so the page reads as
     undifferentiated texture.  Five steps with real gaps between them:
     11 (labels) · 12.5 (captions) · 14 (body) · 16 (lead) · 18 (headings). */
  --fs-label: 11px;
  --fs-caption: 12.5px;
  --fs-body: 14px;
  --fs-lead: 16px;
  --fs-heading: 18px;
  /* Body copy gets a generous leading.  The old default sat near 1.45, which
     is tight for prose; anything a reader has to read as sentences now gets
     1.65. */
  --lh-body: 1.65;
  --lh-tight: 1.45;
  --display: "Inter", ui-sans-serif, -apple-system, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace;
  --grid-line: rgba(255, 255, 255, 0.025);
  --edge-highlight: none;
  --ambient-a: rgba(255, 80, 51, 0.08);
  --ambient-b: rgba(255, 80, 51, 0.05);
}

/* ==========================================================================
   PALETTE VARIANTS (2026-09-17; re-measured 2026-09-18)
   The semantic colour problem: the brand orange (#ff5033) and the error red
   (#ff7a6e) sat 3.6deg apart in hue, so "highlight" and "something is wrong"
   looked identical.

   Visitors choose one in the display menu (the gear in the topbar);
   the choice is stored in localStorage and applied before first paint.
   Set directly with  document.documentElement.dataset.palette = "signal",
   "desk" or "mono".

   signal : keep the warm brand accent; move danger to a true red and
            warn to a clearer amber.
   desk   : aviation instrument amber.  Amber means ATTENTION (brand, active,
            current checkpoint) and never means good or bad.
   mono   : chrome goes grayscale, colour is reserved for data states only.
            The accent becomes near-white.

   Measured 2026-09-18 with tools/audit_palette_contrast.py (OKLab delta-E,
   where >= 0.10 reads as clearly distinct, and WCAG contrast on the canvas):

     signal  closest pair accent/danger 0.061  (was 0.080 before the change)
             -> improved, but still the tightest pair in any palette
     desk    closest pair accent/warn   0.144  (was 0.000 -- see note below)
     mono    closest pair success/warn  0.126  -> no collisions at all

   Note on desk: --warn was originally the SAME hex as --accent (#ffb020
   dark, #9d5f00 light), so "attention" and "warning" were literally one
   colour -- the same collision class this block set out to remove, moved
   from accent/danger to accent/warn.  Fixed 2026-09-18 by moving --warn to
   #c7870c (dark) / #6c4800 (light), chosen by search as the colour nearest
   the accent's hue that still clears 0.10 delta-E and 4.5:1 contrast.

   Every semantic token clears 4.5:1 on its canvas in all three palettes,
   both themes.  Re-run the audit after touching any value here.
   ========================================================================== */

/* Base light theme aligned to the default palette (signal).
   A visitor with no palette set -- or one whose localStorage threw, so
   the prepaint script could not read a choice -- must see the same
   colours as a signal visitor, not the pre-2026-09-17 semantics.
   Declared last so it wins over the earlier base light layers without
   editing them.  Values mirror :root[data-palette="signal"]. */
[data-theme="light"]:not([data-palette]) {
  --success: #2f7d52;
  --success-soft: rgba(47, 125, 82, 0.09);
  --warn: #8a6410;
  --warn-soft: rgba(138, 100, 16, 0.10);
  --danger: #c42a48;
  --danger-soft: rgba(196, 42, 72, 0.09);
  --violet: #6d4bc4;
  --info-soft: rgba(109, 75, 196, 0.09);
  --accent: #c9412a;
  --accent-2: #b4643a;
}

/* --- signal: same identity, fixed semantics --- */
:root[data-palette="signal"] {
  --accent: #ff5033;
  --accent-2: #ffa273;
  --success: #6dd39a;
  --success-soft: rgba(109, 211, 154, 0.10);
  --warn: #e0a83c;
  --warn-soft: rgba(224, 168, 60, 0.10);
  --danger: #ff4d6a;
  --danger-soft: rgba(255, 77, 106, 0.10);
  --violet: #a78bfa;
  --info-soft: rgba(167, 139, 250, 0.10);
}
[data-theme="light"][data-palette="signal"] {
  --accent: #c9412a;
  --accent-2: #b4643a;
  --success: #2f7d52;
  --success-soft: rgba(47, 125, 82, 0.09);
  --warn: #8a6410;
  --warn-soft: rgba(138, 100, 16, 0.10);
  --danger: #c42a48;
  --danger-soft: rgba(196, 42, 72, 0.09);
  --violet: #6d4bc4;
  --info-soft: rgba(109, 75, 196, 0.09);
}

/* --- mono: grayscale chrome, colour only where data speaks --- */
:root[data-palette="mono"] {
  --accent: #faf8f7;
  --accent-2: #c9c5c2;
  --accent-soft: rgba(250, 248, 247, 0.08);
  --cyan-soft: rgba(250, 248, 247, 0.05);
  --ambient-a: rgba(255, 255, 255, 0.03);
  --ambient-b: rgba(255, 255, 255, 0.02);
  --success: #6dd39a;
  --success-soft: rgba(109, 211, 154, 0.10);
  --warn: #e0a83c;
  --warn-soft: rgba(224, 168, 60, 0.10);
  --danger: #ff4d6a;
  --danger-soft: rgba(255, 77, 106, 0.10);
  --violet: #a78bfa;
  --info-soft: rgba(167, 139, 250, 0.10);
}
/* The mono accent is white: interactive fills must flip their text to
   the canvas colour, and the ambient glow must not tint the page. */
:root[data-palette="mono"] .chip.active,
:root[data-palette="mono"] .tab.active { color: var(--text); }
[data-theme="light"][data-palette="mono"] {
  --accent: #242529;
  --accent-2: #525252;
  --accent-soft: rgba(36, 37, 41, 0.06);
  --cyan-soft: rgba(36, 37, 41, 0.04);
  --success: #2f7d52;
  --success-soft: rgba(47, 125, 82, 0.09);
  --warn: #8a6410;
  --warn-soft: rgba(138, 100, 16, 0.10);
  --danger: #c42a48;
  --danger-soft: rgba(196, 42, 72, 0.09);
  --violet: #6d4bc4;
  --info-soft: rgba(109, 75, 196, 0.09);
}

/* Chart series palettes follow the variant too (JS reads these). */
:root[data-palette="signal"] {
  --chart-1: #ff5033; --chart-2: #a78bfa; --chart-3: #ffa273;
  --chart-4: #6dd39a; --chart-5: #e0a83c; --chart-6: #8a8a88;
}
:root[data-palette="mono"] {
  --chart-1: #faf8f7; --chart-2: #a78bfa; --chart-3: #8a8a88;
  --chart-4: #6dd39a; --chart-5: #e0a83c; --chart-6: #5a5a58;
}

/* --- desk: aviation instrument identity ---
   Amber-on-dark is the cockpit night-lighting standard, and it is a
   physiological choice, not a stylistic one: rod cells peak near 498nm
   and barely respond above 600nm, so long-wavelength light preserves
   dark adaptation.  The site's data IS airport weather, so the
   instrument language is native to the subject.

   One rule keeps it unambiguous: amber means ATTENTION (brand, active,
   current checkpoint) and never means good or bad.  Good is green, bad
   is red, informational is blue — the same four-way split aviation
   already uses for flight categories. */
:root[data-palette="desk"] {
  --accent: #ffb020;
  --accent-2: #ffd08a;
  --accent-soft: rgba(255, 176, 32, 0.12);
  --cyan-soft: rgba(255, 176, 32, 0.07);
  --ambient-a: rgba(255, 176, 32, 0.07);
  --ambient-b: rgba(255, 176, 32, 0.04);
  --success: #4ade80;
  --success-soft: rgba(74, 222, 128, 0.10);
  --danger: #ff4d5e;
  --danger-soft: rgba(255, 77, 94, 0.10);
  --violet: #c084fc;
  --info-soft: rgba(192, 132, 252, 0.10);
  --warn: #c7870c;
  --warn-soft: rgba(199, 135, 12, 0.10);
}
[data-theme="light"][data-palette="desk"] {
  --accent: #9d5f00;
  --accent-2: #8a5a10;
  --accent-soft: rgba(157, 95, 0, 0.09);
  --cyan-soft: rgba(157, 95, 0, 0.05);
  --success: #1f7a45;
  --success-soft: rgba(31, 122, 69, 0.09);
  --danger: #c2203a;
  --danger-soft: rgba(194, 32, 58, 0.09);
  --violet: #7c3aed;
  --info-soft: rgba(124, 58, 237, 0.09);
  --warn: #6c4800;
  --warn-soft: rgba(108, 72, 0, 0.10);
}
:root[data-palette="desk"] {
  --chart-1: #ffb020; --chart-2: #60a5fa; --chart-3: #ffd08a;
  --chart-4: #4ade80; --chart-5: #c084fc; --chart-6: #8a8a88;
}

/* --- palette switcher (display menu) --- */
.palette-toggle {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border-radius: 8px;
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
}
.palette-toggle button {
  appearance: none;
  border: 0;
  background: transparent;
  cursor: pointer;
  font: 500 11px var(--font);
  color: var(--muted);
  padding: 3px 9px;
  border-radius: 6px;
  letter-spacing: 0;
}
.palette-toggle button.active { background: var(--surface-3); color: var(--text); }

[data-theme="light"] {
  color-scheme: light;
  --bg: #f4f2ef;
  --bg-soft: #eceae6;
  --surface: #ffffff;
  --surface-2: #f6f4f1;
  --surface-3: #e8e4de;
  --border: #e0dbd4;
  --border-strong: #c9c3ba;
  --text: #191512;
  --text-soft: #45403a;
  --muted: #7a736c;
  --accent: #d63c20;
  --accent-2: #ff8a5e;
  --violet: #8a5bd6;
  --accent-soft: rgba(214, 60, 32, 0.09);
  --cyan-soft: rgba(214, 60, 32, 0.06);
  --success-soft: rgba(46, 143, 90, 0.09);
  --warn-soft: rgba(176, 125, 16, 0.10);
  --danger-soft: rgba(216, 74, 53, 0.09);
  --info-soft: rgba(138, 91, 214, 0.08);
  --grid-line: rgba(0, 0, 0, 0.032);
  --ambient-a: rgba(214, 60, 32, 0.10);
  --ambient-b: rgba(214, 60, 32, 0.06);
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

html {
  background:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
    var(--bg);
  background-size: 64px 64px;
}

body {
  margin: 0;
  min-height: 100vh;
  overflow-x: clip;
  font-family: var(--font);
  background: transparent;
  color: var(--text);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* One big blurred glow behind the fold, exactly like the reference. */
body::before {
  content: "";
  position: fixed;
  z-index: -1;
  top: -18rem;
  left: 50%;
  width: 62rem;
  height: 40rem;
  border-radius: 50%;
  background: var(--ambient-a);
  filter: blur(120px);
  transform: translateX(-50%);
  pointer-events: none;
}

.panel, .stat, .topbar, .foot,
.topbar-search input, .clock, .icon-btn, .ctl, .unit-toggle {
  transition: border-color 0.15s ease;
}

/* ---------- skip link ---------- */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--accent);
  color: var(--bg);
  font-weight: 700;
  font-size: var(--fs-body);
  padding: 8px 14px;
}
.skip-link:focus { left: 0; color: var(--bg); text-decoration: none; }

a { color: inherit; text-decoration: none; }
a:hover { text-decoration: none; }

::selection { background: var(--accent-soft); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.nums, .num { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--mono); }

/* ---------- eyebrow: quiet kicker ---------- */
.eyebrow {
  margin: 0;
  color: var(--muted);
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 550;
  letter-spacing: 0;
  line-height: 1.5;
}

/* ---------- topbar ---------- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 18px;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
  transition: background-color 0.25s ease, box-shadow 0.25s ease;
}
.topbar.scrolled { border-bottom-color: var(--border-strong); }

/* ---------- station sticky strip ---------- */
/* Sits directly under the topbar once the hero scrolls away, so the
   station identity and latest capture stay visible through the long
   checkpoint log.  Hidden by default; station.js unhides via observer. */
.station-sticky {
  position: sticky;
  top: var(--topbar-h);
  z-index: 40;
  margin: 0 0 14px;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.station-sticky-inner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  font-size: var(--fs-body);
  flex-wrap: wrap;
}
.station-sticky-inner strong {
  font-family: var(--mono);
  font-size: 14px;
  letter-spacing: 0;
}
.station-sticky-inner .nums { font-weight: 600; }
@media (max-width: 640px) {
  .station-sticky-inner { gap: 8px; padding: 7px 10px; font-size: var(--fs-caption); }
}

/* ---------- floating compare bar (stations page) ---------- */
/* Follows the user once stations are in the compare cart, so the
   "Compare →" affordance is always one click away. */
.compare-bar {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: 18px;
  z-index: 45;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: inset 0 0 0 0.5px var(--border-strong), 0 6px 24px rgba(0, 0, 0, 0.22);
}
.compare-bar .sel-chip { padding: 2px 8px; border-radius: 999px; border: 0; box-shadow: inset 0 0 0 0.5px var(--border); font-family: var(--mono); }
@media (max-width: 640px) {
  .compare-bar { left: 10px; right: 10px; transform: none; flex-wrap: wrap; border-radius: var(--radius); }
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: var(--fs-body);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
  white-space: nowrap;
}
.brand:hover { text-decoration: none; }

.brand-mark {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border-strong);
  background: var(--surface);
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  color: var(--accent);
  flex: 0 0 auto;
  /* The slot holds an <img> when a brand asset is supplied, and falls back
     to a monogram in the slot's own type when it is not.  overflow:hidden
     keeps the image inside the rounded corners. */
  overflow: hidden;
}

/* The mark fills its tile.  display:block removes the inline baseline gap
   that would otherwise push the image up and clip its base. */
.brand-mark img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.brand em { font-style: normal; color: var(--muted); font-weight: 500; }

.nav { display: flex; align-items: center; gap: 2px; margin-left: 6px; }

/* Nav groups: the inline links (Dashboard, Stations, Predict) sit in a
   cluster; the five secondary pages live in .nav-more's dropdown.  The nav
   used to be a horizontal scroller (overflow-x: auto) when it carried
   eight links -- that is gone, because a scroller clips an
   absolutely-positioned dropdown.  With three inline links it cannot
   overflow on desktop, and on phones the whole nav is display:none and
   the drawer takes over. */
.nav-group { display: inline-flex; align-items: center; gap: 2px; }
.nav-sep {
  width: 0.5px;
  height: 16px;
  background: var(--border);
  margin: 0 7px;
  flex: 0 0 auto;
}

/* ---------- nav: secondary-pages menu ---------- */
.nav-more { position: relative; display: inline-flex; align-items: center; }
.nav-more-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--muted);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.nav-more-btn:hover { color: var(--text-soft); background: var(--surface-2); }
.nav-more-btn[aria-expanded="true"] { color: var(--text); background: var(--surface-2); }
/* The current page is inside this menu: mark the button, because the
   closed menu cannot show its own .active link. */
.nav-more-btn.active { color: var(--text); background: var(--surface-2); }
.nav-more-menu {
  position: absolute;
  left: 0;
  top: calc(100% + 8px);
  z-index: 70;
  min-width: 170px;
  padding: 6px;
  background: var(--surface);
  border-radius: 10px;
  box-shadow: inset 0 0 0 0.5px var(--border-strong), var(--shadow);
  flex-direction: column;
  gap: 1px;
}
.nav-more-menu:not([hidden]) { display: flex; }
/* Two classes deep on purpose: the reskin block further down re-styles
   ".nav a" at equal specificity, and source order would let it override
   the menu's own padding. */
.nav .nav-more-menu a {
  padding: 8px 10px;
  border-radius: 7px;
  color: var(--text-soft);
  white-space: nowrap;
}
.nav .nav-more-menu a:hover { color: var(--text); background: var(--surface-2); }
.nav .nav-more-menu a.active { color: var(--text); background: var(--surface-2); }

.nav a {
  padding: 6px 9px;
  color: var(--muted);
  font-family: var(--font);
  font-weight: 500;
  font-size: var(--fs-body);
  letter-spacing: 0;
  border-radius: 7px;
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease);
}
.nav a:hover { color: var(--text-soft); }
.nav a.active { color: var(--text); background: var(--surface-2); }

/* ---------- display menu (set-once controls) ---------- */
.display-menu { position: relative; display: inline-flex; }
.display-pop {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 70;
  min-width: 250px;
  padding: 12px 14px;
  background: var(--surface);
  border: 0;
  border-radius: 10px;
  box-shadow: inset 0 0 0 0.5px var(--border-strong), var(--shadow);
}
.display-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}
.display-row + .display-row { margin-top: 10px; }
.display-label { font-size: var(--fs-caption); color: var(--muted); white-space: nowrap; }

/* EDGE FINDER: a nav-level call-out that sits beside the topbar search
   (not inside .nav) - accent hairline so it reads as the shortcut to the
   cross-station bargain scan. */
.edge-finder-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  border: 0;
  box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--accent) 45%, var(--border));
  border-radius: 8px;
  background: transparent;
  color: var(--accent);
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 550;
  letter-spacing: 0;
  white-space: nowrap;
  flex: 0 0 auto;
  transition: box-shadow var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.edge-finder-btn:hover {
  box-shadow: inset 0 0 0 1px var(--accent);
  background: var(--accent-soft);
  text-decoration: none;
}
.edge-finder-btn.active { background: var(--accent-soft); }

/* Signed out: the same control, visibly locked.

   The button used to be REMOVED when signed out, which hid the feature from
   every visitor.  Keeping it visible but muted fixes what was actually wrong --
   the accent styling promised a usable link and the click delivered a login
   form.  Muted + padlock + "sign in to use" makes the click honest, and the
   link still carries ?next= so signing in lands on the edge finder.

   Colour and cursor are the only changes; the size, padding and border stay, so
   the topbar does not reflow between signed-in and signed-out states. */
.edge-finder-btn.is-locked {
  color: var(--muted);
  box-shadow: inset 0 0 0 0.5px var(--border);
  cursor: help;
}
.edge-finder-btn.is-locked:hover {
  box-shadow: inset 0 0 0 0.5px var(--border-strong);
  background: transparent;
  text-decoration: none;
}
.edge-finder-lock { font-size: 10px; line-height: 1; }

/* ---------- Polymarket deep links ----------
   A link out to the exact market a forecast is about.  Styled to read as the
   same family as .edge-finder-btn: the accent outline says "this leaves the
   site", and the small ↗ makes that literal rather than implied. */
.market-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  border-radius: 8px;
  box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--accent) 45%, var(--border));
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 550;
  text-decoration: none;
  white-space: nowrap;
  transition: box-shadow var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.market-link:hover {
  box-shadow: inset 0 0 0 1px var(--accent);
  text-decoration: none;
}
.market-link::before { content: "↗"; font-size: 11px; opacity: 0.75; }
.market-link-compact { padding: 3px 8px; font-size: var(--fs-caption); }
.market-link-secondary {
  margin-left: 7px;
  font-size: 11px;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.market-link-secondary:hover { color: var(--accent); }

.topbar-search { position: relative; width: 180px; flex: 0 1 180px; }
.topbar-search input {
  width: 100%;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 28px 6px 26px;
  font: inherit;
  font-size: var(--fs-body);
  transition: border-color 0.15s ease;
}
.topbar-search input:focus { border-color: var(--border-strong); outline: none; }
.topbar-search input::placeholder { color: var(--muted); }
.topbar-search::before {
  content: "";
  position: absolute;
  left: 9px;
  top: 50%;
  transform: translateY(-52%);
  width: 12px;
  height: 12px;
  color: var(--muted);
  background-color: currentColor;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5'%3E%3Ccircle cx='10.5' cy='10.5' r='7'/%3E%3Cline x1='15.5' y1='15.5' x2='21' y2='21'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5'%3E%3Ccircle cx='10.5' cy='10.5' r='7'/%3E%3Cline x1='15.5' y1='15.5' x2='21' y2='21'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: contain; mask-size: contain;
  pointer-events: none;
}
.topbar-search .search-key {
  position: absolute;
  right: 7px;
  top: 50%;
  transform: translateY(-50%);
  font: 500 10px var(--mono);
  color: var(--muted);
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: 3px;
  padding: 1px 5px;
  pointer-events: none;
}
.topbar-search:focus-within .search-key,
.topbar-search.has-value .search-key { display: none; }
.station-pop {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  min-width: 260px;
  max-height: 320px;
  overflow-y: auto;
  background: var(--surface);
  border: 0;
  border-radius: 10px;
  box-shadow: inset 0 0 0 0.5px var(--border-strong), var(--shadow);
  padding: 4px;
  display: none;
  z-index: 60;
}
.station-pop.open { display: block; }
.station-pop-item {
  display: block;
  width: 100%;
  text-align: left;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-body);
  padding: 7px 9px;
  border-radius: 3px;
  cursor: pointer;
}
.station-pop-item:hover { background: var(--surface-2); }
.station-pop-item b { font-family: var(--mono); font-weight: 700; margin-right: 8px; color: var(--accent); }
.station-pop-item span { color: var(--muted); font-size: var(--fs-caption); }

/* "Recent" header inside the station search popup */
.station-pop-label {
  display: block;
  padding: 6px 10px 4px;
  font: 600 10px var(--mono);
  letter-spacing: 0.08em;
  
  color: var(--muted);
  border-bottom: 1px solid var(--border);
  margin-bottom: 2px;
}

/* Profile switch: dip vs confirm strategy ledgers. */
.profile-switch { display: inline-flex; gap: 6px; align-items: center; }
.profile-pill { cursor: pointer; }
.profile-pill.active { background: var(--fg, #fff); color: var(--bg, #0b0d10); border-color: var(--fg, #fff); }

/* Confirm profile control card (H16 confirmation strategy knobs). */
.ctl-confirm { border-color: color-mix(in srgb, var(--ok, #3ecf8e) 30%, var(--border)); }

/* Demo controls: grouped cards with a settings grid instead of inline
   run-on rows.  Each group is a bordered card; fields sit in a responsive
   grid with their unit as a suffix chip. */
.demo-controls { display: flex; flex-direction: column; gap: 14px; }

/* Positions panel: open rows on top, settled history collapsed behind a
   details drawer so the desk view stays short. */
.settled-details { border-top: 1px solid var(--border); }
.settled-details > summary {
  cursor: pointer;
  padding: 10px 16px;
  font: 600 12px var(--mono);
  letter-spacing: 0.06em;
  
  color: var(--muted);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 8px;
}
.settled-details > summary::before { content: "▸"; font-size: var(--fs-label); transition: transform 0.15s ease; }
.settled-details[open] > summary::before { transform: rotate(90deg); }
.settled-details > summary:hover { color: var(--fg, inherit); }
.settled-details > .panel-body { padding-top: 4px; }
.settled-details table { opacity: 0.82; }

.ctl-group { border: 0; box-shadow: inset 0 0 0 0.5px var(--border); border-radius: 10px; padding: 12px 14px 14px; background: var(--surface); }
.ctl-group-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.ctl-group-head > span:first-child { font: 550 12px var(--font); letter-spacing: 0; color: var(--muted); }
.ctl-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.ctl-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); gap: 10px 14px; }
.ctl-field { display: flex; align-items: center; gap: 7px; font-size: var(--fs-body); color: var(--text-soft); white-space: nowrap; min-width: 0; }
.ctl-field input { flex: 1 1 auto; min-width: 0; width: 100%; }
.ctl-field .unit { color: var(--muted); font: 600 11px var(--mono); flex: 0 0 auto; }
.ctl-check { display: flex; align-items: center; gap: 6px; font-size: var(--fs-caption); color: var(--muted); }
.ctl-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 10px; }
.ctl-note { margin: 8px 0 0; font-size: var(--fs-caption); color: var(--muted); }
.ctl-note-inline { font-size: var(--fs-caption); }
.ctl-lock { font: 550 11px var(--font); letter-spacing: 0; color: var(--muted); box-shadow: inset 0 0 0 0.5px var(--border); border-radius: 999px; padding: 3px 10px; }
.ctl-lock.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, var(--border)); }
.ctl-danger { border-color: color-mix(in srgb, var(--danger, #ff6b6b) 35%, var(--border)); }
.chip.danger { color: var(--danger, #ff6b6b); border-color: color-mix(in srgb, var(--danger, #ff6b6b) 35%, var(--border)); }
button.chip:disabled { opacity: 0.45; cursor: not-allowed; }
@media (max-width: 640px) {
  .ctl-grid { grid-template-columns: 1fr 1fr; }
  .ctl-field { white-space: normal; }
}

.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 7px; }

.clock {
  font-family: var(--font);
  font-size: 11px;
  color: var(--muted);
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: 6px;
  background: var(--surface);
  padding: 5px 9px;
  white-space: nowrap;
}

.icon-btn {
  appearance: none;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  color: var(--text-soft);
  border-radius: 6px;
  padding: 6px 11px;
  font-family: var(--font);
  font-size: 11px;
  letter-spacing: 0.04em;
  
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.icon-btn:hover {
  border-color: var(--border-strong);
  color: var(--text);
  background: var(--surface-2);
}
.icon-btn:active { transform: translateY(1px); }

.unit-toggle { display: inline-flex; border: 0; box-shadow: inset 0 0 0 0.5px var(--border); border-radius: 8px; overflow: hidden; background: var(--surface); }
.unit-toggle button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 500;
  letter-spacing: 0;
  padding: 6px 10px;
  cursor: pointer;
}
.unit-toggle button.on { color: var(--accent); }
.unit-toggle button + button { border-left: 1px solid var(--border); }

/* ---------- layout ---------- */
/* 1120px, down from 1180px -- and 1120 is a CEILING, not a preference.

   The dashboard's where-to-look board shares this container and holds five
   fixed 200px cards with 20px gaps: 5*200 + 4*20 = 1080px of CONTENT.  With the
   20px padding each side, the container must stay at 1120px or wider, or every
   group on that board drops to four per row.

   So this narrows the page by 60px (about 5%) and no further.  That is a small
   change on its own, and it is worth being clear about why: the wide-box
   imbalance is mostly in the HERO (which uses 23% of its width), while the data
   tables already use 94-96% and would only get worse if the container shrank
   more.  Narrowing is therefore the safe half of the fix, not the whole one. */
main { max-width: 1120px; margin: 0 auto; padding: 20px 20px 72px; }

.section { margin-top: 40px; }
.section.tight { margin-top: 16px; }

.sec-head { margin: 0 0 12px; }
.sec-head h2 {
  margin: 0;
  font-family: var(--font-head);
  font-size: 17px;
  font-weight: 550;
  letter-spacing: -0.01em;
  color: var(--text);
}
.sec-head p { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-body); line-height: 1.55; max-width: 72ch; }
.sec-head .filters { margin-top: 10px; }

/* ---------- hero: the forecast card ---------- */
.hero {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 18px;
  justify-content: space-between;
  padding: 24px 26px;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.hero > * { position: relative; z-index: 1; }

.hero-tag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 550;
  letter-spacing: 0;
  color: var(--muted);
  margin-bottom: 8px;
}
.hero-tag::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--success);
}
@keyframes blink { 50% { opacity: 0.25; } }

.hero h1 {
  margin: 0;
  font-family: var(--font-head);
  font-size: clamp(24px, 3.2vw, 36px);
  font-weight: 550;
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.hero h1 span {
  display: block;
  font-size: 14px;
  font-weight: 450;
  color: var(--muted);
  letter-spacing: 0;
  line-height: 1.55;
  margin-top: 8px;
  max-width: 68ch;
}

.hero-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 11px;
  border-radius: 999px;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--muted);
}
.status-pill .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border-strong); }
.status-pill.on {
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 30%, var(--border));
}
.status-pill.on .dot {
  background: var(--success);
  box-shadow: 0 0 0 3px var(--success-soft), 0 0 10px color-mix(in srgb, var(--success) 55%, transparent);
  animation: blink 1.8s steps(2, start) infinite;
}
.status-pill.offline {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 30%, var(--border));
}
.status-pill.offline .dot {
  background: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-soft);
}

.last-updated { font-size: var(--fs-caption); color: var(--muted); }

/* ---------- stat cards ---------- */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
  gap: 10px;
}

.stat {
  position: relative;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 14px 16px;
  overflow: hidden;
}
.stat:hover { box-shadow: inset 0 0 0 1px var(--border-strong); }
.stat .label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 550;
  letter-spacing: 0;
  color: var(--muted);
}
.stat .label::before {
  content: "";
  flex: 0 0 auto;
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: var(--border-strong);
}
.stat .value {
  font-family: var(--font-head);
  font-size: 22px;
  font-weight: 550;
  letter-spacing: -0.01em;
  margin-top: 6px;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
  color: var(--text);
}
.stat .value small { font-size: var(--fs-body); font-weight: 500; color: var(--muted); }
.stat .sub { font-size: var(--fs-caption); color: var(--muted); margin-top: 3px; line-height: 1.4; }

/* ---------- panels ---------- */
.panel {
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.panel-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  border-bottom: 0.5px solid var(--border);
  background: transparent;
}
.panel-head h3 {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 550;
  letter-spacing: 0;
  color: var(--text);
}
.panel-head .right { margin-left: auto; font-size: var(--fs-caption); color: var(--muted); font-family: var(--font); }

.panel-body { padding: 16px 18px; }

/* ---------- chips / buttons ---------- */
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
  appearance: none;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  color: var(--muted);
  border-radius: 8px;
  padding: 6px 12px;
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 500;
  letter-spacing: 0;
  cursor: pointer;
  transition: box-shadow var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.chip:hover { box-shadow: inset 0 0 0 1px var(--border-strong); color: var(--text-soft); }
.chip:active { transform: translateY(1px); }
.chip.active {
  box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--accent);
}

/* ---------- segmented tabs ---------- */
.tabs-bar {
  display: inline-flex;
  align-items: stretch;
  gap: 2px;
  padding: 3px;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: 10px;
  background: var(--surface);
  max-width: 100%;
}
.tab {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 0;
  background: transparent;
  color: var(--muted);
  border-radius: 7px;
  padding: 6px 12px;
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 500;
  letter-spacing: 0;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease);
}
.tab:hover { color: var(--text-soft); }
.tab.active {
  background: var(--surface-3);
  color: var(--text);
}
.tab-num {
  font-family: var(--font);
  font-size: var(--fs-label);
  font-weight: 600;
  line-height: 1;
  padding: 2px 6px;
  border-radius: 999px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 0.5px var(--border);
  color: var(--muted);
}
.tab.active .tab-num { color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }

/* ---------- search ---------- */
.search {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  border-radius: 8px;
  padding: 6px 12px;
}
.search input {
  border: 0;
  outline: none;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-body);
  width: 200px;
}
.search input::placeholder { color: var(--muted); }

/* ---------- tables ---------- */
.table-wrap { overflow-x: auto; }

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-caption);
}
table.data th {
  text-align: left;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 550;
  letter-spacing: 0;
  color: var(--muted);
  padding: 9px 12px;
  border-bottom: 0.5px solid var(--border);
  white-space: nowrap;
}
table.data th.sortable { cursor: pointer; user-select: none; }
table.data th.sortable:hover { color: var(--text-soft); }
table.data th .arrow { opacity: 0.5; font-size: var(--fs-label); }
/* The column currently sorted: full-strength arrow, so a click gives visible
   feedback.  Every header used to show a static glyph, which made a no-op sort
   look like a dead header. */
table.data th.sorted { color: var(--text); }
table.data th.sorted .arrow { opacity: 1; }
table.data td {
  padding: 9px 12px;
  border-bottom: 0.5px solid var(--border);
  vertical-align: middle;
}
table.data tbody tr:last-child td { border-bottom: 0; }
table.data tbody tr { transition: background var(--t-fast) var(--ease); }
table.data tbody tr:hover { background: var(--surface-2); }
table.data .num { text-align: right; }

/* Sticky header row inside every scrolling table wrapper: with 37-station
   tables and 48-row logs, scrolling to row 30 used to lose the column
   labels.  The thead sticks to the wrapper's top edge; capped tables get
   their own max-height so the page never turns into a 3000px strip. */
.table-wrap { position: relative; }
.table-wrap table.data thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface-2);
}
.table-wrap.table-capped { max-height: min(62vh, 640px); overflow-y: auto; }
table.data .muted { color: var(--muted); }
table.data a.station-link,
table.data a strong {
  font-weight: 550;
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-caption);
}

/* ---------- badges: hairline chip, colored text ---------- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border-radius: 6px;
  padding: 2px 8px;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 550;
  letter-spacing: 0;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: transparent;
  white-space: nowrap;
}
.badge.success { color: var(--success); box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--success) 35%, var(--border)); }
.badge.warn { color: var(--warn); box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--warn) 35%, var(--border)); }
/* Missed checkpoint rows: honest hole markers, dimmed so captured rows lead */
.missed-row td { opacity: 0.62; }
.missed-row:hover td { opacity: 1; }
.badge.danger { color: var(--danger); box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--danger) 35%, var(--border)); }
.badge.info { color: var(--violet); box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--violet) 35%, var(--border)); }
.badge.dim { color: var(--muted); }

/* ---------- KPI delta pills ---------- */
.delta-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 1px 8px;
  border-radius: 6px;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 550;
  letter-spacing: 0;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: transparent;
}
.delta-pill.good { color: var(--success); box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--success) 40%, var(--border)); }
.delta-pill.bad { color: var(--danger); box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--danger) 40%, var(--border)); }
.delta-pill.flat { color: var(--muted); }

.conf-high { color: var(--success); }
.conf-medium { color: var(--warn); }
.conf-low { color: var(--danger); }

/* ---------- station cards ---------- */
.fresh-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: 1px;
}
.station-card .label {
  display: flex;
  align-items: center;
  gap: 0;
  white-space: nowrap;
}
.station-card .track-row {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-size: var(--fs-caption);
  padding: 6px 8px;
  border-radius: var(--radius);
  background: var(--surface-2);
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  margin-bottom: 8px;
}
.station-card .track-row b { font-size: var(--fs-body); }
.station-card { cursor: pointer; }
.station-card:hover { border-color: var(--border-strong); }

/* ---------- range bar ---------- */
.range-track {
  position: relative;
  height: 7px;
  border-radius: 2px;
  background: var(--surface-3);
  min-width: 90px;
}
.range-band {
  position: absolute;
  top: 0; bottom: 0;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  opacity: 0.85;
}
.range-center {
  position: absolute;
  top: -3px;
  width: 2px;
  height: 13px;
  border-radius: 1px;
  background: var(--text);
}

/* ---------- chart ---------- */
.chart-box { position: relative; width: 100%; }
.chart-svg { display: block; width: 100%; height: auto; }
.chart-tip {
  position: absolute;
  pointer-events: none;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 0;
  border-radius: 8px;
  padding: 7px 9px;
  font-size: var(--fs-caption);
  box-shadow: inset 0 0 0 0.5px var(--border-strong), var(--shadow);
  min-width: 130px;
  z-index: 5;
  opacity: 0;
  transition: opacity 0.1s ease;
}
.chart-tip.show { opacity: 1; }
.chart-legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px; font-size: var(--fs-caption); color: var(--text-soft); }
.chart-legend i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 5px; }

/* ---------- interval viz ---------- */
.interval-viz { margin-top: 12px; }
.interval-track {
  position: relative;
  height: 10px;
  border-radius: 2px;
  background: var(--surface-3);
  margin: 14px 0;
}
.interval-band {
  position: absolute;
  top: 0; bottom: 0;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  opacity: 0.7;
}
.interval-center {
  position: absolute;
  top: -4px;
  width: 3px;
  height: 18px;
  border-radius: 1px;
  background: var(--text);
}
.interval-obs {
  position: absolute;
  top: -5px;
  width: 3px;
  height: 20px;
  border-radius: 1px;
  background: var(--violet);
}
/* Whole-degree boundaries.  Polymarket settles on the nearest whole degree, so
   each X.5 line is where a bucket changes hands: it is the conversion from the
   continuous centre to the discrete thing being traded.  Drawn as marks rather
   than labels, because a number at each line would be five new numbers on the
   card and the owner has ruled that out. */
.interval-tick {
  position: absolute;
  top: -3px;
  width: 1px;
  height: 16px;
  background: var(--border-strong);
  opacity: 0.75;
}
.interval-tick.is-bucket {
  background: var(--text);
  opacity: 0.95;
}

/* ---------- misc layout ---------- */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 860px) { .grid-2 { grid-template-columns: 1fr; } }
@media (max-width: 640px) {
  main { padding: 14px 12px 56px; }
  .nav { display: none; }
  /* The topbar is chrome; on a phone it was 112px because the search wrapped
     to its own row above the fold.  Measured: that alone was 14% of the
     screen, spent on a control the ledger already has inline.  The search
     moves below the masthead's actions rather than claiming a row first. */
  .topbar { flex-wrap: nowrap; height: auto; padding: 8px 12px; gap: 6px; }
  .topbar-search { order: 4; width: 100%; flex-basis: auto; margin-top: 2px; }
  .topbar-search input { width: 100%; }
  .edge-finder-btn { padding: 6px 8px; }
  .hero { padding: 20px 18px; }
  .clock { display: none; }
  .search-key { display: none; }
}

/* Mobile nav drawer: the 9-link nav becomes a toggle + slide-down sheet.
   Desktop keeps the inline nav untouched (the toggle is display:none above). */
.nav-drawer-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  background: transparent;
  color: var(--text-soft);
  font-size: 15px;
  cursor: pointer;
  flex: 0 0 auto;
  position: relative;
  z-index: 70;
}
.nav-drawer-toggle:hover { border-color: var(--accent); color: var(--accent); }

/* The reveal lives AFTER the base rule on purpose: equal specificity means
   source order decides, and the mobile rule must win where it applies. */
@media (max-width: 640px) {
  .topbar .nav-drawer-toggle { display: inline-flex; }
}
.nav-drawer {
  display: none;
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(78vw, 300px);
  background: var(--surface-1);
  border-left: 1px solid var(--border);
  z-index: 60;
  padding: 56px 18px 18px;
  flex-direction: column;
  gap: 2px;
  overflow-y: auto;
}
.nav-drawer.open { display: flex; }
.nav-drawer a {
  padding: 12px 10px;
  font-size: 14px;
  color: var(--text-soft);
  text-decoration: none;
  border-bottom: 1px solid var(--border);
}
.nav-drawer a:hover { color: var(--accent); }
.nav-drawer a.active { color: var(--accent); }
.nav-drawer-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 34px;
  height: 34px;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  background: transparent;
  color: var(--text-soft);
  font-size: 16px;
  cursor: pointer;
}
.nav-drawer-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 55;
}
.nav-drawer-backdrop.open { display: block; }

.skeleton {
  border-radius: var(--radius);
  background: linear-gradient(100deg,
    var(--surface-2) 40%,
    var(--surface-3) 50%,
    var(--surface-2) 60%);
  background-size: 200% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
  height: 80px;
}
@keyframes shimmer {
  0% { background-position: 120% 0; }
  100% { background-position: -80% 0; }
}

.empty-state, .error-state {
  padding: 24px 18px;
  text-align: center;
  color: var(--muted);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  font-size: var(--fs-body);
}
.error-state { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); }

/* ---------- footer ---------- */
.foot {
  max-width: 1180px;
  margin: 8px auto 0;
  border-top: 1px solid var(--border);
  padding: 24px 20px 34px;
  text-align: left;
  color: var(--muted);
  font-family: var(--font);
  font-size: var(--fs-label);
  letter-spacing: 0.05em;
  line-height: 1.9;
  
  display: flex;
  flex-direction: column;
  gap: 6px;
}
@media (max-width: 720px) { .foot { margin-inline: 12px; } }
.foot .warn-line {
  display: inline-flex;
  align-self: flex-start;
  align-items: center;
  gap: 7px;
  color: var(--warn);
  font-weight: 500;
  letter-spacing: 0.08em;
  font-size: var(--fs-label);
  
  padding: 4px 10px;
  border-radius: 3px;
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--warn) 35%, var(--border));
  margin-bottom: 4px;
}

dl.kv { display: grid; grid-template-columns: auto 1fr; gap: 8px 18px; margin: 0; font-size: var(--fs-caption); }
dl.kv dt { color: var(--muted); font-weight: 500; }
dl.kv dd { margin: 0; font-family: var(--mono); font-size: var(--fs-caption); }

.doc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 860px) { .doc-grid { grid-template-columns: 1fr; } }
.doc-grid .doc-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 860px) { .doc-grid .doc-grid-2 { grid-template-columns: 1fr; } }
/* Prose inside doc panels reads better capped; tables and grids keep
   full width (they opt out via .table-wrap / .stat-grid children). */
.doc-grid .panel-body > p,
.doc-grid .panel-body > ul,
.doc-grid .panel-body > ol { max-width: 72ch; }

.doc-list { margin: 0; padding-left: 18px; }
.doc-list li { margin-bottom: 6px; }

.big-note {
  padding: 10px 14px;
  border-radius: var(--radius);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--warn) 35%, var(--border));
  color: var(--warn);
  font-weight: 550;
  font-size: var(--fs-body);
}
.big-note.ok { border-color: color-mix(in srgb, var(--success) 35%, var(--border)); color: var(--success); }
.big-note.info { border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); color: var(--accent); }

select.ctl, .ctl {
  appearance: none;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  color: var(--text-soft);
  border-radius: 6px;
  padding: 6px 12px;
  font: inherit;
  font-size: var(--fs-caption);
  cursor: pointer;
  font-weight: 500;
}

.two-col { display: grid; grid-template-columns: 7fr 5fr; gap: 12px; }
@media (max-width: 860px) { .two-col { grid-template-columns: 1fr; } }

main { animation: page-in 0.2s ease both; }
@keyframes page-in {
  from { opacity: 0; transform: translateY(3px); }
  to { opacity: 1; transform: none; }
}

.to-top {
  position: fixed;
  right: 16px;
  bottom: 64px;
  z-index: 60;
  width: 36px;
  height: 36px;
  padding: 0;
  font-size: 14px;
  border-radius: 6px;
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.to-top.show { opacity: 1; pointer-events: auto; transform: none; }

.back-link {
  display: inline-block;
  margin-top: 10px;
  color: var(--muted);
  font-family: var(--font);
  font-size: 11px;
  
  letter-spacing: 0.06em;
}
.back-link:hover { color: var(--accent); }

.sec-head h2, .hero h1, .panel-head h3 { text-wrap: balance; }

* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: padding-box; }

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

.toast {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translate(-50%, 12px);
  z-index: 90;
  background: var(--surface-3);
  border: 0;
  color: var(--text);
  font-size: var(--fs-body);
  font-weight: 550;
  padding: 8px 14px;
  border-radius: 10px;
  box-shadow: inset 0 0 0 0.5px var(--border-strong), var(--shadow);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-base) var(--ease), transform var(--t-base) var(--ease);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

.copy-btn { margin-left: 8px; padding: 2px 7px; font-size: 11px; vertical-align: 1px; }

.notfound-hero::after { background: linear-gradient(180deg, var(--danger), var(--warn)); }
.notfound-hero {
  background:
    linear-gradient(120deg, var(--danger-soft), transparent 38%),
    linear-gradient(var(--surface), var(--surface));
}

.picker-wrap { position: relative; }
.picker-input {
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 6px;
  padding: 7px 12px;
  font: inherit;
  font-size: var(--fs-body);
  width: min(260px, 100%);
}
.picker-input:focus { border-color: var(--border-strong); outline: none; }
.picker-input::placeholder { color: var(--muted); }
.picker-pop {
  position: absolute;
  left: 0;
  top: calc(100% + 4px);
  min-width: 300px;
  max-height: 300px;
  overflow-y: auto;
  background: var(--surface);
  border: 0;
  border-radius: 10px;
  box-shadow: inset 0 0 0 0.5px var(--border-strong), var(--shadow);
  padding: 4px;
  display: none;
  z-index: 60;
}
.picker-pop.open { display: block; }
.picker-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  text-align: left;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-body);
  padding: 7px 9px;
  border-radius: var(--radius);
  cursor: pointer;
}
.picker-item:hover, .picker-item.hover { background: var(--surface-2); }
.picker-item.selected { opacity: 0.45; cursor: default; }
.picker-item b { font-family: var(--mono); font-weight: 700; color: var(--accent); }
.picker-item span { color: var(--muted); font-size: var(--fs-caption); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-flag {
  flex: 0 0 auto;
  font-style: normal;
  font-family: var(--font);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: 0.06em;
  
  color: var(--muted);
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: 2px;
  padding: 1px 5px;
}
.picker-empty { padding: 10px; font-size: var(--fs-body); color: var(--muted); }

.sel-chips { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; min-height: 30px; margin-top: 4px; }
.sel-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
  background: var(--accent-soft);
  color: var(--text);
  border-radius: var(--radius);
  padding: 5px 6px 5px 10px;
  font-family: var(--mono);
  font-size: var(--fs-body);
  font-weight: 600;
}
.sel-chip i { width: 8px; height: 8px; border-radius: 2px; flex: 0 0 auto; }
.sel-chip button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-body);
  line-height: 1;
  padding: 2px 4px;
  cursor: pointer;
  border-radius: 3px;
}
.sel-chip button:hover { color: var(--danger); }

.win-cell { font-weight: 600; }

.pulse-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 10px 14px;
}
.pulse-left { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.pulse-right { margin-left: auto; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.pill-mini {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 500;
  color: var(--text-soft);
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: transparent;
  border-radius: 999px;
  padding: 4px 11px;
  white-space: nowrap;
}
a.pill-mini { color: var(--accent); }
a.pill-mini:hover { border-color: var(--accent); }
.pill-mini b { font-family: var(--mono); font-weight: 700; color: var(--text); }
.pill-mini.strong { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); color: var(--accent); }
.pill-mini.strong b { color: var(--accent); }
.pill-mini .dotc { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.pulse-sep { width: 1px; height: 18px; background: var(--border); margin: 0 4px; }

.stat-link { display: block; color: inherit; }
.stat-link:hover { color: inherit; }
.stat-link .stat { height: 100%; cursor: pointer; }

.recap-details summary {
  cursor: pointer;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 500;
  color: var(--muted);
  
  letter-spacing: 0.07em;
  padding: 4px 0;
  user-select: none;
}
.recap-details summary:hover { color: var(--text-soft); }
.recap-details[open] summary { margin-bottom: 8px; color: var(--text-soft); }

.station-card { position: relative; }
.station-card .card-cover {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
}
.station-card .card-cover:hover { text-decoration: none; }
.cmp-add {
  position: relative;
  z-index: 2;
  appearance: none;
  margin-left: auto;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: 0.05em;
  
  border-radius: 999px;
  padding: 3px 9px;
  cursor: pointer;
  transition: all 0.12s ease;
}
.cmp-add:hover { color: var(--accent); border-color: var(--accent); }
.cmp-add.in { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.conf-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  vertical-align: 1px;
}

.map-wrap { position: relative; }
.map-svg { display: block; width: 100%; height: auto; }
.map-dot { cursor: pointer; }
.map-dot:hover circle { stroke-width: 2.5; filter: brightness(1.15); }
.map-tip { min-width: 170px; max-width: 260px; }
.map-legend {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 10px;
  font-size: var(--fs-caption);
}
.map-legend i {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 5px;
  vertical-align: -1px;
}
.notfound-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin: 16px 0 0;
}
.notfound-actions .icon-btn {
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  color: var(--text-soft);
}

/* ---- engine page: overall tier score ---- */
.tier-card {
  display: flex; align-items: center; gap: 18px;
  padding: 16px 22px; border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: var(--radius); background: var(--surface);
}
.tier-number {
  font-family: var(--mono); font-size: 58px;
  font-weight: 500; line-height: 1; letter-spacing: -0.06em;
}
.tier-number.good { color: var(--success); }
.tier-number.info { color: var(--accent); }
.tier-number.warn { color: var(--warn); }
.tier-number.bad { color: var(--danger); }
.tier-side { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.tier-word {
  font-family: var(--font-head); font-size: 20px;
  font-weight: 600; letter-spacing: -0.02em;
}
.tier-desc { margin-top: 12px; max-width: 900px; }

/* ---- conic gauge around a headline number ---- */
.gauge {
  --score: 0;
  position: relative;
  display: grid;
  place-items: center;
  width: 148px;
  height: 148px;
  flex: 0 0 auto;
}
.gauge::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(
    var(--ring, var(--accent)) calc(var(--score) * 1%),
    var(--surface-3) 0
  );
  -webkit-mask: radial-gradient(circle, transparent 66%, #000 67%);
  mask: radial-gradient(circle, transparent 66%, #000 67%);
}
.gauge-label {
  position: absolute;
  bottom: 14px;
  font-family: var(--font);
  font-size: var(--fs-label);
  letter-spacing: 0.14em;
  
  color: var(--muted);
}

/* ---------- login ---------- */

.login-body {
  display: grid;
  place-items: center;
  min-height: 100vh;
  background:
    radial-gradient(900px 480px at 70% -10%, rgba(255, 80, 51, 0.14), transparent 60%),
    var(--bg);
}
.login-wrap {
  display: grid;
  justify-items: center;
  gap: 18px;
  width: min(360px, calc(100vw - 40px));
}
.login-wrap .brand { font-size: 15px; }
.login-panel { width: 100%; }
.login-panel .panel-body { display: grid; gap: 10px; padding: 26px 24px; }
.login-eyebrow {
  font-family: var(--font);
  font-size: var(--fs-label);
  letter-spacing: 0.22em;
  color: var(--accent);
}
.login-title {
  margin: 0 0 8px;
  font-family: var(--display);
  font-size: 26px;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
.login-label {
  font-family: var(--font);
  font-size: var(--fs-label);
  letter-spacing: 0.18em;
  
  color: var(--muted);
  margin-top: 6px;
}
.login-input {
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  font-family: var(--font);
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 0;
  outline: none;
}
.login-input:focus { border-color: var(--accent); }
.login-error {
  margin-top: 6px;
  padding: 8px 10px;
  font-size: var(--fs-body);
  color: var(--danger, #ff5033);
  border: 1px solid currentColor;
}
.login-submit {
  margin-top: 12px;
  padding: 11px 16px;
  font-family: var(--font);
  font-size: var(--fs-caption);
  letter-spacing: 0.16em;
  
  color: var(--bg);
  background: var(--accent);
  border: none;
  cursor: pointer;
}
.login-submit:hover { filter: brightness(1.08); }
.login-submit:disabled { opacity: 0.6; cursor: wait; }
.login-foot { font-size: 11px; text-align: center; }

/* nav auth button */
.nav-auth {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  font-family: var(--font);
  font-size: var(--fs-label);
  letter-spacing: 0.12em;
  
  color: var(--text-soft);
  border: 1px solid var(--line);
  background: transparent;
  cursor: pointer;
}
.nav-auth:hover { border-color: var(--accent); color: var(--accent); }

/* ---------- edge finder station cards ---------- */
.station-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px;
}
.station-card {
  display: grid;
  gap: 3px;
  padding: 10px 12px;
  background: var(--surface-1);
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--text);
}
.station-card:hover { border-color: var(--accent); }
.station-card.selected { border-color: var(--accent); background: var(--surface-2); }
.station-card .sc-code { font-family: var(--mono); font-weight: 700; font-size: var(--fs-body); }
.station-card .sc-note { font-size: 11px; color: var(--muted); line-height: 1.35; }
.station-card .sc-edge { font-family: var(--mono); font-size: 11px; }

/* ---------- demo: contract results (tight tables) ---------- */
.demo-tight table.data td { padding: 7px 10px; font-size: var(--fs-caption); vertical-align: top; }
.demo-tight table.data th { font-size: 11px; }
.demo-tight .cr-mark { font-weight: 800; margin-right: 6px; }
.demo-tight .cr-name { color: var(--text-soft); }
.demo-tight .cr-meta { font-size: 11px; color: var(--muted); margin-top: 2px; }
.demo-tight .table-scroll { max-height: 430px; overflow-y: auto; }



/* language switch — fixed bottom-left, floats above page content */
.lang-switch { position:fixed; bottom:16px; left:16px; z-index:80; display:inline-flex; gap:2px; border:1px solid var(--border); border-radius:999px; padding:2px; background:color-mix(in srgb, var(--surface) 85%, transparent); backdrop-filter:blur(8px); box-shadow:0 2px 10px rgba(0,0,0,.12); }
.lang-switch button { font:600 11px var(--mono); padding:4px 8px; border-radius:999px; border:0; background:transparent; color:var(--muted); cursor:pointer; }
.lang-switch button.active { background:var(--accent); color:#fff; }



/* ---------- site footer: brand, support, link row (built by common.js
   donateFoot; the slot div is filled in-place on pages that carry a
   footer, and a standalone box is appended on pages that do not). ---------- */
.site-foot {
  margin-top: 26px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-family: var(--font);
  font-size: 11px;
}
.site-foot-inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 26px 20px 40px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
@media (max-width: 640px) {
  .site-foot-inner { padding: 22px 12px 34px; }
}
.site-foot-inner > [data-donate-slot]:empty { display: none; }
.foot-brand {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 3px 14px;
}
.foot-brand strong {
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 600;
  letter-spacing: -0.01em;
}
.foot-brand span { color: var(--muted); }

/* support: one quiet note + one address chip (icon, address, copy) */
.foot-support {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
}
.support-note {
  margin: 0;
  color: var(--text-soft);
  line-height: 1.6;
}
.support-kicker {
  margin-right: 8px;
  color: var(--accent);
  font-family: var(--font);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: 0.12em;
  
}
.support-chip {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 4px 4px 4px 12px;
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: 999px;
  background: var(--surface);
  transition: border-color 0.15s ease;
}
.support-chip:hover { border-color: var(--border-strong); }
.support-sol { display: inline-flex; align-items: center; flex: 0 0 auto; }
.support-sol svg { display: block; }
.support-addr {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-soft);
}
.addr-short { display: none; }
.support-copy {
  appearance: none;
  border: 0;
  border-radius: 999px;
  padding: 5px 12px;
  background: var(--surface-2);
  color: var(--text-soft);
  font-family: var(--font);
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
  
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.support-copy:hover { background: var(--accent-soft); color: var(--accent); }
@media (max-width: 560px) {
  .addr-full { display: none; }
  .addr-short { display: inline; }
}

/* link row: mirrors the topbar nav's mono microtype */
.foot-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 24px;
}
.foot-nav a {
  color: var(--muted);
  font-family: var(--font);
  font-weight: 500;
  font-size: 11px;
  
  letter-spacing: 0.07em;
  white-space: nowrap;
  transition: color 0.15s ease;
}
.foot-nav a:hover { color: var(--text-soft); }
.foot-x { white-space: nowrap; }

/* Manual booking form on /live (operator-only page). */
.live-book-grid {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px 14px;
}
.live-book-grid label {
  display: flex; flex-direction: column; gap: 5px;
  font-family: var(--font); font-size: var(--fs-label); letter-spacing: 0.08em;
   color: var(--muted);
}
.live-book-grid label.wide { grid-column: span 2; }
.live-book-grid input {
  background: var(--surface-2); color: var(--text);
  border: 0; box-shadow: inset 0 0 0 0.5px var(--border); border-radius: var(--radius-sm);
  padding: 7px 10px; font-family: var(--font); font-size: var(--fs-caption);
}
.live-book-actions {
  display: flex; align-items: center; gap: 12px; margin-top: 14px;
  flex-wrap: wrap;
}
@media (max-width: 720px) {
  .live-book-grid { grid-template-columns: 1fr 1fr; }
  .live-book-grid label.wide { grid-column: span 2; }
}

/* ---------- mobile hardening (2026-09-10): small screens ---------- */
/* Additive only — everything below lives in max-width queries so desktop
   rendering is untouched. These rules sit at the end of the file so they
   win specificity ties. */

/* Scroll wrappers around wide data tables (all pages share the class). */
.table-scroll {
  overflow-x: auto;
  max-width: 100%;
  -webkit-overflow-scrolling: touch;
}

/* Code blocks: scroll sideways, never push the page out of the viewport. */
pre { overflow-x: auto; max-width: 100%; }

/* Mobile nav: the inline 9-link nav used to vanish entirely under 640px
   while no drawer markup exists on any page — phones lost all navigation.
   On small screens the topbar wraps and the nav rides a scrollable
   second row instead of disappearing. */
@media (max-width: 640px) {
  .topbar { flex-wrap: wrap; row-gap: 6px; }
  .brand { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Demo tabs render bare <table> straight into .panel-body (no wrapper).
     Contain them with a horizontal scroller instead of cropping the page. */
  .panel-body > table {
    display: block; overflow-x: auto; max-width: 100%;
    -webkit-overflow-scrolling: touch;
  }
  .panel-body > table td, .panel-body > table th { white-space: nowrap; }

  /* Tabs + filter chips: wrap their rows instead of running past the edge. */
  .tabs-bar { display: flex; flex-wrap: wrap; row-gap: 6px; }
  .sec-head .filters { display: flex; flex-wrap: wrap; gap: 8px; }

  /* Station search popover anchored to narrow inputs must not outgrow
     the viewport. */
  .picker-pop { max-width: calc(100vw - 32px); min-width: 0; }
}

/* One-column form controls below 560px (live/ops risk forms crowd in 2). */
@media (max-width: 560px) {
  .ctl-grid { grid-template-columns: 1fr; }
  .ctl-actions { flex-wrap: wrap; }
}

/* ==========================================================================
   GLIMM-STYLE RESKIN (2026-09-17)
   Aesthetic pass inspired by glimm.dev: quieter typography, larger radii,
   0.5px inset hairlines, neutral grays, accent reserved for state.

   Typographic discipline (the part that reads as "designed" rather than
   generated): ONE scale, ONE weight set, TWO tracking values.
     Scale:    11 / 12 / 13 / 14 / 17 / 22 / clamp(24-36)  — no half pixels
     Weights:  450 (reading), 550 (labels/titles), 600 (emphasis)
     Tracking: -0.01em for display sizes, 0 for everything else
   Inter loads as a variable font (wght 400..700) so 450/550 render as
   themselves; with static weights they silently snap to 400/600 and the
   page reads inconsistent.
   ========================================================================== */

/* --- radii: softer corners --- */
:root, [data-theme="light"] {
  --radius: 10px;
  --radius-sm: 8px;
}

/* --- borders: hairline insets instead of hard strokes --- */
.panel, .stat {
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
}
[data-theme="light"] .panel, [data-theme="light"] .stat {
  box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.07);
}
.topbar { border-bottom: 0; box-shadow: 0 0.5px 0 var(--border); }
.panel-head { border-bottom: 0.5px solid var(--border); }

/* --- display type: tight tracking, medium weight --- */
.hero h1 {
  font-size: clamp(24px, 3.2vw, 36px);
  font-weight: 550;
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.hero h1 span {
  font-size: 14px;
  font-weight: 450;
  letter-spacing: 0;
  margin-top: 8px;
  line-height: 1.55;
}
.sec-head h2 {
  font-size: 17px;
  font-weight: 550;
  letter-spacing: -0.01em;
}
.sec-head p { font-size: var(--fs-body); letter-spacing: 0; }
.panel-head h3 {
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--fs-body);
  font-weight: 550;
  font-family: var(--font);
  color: var(--text);
}
.hero-tag {
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 550;
  color: var(--muted);
}
.hero-tag::before {
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--success);
  animation: none;
}

/* --- nav: sentence case, quiet active state --- */
.nav a {
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 500;
  border-radius: 7px;
  padding: 6px 9px;
}
.nav a.active {
  color: var(--text);
  background: var(--surface-2);
}

/* --- controls --- */
.chip, .icon-btn {
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 500;
  border-radius: 8px;
}
.badge {
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 550;
  border-radius: 5px;
}

/* --- tables: calm headers, hairline rows --- */
table.data th {
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 550;
  color: var(--muted);
  border-bottom: 0.5px solid var(--border);
}
table.data td { border-bottom: 0.5px solid var(--border); }

/* --- neutral gray family --- */
/* These two tokens override the palette block's warm greys with cool ones.
   The selectors are identical in specificity, so this block wins by source
   order -- it is not a fallback, it is the value in force.

   The light --muted was #9ca3af (Tailwind gray-400), which measures 2.54:1 on
   the white card and 2.33:1 on the page.  Muted text at 10.5px needs 4.5:1, so
   every muted string in the light theme was failing -- including the rank-move
   dash on the where-to-look cards, which read as a missing pill rather than a
   faint one.

   #4b5563 is Tailwind gray-600: same cool family, 7.56:1 on white and 6.94:1
   on the page.  Measured, not estimated.  The dark value is untouched (6.45:1
   on the dark card, already fine). */
:root {
  --muted: #9a9a9d;
  --text-soft: #c6c6c9;
}
[data-theme="light"] {
  --muted: #4b5563;
  --text-soft: #525252;
}

/* --- consistency sweep ---
   The sheet predates the reskin and carries a scattered radius scale
   (1/2/3/4/5/6/7/8px) and 45 hard 1px strokes.  These few rules fold the
   interactive surfaces onto the new system so the page stops mixing two
   design languages: controls at 8px, panels at 10px, chips at 999px. */
.ctl, .picker-input, .picker-pop, .tabs-bar, .search,
.topbar-search input, .unit-toggle, .toc-rail a {
  border-radius: 8px;
}
.tabs-bar { border: 0; box-shadow: inset 0 0 0 0.5px var(--border); }
.tab { border-radius: 6px; }
.picker-input, .search, .topbar-search input, .unit-toggle {
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
}
[data-theme="light"] .picker-input, [data-theme="light"] .search,
[data-theme="light"] .topbar-search input, [data-theme="light"] .unit-toggle,
[data-theme="light"] .tabs-bar {
  box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.07);
}
.g-card, .ctl-group, .big-note, .recap-details,
.details-panel, .settled-details {
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: 10px;
}

/* --- light theme: paper-white world (#fff surface, near-black ink,
       #ECECEC hairlines, accent removed — glimm runs monochrome) --- */
[data-theme="light"] {
  --bg: #fbfbfb;
  --bg-soft: #f7f7f7;
  --surface: #ffffff;
  --surface-2: #f7f7f7;
  --surface-3: #f0f0f0;
  --border: #ececec;
  --border-strong: #dcdcdc;
  --text: #242529;
  --accent: #242529;
  --accent-2: #525252;
  --accent-soft: rgba(36, 37, 41, 0.06);
  --grid-line: transparent;
  --ambient-a: transparent;
  --ambient-b: transparent;
}
[data-theme="light"] body::before { display: none; }

/* --- Phase 2: sticky section rail (glimm's TOC pattern) --- */
/* Two-column layout: a quiet 168px rail of section links that sticks
   while the long scoreboard scrolls past.  Collapses to a horizontal
   scroll strip under 1100px so the data column keeps full width. */
.toc-layout {
  display: grid;
  grid-template-columns: 168px minmax(0, 1fr);
  gap: 28px;
  align-items: start;
  margin-top: 44px;
}
.toc-rail {
  position: sticky;
  top: calc(var(--topbar-h) + 18px);
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--fs-body);
  max-height: calc(100vh - var(--topbar-h) - 40px);
  overflow-y: auto;
}
.toc-rail .toc-title {
  font-size: 11px;
  font-weight: 550;
  color: var(--muted);
  margin-bottom: 7px;
  letter-spacing: 0;
}
.toc-rail a {
  color: var(--muted);
  padding: 4px 9px;
  border-left: 1.5px solid var(--border);
  line-height: 1.35;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.toc-rail a:hover { color: var(--text); }
.toc-rail a.active {
  color: var(--text);
  border-left-color: var(--accent);
  font-weight: 550;
}
.toc-body .section:first-child { margin-top: 0; }
@media (max-width: 1100px) {
  .toc-layout { grid-template-columns: 1fr; gap: 0; }
  .toc-rail {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 4px;
    max-height: none;
    overflow: visible;
    margin-bottom: 6px;
  }
  .toc-rail .toc-title { display: none; }
  .toc-rail a { border-left: 0; border: 0.5px solid var(--border); border-radius: 999px; padding: 3px 10px; font-size: var(--fs-caption); }
  .toc-rail a.active { border-color: var(--border-strong); background: var(--surface-2); }
}

/* ---------- layout rhythm pass (2026-09-17) ---------- */
/* On phones the 44px inter-section gap + 18px panel padding + 26px hero
   padding stacked into a lot of dead vertical space, pushing real content
   below the fold.  Tighten the vertical rhythm on small screens only;
   desktop spacing is untouched. */
@media (max-width: 640px) {
  .section { margin-top: 26px; }
  .section.tight { margin-top: 10px; }
  .panel-body { padding: 12px; }
  .panel-head { padding: 10px 12px; }
  .hero { padding: 18px 16px 20px; gap: 12px; }
  .hero h1 { font-size: clamp(24px, 7.4vw, 34px); }
  .hero h1 span { font-size: 14px; margin-top: 8px; line-height: 1.5; }
  .sec-head h2 { font-size: 17px; }
  .sec-head p { font-size: var(--fs-body); line-height: 1.55; }
  .stat-grid { gap: 8px; }
  .stat { padding: 10px 11px; }
  /* Wide tables keep their horizontal scroll but lose some cell padding
     so more columns fit before the swipe. */
  table.data th, table.data td { padding: 7px 8px; }
}

/* Medium screens: the 4-up stat rows get cramped between 641-860px;
   let them settle to two columns instead of squeezing four. */
@media (min-width: 641px) and (max-width: 860px) {
  .stat-grid { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
}

/* ==========================================================================
   PHASE 3 — motion, numbers, chart chrome, states
   ========================================================================== */

/* --- motion: two durations, one curve --- */
:root {
  --ease: cubic-bezier(0.23, 1, 0.32, 1);
  --t-fast: 120ms;   /* hover, focus, color */
  --t-base: 180ms;   /* surfaces, transforms, reveals */
}
.panel, .stat, .chip, .icon-btn, .nav a, .tab, .toc-rail a,
.compare-bar, .station-sticky, input, select, .badge {
  transition-duration: var(--t-fast);
  transition-timing-function: var(--ease);
}
.panel, .stat, .compare-bar, .station-sticky {
  transition-duration: var(--t-base);
}

/* --- numbers: tabular everywhere a value can change --- */
table.data .num, .nums, .stat .value, .stat .sub,
.kv dd, .mono, code, kbd, samp {
  font-variant-numeric: tabular-nums;
}

/* --- chart chrome: quieter gridlines, calmer labels --- */
.chart-svg text {
  font-family: var(--font);
  font-size: var(--fs-label);
  letter-spacing: 0;
}
.chart-svg line[stroke="var(--border)"] { stroke-width: 0.5; }
.chart-tip {
  border-radius: 8px;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border), var(--shadow-sm);
  font-size: var(--fs-caption);
  padding: 7px 10px;
}

/* --- focus ring: legible in both themes --- */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
[data-theme="light"] :focus-visible { outline-color: #242529; }

/* --- empty / error states: calm, not alarming --- */
.empty-state, .error-state {
  color: var(--muted);
  font-size: var(--fs-body);
  line-height: 1.55;
  padding: 16px;
  text-align: center;
  /* keep the dashed "placeholder" signal, at hairline weight */
  border: 0;
  box-shadow: none;
  outline: 0.5px dashed var(--border-strong);
  outline-offset: -0.5px;
}
.error-state {
  color: var(--danger);
  outline-color: color-mix(in srgb, var(--danger) 35%, var(--border));
}

/* --- page-load fade: 150ms, no layout shift, motion-safe --- */
@keyframes page-in { from { opacity: 0; } to { opacity: 1; } }
main { animation: page-in var(--t-base) var(--ease) both; }
@media (prefers-reduced-motion: reduce) {
  main { animation: none; }
}

/* ==========================================================================
   PHASE 4 — the last mile: every remaining uppercase/mono label, the
   predict + login pages, the operator pages, and the edge cases.  The
   goal is one design language everywhere: sentence-case Inter labels,
   system weights, hairline borders.
   ========================================================================== */

/* --- sweep: the last uppercase mono labels, folded onto the system --- */
.eyebrow,
.station-pop-label,
.settled-details > summary,
.ctl-group-head > span:first-child,
.ctl-lock,
.status-pill,
.back-link,
.picker-flag,
.recap-details summary,
.gauge-label,
.login-label,
.login-submit,
.nav-auth,
.support-kicker,
.support-copy,
.foot .warn-line,
.foot-nav a,
.live-book-grid label {
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font);
  font-weight: 500;
}
.eyebrow { font-size: var(--fs-caption); font-weight: 550; color: var(--muted); }
.station-pop-label { font-size: 11px; font-weight: 550; }
.settled-details > summary,
.recap-details summary { font-size: var(--fs-body); font-weight: 500; }
.ctl-group-head > span:first-child { font-size: var(--fs-body); font-weight: 550; }
.ctl-lock { font-size: 11px; font-weight: 500; }
.status-pill { font-size: var(--fs-caption); font-weight: 500; }
.back-link { font-size: var(--fs-caption); font-weight: 500; }
.picker-flag { font-size: 11px; font-weight: 550; }
.gauge-label { font-size: 11px; font-weight: 550; }
.login-label { font-size: var(--fs-caption); font-weight: 550; }
.login-submit { font-size: 14px; font-weight: 550; }
.nav-auth { font-size: var(--fs-body); font-weight: 500; }
.support-kicker { font-size: 11px; font-weight: 550; }
.support-copy { font-size: var(--fs-body); font-weight: 450; }
.foot .warn-line { font-size: 11px; font-weight: 500; }
.foot-nav a { font-size: var(--fs-caption); font-weight: 500; }
.live-book-grid label { font-size: var(--fs-caption); font-weight: 550; }

/* --- skeleton: same motion system as everything else --- */
.skeleton {
  border-radius: var(--radius);
  background: linear-gradient(100deg,
    var(--surface-2) 40%,
    var(--surface-3) 50%,
    var(--surface-2) 60%);
  background-size: 200% 100%;
  animation: shimmer 1.6s var(--ease) infinite;
}
@keyframes shimmer {
  0% { background-position: 120% 0; }
  100% { background-position: -80% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; background: var(--surface-2); }
}

/* --- predict page: the .pc-* family joins the system --- */
.pc-step b {
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 550;
  letter-spacing: 0;
  text-transform: none;
}
.pc-step i { font-style: normal; }
.pc-call-deg { font-weight: 550; letter-spacing: -0.01em; }
.pc-scale .lo, .pc-scale .mid, .pc-scale .hi {
  text-transform: none;
  letter-spacing: 0;
  font-size: 11px;
  font-weight: 500;
}
.pc-cta-note { font-size: var(--fs-caption); line-height: 1.5; }
.pc-msg { font-size: var(--fs-body); }
.pc-mini { font-size: var(--fs-caption); font-weight: 500; }

/* --- login page: same treatment --- */
.login-title { font-weight: 550; letter-spacing: -0.01em; }
.login-card { border-radius: var(--radius); }
.login-input {
  border-radius: 8px;
  font-size: 14px;
}
/* The form predates the reskin: mono uppercase labels, square inputs and
   a letter-spaced all-caps button.  Fold it onto the system. */
.login-eyebrow {
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 550;
  letter-spacing: 0;
  text-transform: none;
  color: var(--muted);
}
.login-label {
  font-family: var(--font);
  font-size: var(--fs-caption);
  font-weight: 550;
  letter-spacing: 0;
  text-transform: none;
}
.login-input {
  font-family: var(--font);
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: 8px;
}
.login-input:focus { box-shadow: inset 0 0 0 1px var(--border-strong); }
.login-error {
  border: 0;
  box-shadow: inset 0 0 0 0.5px currentColor;
  border-radius: 8px;
  font-size: var(--fs-body);
}
.login-submit {
  font-family: var(--font);
  font-size: 14px;
  font-weight: 550;
  letter-spacing: 0;
  text-transform: none;
  border-radius: 8px;
  transition: filter var(--t-fast) var(--ease);
}
.login-foot { font-size: var(--fs-caption); }

/* --- operator pages (ops / delivery / feed): quieter chrome --- */
.ops-row, .feed-row { border-bottom: 0.5px solid var(--border); }
.health-chip, .status-chip {
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--fs-caption);
  font-weight: 500;
}

/* --- predicted vs. observed card (station page) ---
   The header carries a title, a 4-chip checkpoint switcher and a
   right-hand caption; at narrow widths those three fought for one row
   and the chips spilled past the panel edge.  Order on desktop:
   title · chips · caption (pushed right).  Below 720px the chips drop
   to their own full-width line. */
.cmp-head { flex-wrap: wrap; row-gap: 8px; }
.cmp-head h3 { flex: 0 0 auto; order: 1; }
.cmp-chips { display: flex; gap: 6px; flex-wrap: wrap; order: 3; flex: 1 1 100%; }
.cmp-head .right { order: 2; margin-left: 0; }
@media (min-width: 720px) {
  .cmp-chips { order: 2; flex: 0 0 auto; }
  .cmp-head .right { order: 3; margin-left: auto; }
}
.cmp-stats { margin-bottom: 0; }
.cmp-stats .stat { padding: 12px 14px; }
.cmp-gap-ok { color: var(--success); font-weight: 550; }
.cmp-gap-off { color: var(--warn); font-weight: 550; }

/* ==========================================================================
   DASHBOARD REDESIGN (2026-09-17)
   The home page was a stack of boxes: hero card → pulse box → verdict
   box → two more status boxes.  Five framed rectangles before the first
   heading, and three of them restated each other.  The new shape reads
   top-down like a document:

     masthead      typography + a hairline rule, no box
     status line   one quiet row of facts, no box, no pills
     verdict strip one instrument, three numbers
     content       sections with headings (recap, ledger, leaderboard)

   Boxes now appear only where they hold data you act on.
   ========================================================================== */

/* --- masthead: a status bar, not a hero ---
   Measured before this change: 120px tall, and 439px of chrome preceded the
   first forecast number on a 900px screen (49%), 735px on a 375px phone (91%).
   A 40px H1 restating the product name is the largest single contributor, and
   it carries no information -- the name is in the tab title and the topbar.
   The row is now one baseline-aligned line that states live state instead. */
.masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  padding: 0 0 12px;
  border-bottom: 0.5px solid var(--border);
}
.masthead-main {
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 10px;
  /* NOWRAP, and the name must not grow.  With wrap, the h1 (a block-level
     flex item) claimed the full 359px row on a phone and pushed the note onto
     a second line -- measured 76px of masthead for one line of text. */
  flex-wrap: nowrap;
  flex: 1 1 auto;
}
.masthead-name { flex: 0 0 auto; }
.masthead-name {
  margin: 0;
  font-family: var(--font-head);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
  color: var(--text);
}
.masthead-note {
  font-size: 12.5px;
  color: var(--muted);
  line-height: 1.2;
}
.masthead-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

#volume-line {
  margin-top: 10px;
  font-size: var(--fs-caption);
  padding-left: 2px;
}
/* On a narrow screen the masthead stacks: the name and note on one line, the
   controls below.  It does NOT grow the name -- this rule used to re-inflate
   the h1 to 30px, which was right when the masthead was a 40px hero and is
   exactly backwards now that it is a status bar.  Measured on the phone
   viewport: it was forcing 30px and costing 139px of the first screen. */
@media (max-width: 640px) {
  .masthead { align-items: center; }
  /* Controls wrap to their own line rather than competing with the name for
     one -- but the row itself stays one line tall. */
  .masthead-actions { width: 100%; justify-content: flex-start; }
  .masthead-note { display: none; }
}

/* ==========================================================================
   DASHBOARD MOSAIC (2026-09-17)
   The dashboard is a 12-column grid, not a stack of full-width sections.
   The day's result owns the largest cell; the verdict numbers ride a
   right rail; the working surfaces (ledger, leaderboard) span full
   width.  Cells share one hairline grid instead of each drawing a box.
   ========================================================================== */

.dash-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0;
  margin-top: 16px;
  border-top: 0.5px solid var(--border);
}
.dash-cell {
  padding: 16px 0;
  border-bottom: 0.5px solid var(--border);
  min-width: 0;
}
/* Vertical rules between side-by-side cells. */
.dash-hero { grid-column: 1 / span 7; padding-right: 24px; border-right: 0.5px solid var(--border); }
.dash-verdicts { grid-column: 8 / span 5; padding-left: 24px; }
.dash-status { grid-column: 1 / span 12; padding-block: 10px; }
.dash-scorecards { grid-column: 1 / span 12; }

/* --- the fold has a weight gradient ---
   Every cell used to share one padding and one hairline, so nothing led and
   nothing receded -- the eye had no entry point.  Three weights now:
     .dash-pickability  the answer, closest to the top, most room
     default cells      evidence
     .dash-meta         the audit trail, compressed and visibly secondary
   Nothing is hidden; the order and the space just say what matters.

   `.dash-ledger` was the top weight here until the "Today's forecasts" cell
   was removed; its rules went with the cell rather than being left inert, so
   a future edit cannot mistake them for live styling. */
/* Where to look: the visitor's second question, answered FIRST.
   The board leads the dashboard because choosing an airport is a decision made
   BEFORE reading any forecast, not an audit of one.

   Cards, not table rows: a table says "compare these 37 rows on shared
   columns", while the visitor is choosing ONE airport.  Each card carries the
   same facts in the same places, so the eye compares across cards without a
   column to line them up.

   What keeps the cards small is that everything repeated is stated ONCE: the
   two labels live in the legend, the category lives in the tab, and the
   explanation lives in the tooltip.  On a filtered group every card would
   otherwise repeat the same two labels and the same badge. */
.dash-pickability {
  grid-column: 1 / span 12;
  padding-top: 16px;
  /* More room before "Today's forecasts" starts.

     The two cells are adjacent and each carries its own 16px padding, so the
     visual break was only 31px -- measured in a browser -- while the board is a
     dense grid of cards.  The extra 16px separates the answer to "which airport"
     from the answer to "what is today's high", which are different questions. */
  padding-bottom: 32px;
}
.dash-pickability .cell-head h2 { font-size: 17px; }
.dash-pickability .cell-head .filters { margin-left: auto; }

/* The group tabs.  Each carries its own count, so the tab row is also the
   summary and no separate row of stat cards is needed. */
.pk-filters { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.pk-filter {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--font); font-size: 12.5px; font-weight: 500;
  color: var(--muted); background: var(--surface-2);
  box-shadow: inset 0 0 0 0.5px var(--border); border: 0;
  border-radius: 999px; padding: 5px 12px; cursor: pointer;
  transition: color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.pk-filter:hover { color: var(--text-soft); box-shadow: inset 0 0 0 1px var(--border-strong); }
.pk-filter.active { color: var(--text); background: var(--surface-3); box-shadow: inset 0 0 0 1px var(--border-strong); }
.pk-count {
  font-size: 11px; color: var(--muted);
  background: var(--surface); border-radius: 999px; padding: 1px 7px;
}
.pk-filter.active .pk-count { color: var(--text-soft); }

/* One line saying what the two numbers mean, replacing a sentence on every
   card -- which is where most of the word count was. */
.pk-legend {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  font-size: var(--fs-caption); line-height: 1.5; margin-bottom: 12px;
}
.pk-legend b { color: var(--text-soft); font-weight: 600; }
/* A separator between two legend phrases.  It should be quiet -- it must not
   compete with the words it divides -- but it used --border-strong, which at
   1.99:1 (dark) and 1.75:1 (light) is not quiet, it is absent, so the phrases
   ran together.  --muted is the quiet-but-legible token: 6.45:1 dark, 7.56:1
   light. */
.pk-key-sep { color: var(--muted); margin: 0 2px; }

/* Cards, centred, with ONE fixed width.

   Was a grid of fractional columns, which stretched every card to fill the
   row.  That leaves a ragged hole whenever a group does not divide evenly:
   measured at 1400px it fits 6 per row, so "Easy pick" (7 cards) broke as
   6 + 1 and left five empty slots on the second row -- and "Forecast helps"
   (21) ended on a lone card with four empty beside it.

   Flex with a FIXED basis, not a grid with a fractional one:
     * every card is the same width, so nothing changes size between rows
       (letting short rows grow is worse than the hole -- a lone card would
       stretch to 1364px, and a 3-card row to 454px each);
     * `justify-content: center` centres whatever lands on the last row, so a
       short row reads as a deliberate centred group rather than as a row that
       ran out.
   210px fits 6 per row at 1400px and 4 at 1024px. */
.pk-cards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  /* 20px between cards, up from the 12px this started at.

     The gap is not free: five cards plus four gaps must fit the container, and
     the container caps at 1140px on a wide screen and only shrinks below a
     ~1200px viewport -- so the constraint bites in a narrow band, not at the
     extremes.  Five 200px cards need 1000px plus 4 gaps, so:

         container 1082px (viewport 1140) -> max gap 20px
         container 1102px (viewport 1160) -> max gap 25px
         container 1140px (wide)          -> max gap 35px

     20px is set because 1082px is the tightest container the layout has to
     survive at five per row, and that is ALREADY where the old 210px/12px
     layout dropped to four.  So this is more air and a wider usable range at
     once, not a trade. */
  gap: 20px;
}
.pk-card {
  /* 200px, down from 210px, and the 10px goes to the GAP.

     The card width and the gap trade off directly: five cards plus four gaps
     must fit the container, so every pixel taken off a card is a pixel added to
     every gap.  Narrowing the card is therefore the only way to widen the gaps
     without losing a card per row -- which is what the previous attempt failed
     to do, since it left the cards at 210px and could only reach 12px.

     THE FLOOR IS THE CONTENT, and it is measured against the REAL font.  The
     longest action label the board produces is "Say the usual degree" at 170px
     in Inter, plus 14px padding each side = 198px.  So 200px leaves 2px.

     That number is 8px larger than the first estimate, because the first
     measurement ran before the web font had loaded and read a fallback face
     (162px instead of 170px).  A probe that does not wait for `fonts.ready`
     under-measures every string on the page -- the same trap that once shipped
     a wrapped evidence line.  Anything narrower than 198px overflows this label
     by 2px, and 196px was tried and reverted for exactly that reason.

     It is also strictly BETTER than 210px, not a trade: at a 1140px viewport
     (container 1082px) five 210px cards need 1098px and drop to four per row,
     while five 200px cards with a 20px gap need 1080px and still fit five. */
  flex: 0 0 200px;
  /* `min-width: 0` is load-bearing, not tidiness.

     A flex item defaults to `min-width: auto`, which refuses to shrink below
     its content's min-content width -- so a long airport name ("Manila Ninoy
     Aquino International") pushed the card WIDER than its 210px basis instead
     of letting the name truncate.  Measured: those cards came out 268px,
     248px and 240px beside 210px neighbours, so the board was ragged and the
     `text-overflow: ellipsis` on the name never fired (there was nothing to
     overflow -- the box had grown).  Short placeholder names hide this
     completely, which is why it has to be measured with REAL names. */
  min-width: 0;
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px 14px 13px;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: inset 0 0 0 0.5px var(--border);
  text-decoration: none; color: inherit;
  transition: box-shadow var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.pk-card:hover { box-shadow: inset 0 0 0 1px var(--border-strong); transform: translateY(-1px); }
.pk-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pk-card-top { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.pk-card-code { font-family: var(--mono); font-size: 18px; font-weight: 700; letter-spacing: -0.01em; flex: none; }
.pk-card-name {
  font-size: var(--fs-caption); line-height: 1.35;
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The ACTION, and the reason this card is finally legible.

   Three attempts got here, and each failure is worth recording because the
   final rule is not obvious:

     1. two bare percentages -- a rate with no reference reads as a grade, and
        "57%?" looks mediocre;
     2. "+N pts vs usual degree" -- promoted to headline, but the operator could
        not say what it meant.  It is arithmetic: `edge` is exactly
        `forecast - usual` (measured: max |(A - B) - edge| = 0.000000000000), so
        the card printed A, B and A-minus-B.  It was also read as an EASINESS
        score, because more points looks like more good, when in fact points RISE
        as the weather gets harder;
     3. the action -- what to actually do about this airport.

   So the headline is now a DECISION, not a measurement.  It needs no
   interpretation, and it is the same word as the tab the card sits under, which
   is what makes one card readable on its own.

   Colour carries the tone and is the only signal, so the label stays one line:
   green follow it, blue the usual degree beats it, purple follow it but there
   is no fallback, amber neither works. */
.pk-card-action {
  font-size: 17px; font-weight: 700; letter-spacing: -0.01em;
  line-height: 1.3; white-space: nowrap; margin: 1px 0 3px;
}
.pk-card-action[data-tone="success"] { color: var(--success); }
.pk-card-action[data-tone="info"] { color: var(--accent); }
.pk-card-action[data-tone="accent"] { color: var(--accent-2); }
.pk-card-action[data-tone="warn"] { color: var(--warn); }

/* The evidence, under the action: the two rates and their gap as prose.

   Not three labelled columns -- that is the shared-table layout the card was
   chosen to replace.  The whole line must hold ONE line, or the card grows and
   the board stops being a uniform grid.

   The size is 11.5px, and that is a MEASUREMENT rather than a preference.  The
   longest evidence the board produces measures, with `white-space: nowrap`:

       12.5px -> 189px   (the old value: WRAPPED on every card, +18px height)
       12px   -> 181px   (fits by 1px -- too tight to be safe)
       11.5px -> 174px   (8px of slack)
       11px   -> 166px   (16px of slack, but visibly smaller than the name)

   The first attempt at this shipped 12.5px because the measuring probe used
   `Range.getBoundingClientRect().width`, which returns the bounding box of ALL
   line rects -- for wrapped text that is the WIDEST LINE, not the width the
   text needs, so a line that had already wrapped reported as "fits".  The only
   visible symptom was 18px of extra card height.  Any future measurement here
   must clone with `nowrap` and read the box, which is what the numbers above
   are. */
.pk-card-evidence {
  font-size: 11.5px; color: var(--muted); line-height: 1.4;
  font-variant-numeric: tabular-nums;
}

/* ---------- the star rating ----------

   The confidence scale, and the reason the card changed again: a reader asked
   for something simple enough to act on without interpreting a metric, and a
   five-star scale is that.  Each star is one multiple of blind guessing, so the
   rating is checkable rather than decorative -- see STAR_BANDS in
   web/data/pickability.py for the bands and the measured distribution.

   Colour follows the count so the scale reads across a whole row at a glance,
   and the FILLED/EMPTY pair is drawn rather than the number printed: "★★★★☆"
   says four at a glance, where "4" would need a legend. */
.pk-card-rating { margin: 2px 0 1px; }
.pk-stars {
  font-size: 15px; letter-spacing: 1px; line-height: 1.2;
  white-space: nowrap;
}
.pk-stars[data-stars="5"] { color: var(--success); }
.pk-stars[data-stars="4"] { color: var(--accent-2); }
.pk-stars[data-stars="3"] { color: var(--accent); }
.pk-stars[data-stars="2"] { color: var(--warn); }
.pk-stars[data-stars="1"] { color: var(--danger); }

/* The sentence that makes the stars mean something: WHICH checkpoint the
   rating comes from, and the win rate there.  The checkpoint differs per
   station -- day start for a few, noon for most -- so it is named per card
   rather than once in a legend. */
.pk-best {
  font-size: 11.5px; line-height: 1.35; margin-bottom: 3px;
  color: var(--text);
}
.pk-best b { font-weight: 700; }

/* What the engine reaches by 4pm.  Deliberately quieter than everything above
   it, because it is context rather than the rating -- it is only here so a
   reader does not read a 2-star card, see "93%", and conclude the stars are
   broken. */
.pk-settled { font-size: 11px; line-height: 1.35; margin-top: 3px; }

/* Rank movement since the previous update.  The board recomputes as days
   settle and the ranks genuinely move, but a number that changes without
   saying so looks frozen.

   The "unchanged" state used --border-strong, which is a BORDER token: a
   border only has to read as an edge, while this is text.  Measured in a real
   browser against the card's painted background rgb(22,22,22):

       up    rgb(109,211,154)  ratio  9.85   good
       down  rgb(255, 77,106)  ratio  5.62   good
       flat  rgb( 74, 72, 70)  ratio  1.99   INVISIBLE

   So a card showed either a legible triangle or a dash that could not be
   seen, which reads as "the pill is missing on some cards".  --muted is a
   real text token and measures 7.4:1 here. */
.pk-move {
  margin-left: auto; font-size: 10.5px; font-weight: 700;
  white-space: nowrap; flex: none;
}
.pk-move.up { color: var(--success); }
.pk-move.down { color: var(--danger); }
.pk-move.flat { color: var(--muted); font-weight: 500; }

/* WHEN the numbers describe.  The board recomputes as days settle and the end
   date advances, but nothing said so -- and a ranking that silently changes
   reads as either stale or arbitrary. */
.pk-fresh {
  margin: 0 0 8px; font-size: 11.5px; line-height: 1.4;
}
.pk-fresh b { color: var(--text); font-weight: 700; }

/* A window change takes ~1.8s to recompute.  Without a visible state the
   board looked like it had ignored the click, which is why the reported
   symptom was "I have to click 20d twice".  The cards stay on screen (dimmed
   rather than replaced by skeletons) so the board does not flash empty while
   the reader is looking at it. */
.dash-pickability.is-loading #pickability-panel {
  opacity: 0.55;
  transition: opacity 0.15s var(--ease);
}
.dash-pickability.is-loading #pickability-window .chip.active::after {
  content: "";
  display: inline-block;
  width: 8px; height: 8px;
  margin-left: 6px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  animation: pk-spin 0.7s linear infinite;
  vertical-align: -1px;
}
@keyframes pk-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .dash-pickability.is-loading #pickability-window .chip.active::after {
    animation: none;
    border-top-color: currentColor;
    opacity: 0.5;
  }
}

/* The star tabs.  The stars are the label, so they need the size of a control
   rather than of body text, and the count sits beside them as on the other
   tabs. */
.pk-filter-stars { letter-spacing: 0.5px; }
.pk-filter-stars[data-stars="5"].active { color: var(--success); }
.pk-filter-stars[data-stars="4"].active { color: var(--accent-2); }
.pk-filter-stars[data-stars="3"].active { color: var(--accent); }
.pk-filter-stars[data-stars="2"].active { color: var(--warn); }
.pk-filter-stars[data-stars="1"].active { color: var(--danger); }

/* The intention, stated once above the board.

   The board answers a QUESTION -- "is the forecast worth following here?" --
   and nothing on the page said so.  The operator, who built it, could not name
   the intention, which is what a missing sentence looks like from the outside.
   One line, before any number, costs ~15 words and removes the guesswork. */
.pk-intro { margin: 0 0 10px; font-size: var(--fs-body); max-width: 68ch; }

/* The two-rate columns are GONE, along with their rules.  They were the
   evidence presented as two competing numbers, and the gap between them is
   what the action label already summarises ("Use the forecast" IS "the forecast
   rate won").  The same two values still appear, as one line of prose under the
   action, so nothing is hidden -- the redundant PRESENTATION was removed, and
   dead CSS would tell the next reader those classes were still live. */

@media (max-width: 560px) {
  /* One per row, and full width: a 196px card leaves a wide margin on a 360px
     phone, and the action label plus its evidence line read better with the
     room. */
  .pk-card { flex: 1 1 100%; }
}


/* ---------- the regime card, set as prose ----------
   This panel used to carry four font sizes (17 / 14 / 12.5 / 11.5) inside one
   box, with the paragraphs running the full width of the panel.  At 1400px
   that is a ~150-character line, roughly twice a comfortable measure, and it
   is the specific text that read as dizzying.

   Now: one heading size, one body size, one caption size, and every paragraph
   capped at 68ch -- the measure where the eye can find the next line without
   a conscious search. */
.regime-head { display: flex; align-items: center; gap: 14px; }
.regime-icon { color: var(--accent); flex: none; }
.regime-title { font-size: var(--fs-lead); font-weight: 600; line-height: 1.35; }
.regime-blurb { font-size: var(--fs-caption); line-height: 1.5; margin-top: 2px; }
.regime-headline {
  font-size: var(--fs-body); line-height: var(--lh-body);
  margin: 16px 0 0; max-width: 68ch;
}
.regime-note {
  font-size: var(--fs-caption); line-height: 1.6;
  margin: 8px 0 0; max-width: 68ch;
}
.regime-foot {
  font-size: var(--fs-caption); line-height: 1.6;
  margin: 12px 0 0; max-width: 68ch;
  padding-top: 10px; border-top: 0.5px solid var(--border);
}

/* ---------- prose measure, site-wide ----------
   Any paragraph long enough to be read as sentences gets a cap.  Without one,
   a paragraph in a full-width panel runs to 150+ characters and the reader
   loses the line on the return sweep. */
.panel-body > p,
.panel-body > div > p,
.regime-headline,
.regime-note,
.regime-foot { max-width: 68ch; }

/* Body copy inside panels: one size, generous leading. */
.panel-body { font-size: var(--fs-body); line-height: var(--lh-body); }
.panel-body p { line-height: var(--lh-body); }
.panel-body p + p { margin-top: 10px; }

/* Tables and dense data stay at the tighter size -- they are scanned, not
   read, and loosening them would push the columns off screen. */
.panel-body table { font-size: var(--fs-caption); line-height: var(--lh-tight); }

/* ==========================================================================
   STATION SUMMARY (2026-09-26)
   The station page carried 1407 words, 7 panels, 9 stat cards, 2 tables and
   7 charts before its answer.  This block IS the answer, and everything else
   is folded below it as evidence.

   The one big number, at 64px: it is now the only figure on the screen, so it
   carries the page.  What sits beside it is what the site is FOR rather than
   decoration -- the 50% band (a probability, not a plain range), the observed
   high, and the plain-English bias sentence.
   ========================================================================== */
.sum-panel { border-left: 3px solid var(--accent); }

/* The star rating, at the top of the station summary box.

   It answers the same question the dashboard board does, from the same data, so
   the two surfaces agree by construction rather than by convention.

   The stars sit on their own row ABOVE the "Today - Sep 25 - noon checkpoint"
   line, because that line is about TODAY's capture and the rating is about the
   station's whole record.  Stacking them on one line would put two different
   time frames in one sentence.

   Colour follows the count, the same scale as the dashboard cards, so a reader
   who has seen the board reads this instantly.  The note beside it carries the
   evidence: which checkpoint the rating comes from, and the rate. */
.sum-rating {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  margin-bottom: 8px; padding-bottom: 8px;
  border-bottom: 0.5px solid var(--border);
}
.sum-rating-stars {
  font-size: 19px; letter-spacing: 1.5px; line-height: 1.1;
  white-space: nowrap; flex: none;
}
.sum-rating[data-stars="5"] .sum-rating-stars { color: var(--success); }
.sum-rating[data-stars="4"] .sum-rating-stars { color: var(--accent-2); }
.sum-rating[data-stars="3"] .sum-rating-stars { color: var(--accent); }
.sum-rating[data-stars="2"] .sum-rating-stars { color: var(--warn); }
.sum-rating[data-stars="1"] .sum-rating-stars { color: var(--danger); }
/* The unrated state needs its OWN colour.  Without a rule it inherits the body
   colour, which in the light theme is near-white -- measured at rgb(250,248,247)
   on the live page, so the hollow stars were effectively invisible.  Muted grey
   reads as "no rating here" rather than as a rating of zero.

   It said "muted grey" but used --border-strong, which is a BORDER token: a
   border only has to read as an edge.  Measured on the card background that is
   1.99:1 in dark and 1.75:1 in light, so the hollow stars were invisible in
   BOTH themes -- the same defect the comment was written to fix, one token
   over.  --muted is the token the comment actually describes. */
.sum-rating[data-stars="0"] .sum-rating-stars { color: var(--muted); }
.sum-rating-note { font-size: 12.5px; color: var(--muted); line-height: 1.35; }

/* The two honesty flags under the summary number.

   "the day is done - this is the outcome, not a forecast" appears when the
   capture came after the peak, which on most stations is every H16 capture.
   Without it the box reads "32.0C forecast high / observed so far 32.0C" and a
   visitor concludes the forecast was perfect, when the engine had simply
   converged on the answer.

   The stale note appears when the newest capture is from another local day, so
   "Yesterday" reads as the honest state of the ledger rather than as a bug.

   Muted and small: these are qualifications on the number above, not headlines
   competing with it.  The settled note carries a hairline rule so it separates
   the number from the evidence below. */
.sum-flags {
  display: flex; flex-wrap: wrap; gap: 4px 12px;
  margin-top: 10px; padding-top: 8px;
  border-top: 0.5px solid var(--border);
  font-size: 11.5px; line-height: 1.4;
}
.sum-settled { color: var(--warn); }
.sum-stale { color: var(--muted); }

@media (max-width: 560px) {
  /* The note is a sentence beside a glyph row; at phone width it would squeeze
     the stars, so it drops to its own line instead. */
  .sum-rating-note { flex-basis: 100%; }
}
.sum-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px;
  font-size: 12.5px; color: var(--muted); margin-bottom: 10px;
}
.sum-head b { color: var(--text); font-weight: 600; }
.sum-body { display: flex; flex-wrap: wrap; align-items: center; gap: 20px 32px; }
.sum-number { min-width: 0; }
.sum-value {
  font-family: var(--font-head); font-size: clamp(48px, 11vw, 64px);
  font-weight: 700; line-height: 1; letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums;
}
.sum-value-label { font-size: var(--fs-caption); margin-top: 4px; }
.sum-side { display: flex; flex-direction: column; gap: 6px; font-size: var(--fs-body); }
.sum-band b { font-size: 16px; font-weight: 650; }
.sum-obs { color: var(--text-soft); }
.sum-conf { font-size: 12.5px; color: var(--muted); }

/* The bias headline: the page's only sentence that says what to DO with the
   number, so it is not folded away with the rest of the commentary. */
.sum-bias {
  margin-top: 14px; padding: 10px 12px; border-radius: var(--radius);
  background: var(--surface-2); font-size: var(--fs-body); line-height: 1.55;
}
.sum-bias:empty { display: none; }
.sum-bias-tag {
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); margin-right: 6px;
}

.sum-record {
  margin-top: 12px; padding-top: 10px; border-top: 0.5px solid var(--border);
  font-size: 12.5px; line-height: 1.6;
}
.sum-record b { color: var(--text); font-weight: 650; }
.sum-market { margin-top: 12px; }

/* ---------- folds ----------
   Every evidence section is a <details>.  The summary row is the heading, so
   the page still reads as a document rather than as a stack of collapsed
   boxes, and the closed state costs one line each. */
details.fold {
  margin-top: var(--station-gap, 44px); border: 0.5px solid var(--border);
  border-radius: 12px; background: var(--surface);
}
details.fold > summary {
  cursor: pointer; padding: 18px 22px; list-style: none;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
}
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary::before {
  content: "\25B8"; color: var(--muted); font-size: 11px;
  margin-right: 2px; transition: transform var(--t-fast) var(--ease);
}
details.fold[open] > summary::before { content: "\25BE"; }
details.fold > summary:hover h2 { color: var(--accent); }
details.fold > summary h2 {
  margin: 0; font-family: var(--font-head); font-size: 15px;
  font-weight: 550; letter-spacing: -0.01em; color: var(--text);
}
.fold-note { font-size: var(--fs-caption); }
/* THE INNER PADDING, and why it is set on the CHILDREN rather than the box.

   This was `summary + * { padding: 0 16px 14px }`, which put the inset on the
   <section> that follows the summary.  It did nothing, because
   `.section { padding: 0 }` below it has equal specificity and comes later --
   so every fold's text sat flush against the border, which is exactly the
   "text is near the edge" this rule was written to prevent.

   Insetting the innermost content instead cannot be overridden by accident:
   the padding lives on the same element the text does. */
details.fold .panel-body,
details.fold .stat-grid,
details.fold > .section > .panel-body { padding: 0 22px 22px; }

/* The folded sections keep their own panel chrome off: they are already inside
   a bordered box, and nesting a second frame reads as a bug. */
details.fold .panel { border: 0; box-shadow: none; background: transparent; border-radius: 0; }
details.fold .section { margin-top: 0; padding: 0; }
/* The fold's summary row IS the heading, so a nested one repeats it verbatim
   ("Weather right now" appeared twice on the same screen).  Only the heading
   is hidden -- the right-hand controls beside it (density toggle, window
   chips) stay, because those are the panel's actual affordances. */
details.fold .sec-head { display: none; }
details.fold .panel-head h3 { display: none; }

/* Inside a fold the head has NO visible title, so `margin-left: auto` on
   `.right` has nothing to push against -- it floats that one label to the far
   right of an otherwise empty row.  Measured on the live regime card: the
   "30-day record" span sat at left=1035px of 1138px, so ~1000px of blank row
   with one small grey label at the end.

   `.chips` is deliberately NOT touched: a control group is still an
   affordance and stays where it was.  Where a head holds BOTH (Daily error
   trend), the caption left-aligns and the chips keep their place. */
details.fold .panel-head .right {
  margin-left: 0;
  margin-right: auto;
}

/* The fold's head inset is 39px on the LEFT and 18px on the right.

   39px is where the fold's own <h2> starts -- 16px summary padding, a 13px
   disclosure marker, a 12px flex gap -- and the caption must line up with the
   heading DIRECTLY ABOVE it.  It sat at 18px before, which is correct against
   the panel's own edge and 21px out of step with the title, and that stagger is
   what reads as "not properly positioned".

   The right side stays 18px: there is no heading above it to align to.

   The BODY below keeps 18px, deliberately -- it is text under the caption, not
   under the title, and indenting it to 39px would push it past the panel. */
details.fold .panel-head { margin-bottom: 12px; padding: 0 22px 0 45px; }
/* A table inside a fold scrolls inside its own inset rather than bleeding to
   the border. */
details.fold .table-wrap { margin: 0 22px 22px; }
details.fold .panel-body .table-wrap { margin: 0; }

@media (max-width: 640px) {
  .sum-body { gap: 14px 20px; }
  .sum-value { font-size: 52px; }
  details.fold > summary { padding: 15px 16px; }
}

.dash-meta { padding-block: 12px; }
.dash-meta .cell-head { margin-bottom: 8px; }
.dash-meta .cell-head h2 {
  font-size: var(--fs-body);
  font-weight: 550;
  color: var(--text-soft);
}
.dash-meta .cell-note { font-size: var(--fs-caption); }

/* Cell headers: small, aligned with the grid, never boxed. */
.cell-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.cell-head h2 {
  margin: 0;
  font-family: var(--font-head);
  font-size: 15px;
  font-weight: 550;
  letter-spacing: -0.01em;
  color: var(--text);
}
.cell-note { font-size: var(--fs-caption); color: var(--muted); }
/* `.ledger-summary` and `.ledger-note` styled the removed forecast cell's
   one-sentence day summary and its midnight-to-midnight rule.  The rule is
   still stated on the roster and each station page; only this copy is gone. */
.cell-head .filters { margin-left: auto; }
.cell-body { min-width: 0; }

/* The verdict rail: three stacked readings, one pattern each.
   label (12px muted) → value (28px, tabular) → one context line.
   The rows share equal height so the three readings scan as a column. */
.dash-verdicts .stat-grid {
  grid-template-columns: 1fr;
  box-shadow: none;
  background: transparent;
  border-radius: 0;
  gap: 0;
}
.dash-verdicts .stat {
  display: grid;
  gap: 3px;
  padding: 0 0 16px;
  border-radius: 0;
  background: transparent;
}
/* The divider belongs on the WRAPPER, not the .stat.
   verdictCard() (index.js:131) emits `<a class="stat-link"><div class="stat">`,
   so the three .stat elements are never siblings of one another -- they are
   each an only child of their own anchor.  `.dash-verdicts .stat + .stat`
   therefore matched ZERO elements (measured on the live page: 3 for
   `.dash-verdicts .stat`, 0 for the sibling form), and the rail rendered as
   three stacked boxes with no separators instead of the flat strip it was
   designed to be.  The same mistake is why `.stat:last-child` below was dead. */
.dash-verdicts .stat-link + .stat-link .stat {
  padding-top: 16px;
  box-shadow: inset 0 0.5px 0 var(--border);
}
.dash-verdicts .stat-link:last-child .stat { padding-bottom: 0; }
.dash-verdicts .stat .label {
  font-size: var(--fs-caption);
  color: var(--muted);
  min-height: 18px;
}
.dash-verdicts .stat .value {
  font-size: 28px;
  font-weight: 550;
  letter-spacing: -0.015em;
  line-height: 1.1;
  margin-top: 0;
}
.dash-verdicts .stat .sub {
  font-size: var(--fs-caption);
  line-height: 1.45;
  margin-top: 2px;
  color: var(--muted);
}
/* Links in the rail keep the card hover but drop the box treatment. */
.dash-verdicts .stat-link { display: block; text-decoration: none; }
.dash-verdicts .stat-link:hover .stat .value { color: var(--accent); }

/* Status cell: quiet facts, dot-separated, no pills. */
.dash-status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  font-size: 12.5px;
  color: var(--muted);
}
.dash-status .status-left, .dash-status .status-right {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
}
.dash-status .status-right { margin-left: auto; }
.dash-status .pill-mini {
  box-shadow: none; padding: 0; border-radius: 0;
  font-size: 12.5px; color: var(--muted);
}
.dash-status .pill-mini + .pill-mini::before {
  /* The separator between status pills.  Its own siblings use --muted, and it
     used --border-strong, which is a border token at 1.99:1 (dark) / 1.75:1
     (light) -- so the separator was invisible and the pills read as one
     run-on string.  Matches its siblings now. */
  content: "·"; margin-right: 14px; color: var(--muted);
}
.dash-status .pill-mini b { color: var(--text-soft); font-weight: 550; }
.dash-status a.pill-mini { color: var(--accent); }
.dash-status .muted { font-size: 12.5px; }

/* Ledger + leaderboard: their controls sit in the cell head. */

/* --- region chips: one aligned row ---
   Left group = region chips; right group = search + export + guide.
   Both groups are flex rows on the same baseline, so nothing wraps to
   a stray line and the search field is the tallest item, setting the
   row height for everything beside it.
   Shared by /stations, /scorecards and /edge-finder.  The dashboard's own
   copy of this row left with the forecast ledger. */
#region-chips {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  width: 100%;
  margin-top: 2px;
}
#region-chips .toolbar-group {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
#region-chips .toolbar-end {
  margin-left: auto;
  gap: 10px;
}
#region-chips .toolbar-end .search { min-width: 220px; }
#region-chips .toolbar-link {
  font-size: var(--fs-caption);
  text-decoration: none;
  white-space: nowrap;
}
#region-chips .toolbar-link:hover { color: var(--text-soft); }
@media (max-width: 900px) {
  #region-chips .toolbar-end { margin-left: 0; }
  #region-chips .toolbar-end .search { min-width: 0; flex: 1 1 100%; }
}

/* Responsive: the mosaic collapses to one column, rails become rows.
   NOTE the wrapper in these two rules.  `.dash-verdicts .stat + .stat` matches
   NOTHING, because verdictCard() puts each .stat inside its own .stat-link
   anchor -- so both of these responsive dividers were dead too, not just the
   base one.  A stylesheet-wide scan caught them; the base rule alone had
   already been fixed. */
@media (max-width: 1000px) {
  .dash-hero { grid-column: 1 / span 12; padding-right: 0; border-right: 0; }
  .dash-verdicts { grid-column: 1 / span 12; padding-left: 0; border-top: 0.5px solid var(--border); }
  .dash-verdicts .stat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; }
  .dash-verdicts .stat { padding: 14px 16px; }
  .dash-verdicts .stat-link + .stat-link .stat {
    padding-top: 14px;
    box-shadow: inset 0.5px 0 0 var(--border);
  }
}
@media (max-width: 640px) {
  .dash-verdicts .stat-grid { grid-template-columns: 1fr; }
  .dash-verdicts .stat-link + .stat-link .stat { box-shadow: inset 0 0.5px 0 var(--border); }
  .dash-cell { padding: 16px 0; }
}

.topbar-search input { font-size: var(--fs-body); }
table.data a strong { font-size: var(--fs-caption); }
.delta-pill { font-size: 11px; }
.big-note { font-size: var(--fs-body); }
.toast { font-size: var(--fs-body); }
.picker-empty { font-size: var(--fs-body); }
.chart-legend { font-size: var(--fs-caption); }
.stat .sub { font-size: var(--fs-caption); }
.stat .value small { font-size: var(--fs-body); }

/* ==========================================================================
   LAYOUT REDESIGN (2026-09-17) — "the document, not the dashboard"
   The site was boxes inside boxes: section → panel(border) → body(pad),
   every block the same visual weight, several with two competing headings.
   This pass flattens the nesting, merges the double headings, turns stat
   cards into a metric strip, and lets tables own the width.  Boxes survive
   only where they group genuinely parallel items (stat strips, station
   cards, charts that need a frame).
   ========================================================================== */

/* --- 6: section rhythm variants ---
   Three weights instead of one flat 44px gap:
     .section          default editorial spacing
     .section.dense    tight — table clusters, related panels
     .section.display  medium — stat strips, charts
   Section headings gain a hairline rule so the page reads as a document. */
.section { margin-top: 40px; }
.section.dense { margin-top: 24px; }
.section.display { margin-top: 32px; }
.sec-head { margin-bottom: 12px; }
.sec-head h2 {
  display: flex;
  align-items: center;
  gap: 12px;
}
.sec-head h2::after {
  content: "";
  flex: 1;
  height: 0.5px;
  background: var(--border);
}

/* --- 1: flatten nesting ---
   A section that holds exactly one panel drops the panel chrome: the
   section itself is the container.  Sections with several panels (paired
   boards) keep their frames.  :has() is supported in every current
   browser; without it the page simply keeps the old boxed look.

   IMPORTANT: a panel that keeps its frame (chart panels below) must also
   keep its head/body padding.  Stripping padding from a framed panel
   pressed the title, chips and caption against the border — the
   "overflowing header" bug on the station compare card. */
.section:not(:has(> .panel ~ .panel)) > .panel:not(:has(.chart-box)) {
  border: 0;
  box-shadow: none;
  border-radius: 0;
  background: transparent;
}
/* A fold supplies its own inset, so this flattening rule must not zero it.

   THE BUG THIS FIXES: this rule is specificity (0,6,0) thanks to its two
   :has() chains, while the fold's own inset rule is (0,2,1).  So `padding: 0`
   won no matter where either rule sat in the file, and every fold's text sat
   flush against the border -- the "text is near the edge" complaint.  Adding
   the fold rule a second time, later, changed nothing, which is why the fix
   looked like it had not deployed.

   Excluded rather than out-specified: an arms race against a :has() chain is
   unwinnable, and a reader could never tell which rule won.  This rule was
   written for bare sections on the page, before folds existed; it should not
   reach inside one. */
.section:not(details.fold *) > .panel:not(:has(.chart-box)) > .panel-body { padding: 0; }
/* The HEAD half of the same fix.  This rule carried the identical :has()
   chain, and the identical problem: `padding: 0 0 10px` zeroes the horizontal
   padding, so inside a fold the panel head ran to the very edge of the box.
   Measured on the live regime card: the "-day record" span sat 0px from the
   panel's right edge, while the panel BODY below it was inset 18px.

   Excluded the same way rather than out-specified, for the same reason the
   sibling rule above records: an arms race against a :has() chain is
   unwinnable, and a reader could never tell which rule won. */
.section:not(details.fold *):not(:has(> .panel ~ .panel)) > .panel:not(:has(.chart-box)) > .panel-head {
  padding: 0 0 10px;
  border-bottom: 0.5px solid var(--border);
  margin-bottom: 12px;
  background: transparent;
}
/* Chart panels keep their frame AND their padding: the plot needs the
   boundary, and the framed header needs breathing room. */
.section:not(:has(> .panel ~ .panel)) > .panel:has(.chart-box) {
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
}
.section:not(:has(> .panel ~ .panel)) > .panel:has(.chart-box) > .panel-body {
  padding: 16px;
}

/* --- 2: merge double headings ---
   Only a FLATTENED panel's title is demoted (it sits under a section
   head already).  A framed panel's title is its only heading and stays
   full size. */
.section:not(:has(> .panel ~ .panel)) > .panel:not(:has(.chart-box)) > .panel-head h3 {
  font-size: var(--fs-caption);
  font-weight: 550;
  color: var(--muted);
}

/* --- 3: stat cards → metric strip ---
   The 4-up grid becomes one strip: shared hairline frame, vertical
   dividers between cells, no per-card boxes.  Reads as one instrument
   instead of four competing tiles. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0;
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.stat-grid > .stat {
  border: 0;
  box-shadow: none;
  border-radius: 0;
  background: transparent;
  padding: 14px 16px;
}
.stat-grid > .stat + .stat { box-shadow: inset 0.5px 0 0 var(--border); }
.stat-grid > .stat:hover { background: var(--surface-2); }
/* A lone stat (or an error/empty block) keeps its own frame. */
.stat-grid > .stat:only-child { box-shadow: none; }
/* Nested grids (a stat-grid inside a panel) keep the strip look. */
.panel-body > .stat-grid { box-shadow: inset 0 0 0 0.5px var(--border); }

/* --- 4: tables own the width ---
   Tables bleed to the section edges with hairline rows; the header is a
   quiet tint rather than a border; rows highlight on hover instead of
   every cell drawing its own line. */
.table-wrap table.data { border-collapse: separate; border-spacing: 0; }
table.data th {
  background: var(--surface-2);
  border-bottom: 0.5px solid var(--border);
  padding: 9px 12px;
}
table.data th:first-child { border-top-left-radius: var(--radius); }
table.data th:last-child { border-top-right-radius: var(--radius); }
table.data td {
  border-bottom: 0.5px solid var(--border);
  padding: 9px 12px;
}
table.data tbody tr:last-child td { border-bottom: 0; }
table.data tbody tr:hover { background: var(--surface-2); }
/* Inside a flattened section the table spans the full column. */
.section:not(:has(> .panel ~ .panel)) > .panel > .panel-body > .table-wrap {
  margin-inline: 0;
}

/* --- chart panels keep a light frame (they need the boundary) ---
   EXCLUDING FOLDS, like every other flattening rule in this block.  Inside a
   fold the boundary is already the fold's own border, so this rule was
   re-framing every walk panel and undoing the de-boxing pass: measured 37
   framed elements where the pass had reached 9.

   This rule sits LATE in the file and matched `.panel:has(.chart-box)` with
   no exclusion at all, so it beat `details.fold .panel { box-shadow: none }`
   on source order despite that rule being more specific about intent.  The
   `:not(details.fold *)` guard is the same one the sibling rules above carry;
   it was simply missing here. */
.panel:not(details.fold *):has(> .panel-body > .chart-box),
.panel:not(details.fold *):has(.chart-box) {
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
}
.panel:not(details.fold *):has(.chart-box) > .panel-body { padding: 16px; }

/* --- two-column rhythm for related blocks (paired with item 5 markup) --- */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 24px;
  align-items: start;
}
.split.even { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 980px) {
  .split, .split.even { grid-template-columns: 1fr; gap: 28px; }
}
/* A 2-up stat strip for narrow columns (station track record). */
.stat-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.stat-grid-2 > .stat:nth-child(odd) { box-shadow: none; }
.stat-grid-2 > .stat:nth-child(even) { box-shadow: inset 0.5px 0 0 var(--border); }
.stat-grid-2 > .stat:nth-child(n+3) { box-shadow: inset 0 0.5px 0 var(--border); }
.stat-grid-2 > .stat:nth-child(even):nth-child(n+3) {
  box-shadow: inset 0.5px 0 0 var(--border), inset 0 0.5px 0 var(--border);
}

/* --- 7: stations list view --- */
.st-table { font-size: var(--fs-caption); }
.st-row { cursor: pointer; }
.st-row:hover { background: var(--surface-2); }
.st-code { white-space: nowrap; }
.st-code .fresh-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: 8px; vertical-align: 1px;
}
.st-name { color: var(--text-soft); }
.st-name .muted { font-size: var(--fs-caption); }
.st-when { white-space: nowrap; font-size: var(--fs-caption); }
.st-cmp { width: 40px; text-align: right; }
.st-cmp .cmp-add {
  appearance: none; border: 0; background: transparent; color: var(--muted);
  cursor: pointer; font-size: 14px; padding: 2px 6px; border-radius: 6px;
}
.st-cmp .cmp-add:hover { color: var(--text); background: var(--surface-3); }
.st-cmp .cmp-add.in { color: var(--success); }
@media (max-width: 720px) {
  .st-name, .st-table th:nth-child(6), .st-table td:nth-child(6) { display: none; }
}

/* --- 8: row density control ---
   Compact/comfortable for the big tables.  The toggle lives in the
   table's section head; the class lands on <body> so one setting covers
   every table on the page and persists per visitor. */
body.density-compact table.data th,
body.density-compact table.data td { padding: 5px 10px; }
body.density-compact table.data { font-size: var(--fs-caption); }
body.density-compact .stat-grid > .stat { padding: 10px 13px; }
body.density-compact .st-row { line-height: 1.3; }
.density-toggle {
  display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px;
  box-shadow: inset 0 0 0 0.5px var(--border); background: var(--surface);
}
.density-toggle button {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  font: 500 11px var(--font); color: var(--muted); padding: 3px 9px;
  border-radius: 6px; letter-spacing: 0;
}
.density-toggle button.active { background: var(--surface-3); color: var(--text); }



/* ==========================================================================
   DESIGN REVIEW FOLLOW-UPS (2026-09-19)
   Brand mark, consolidated fixed chrome (social pill + back-to-top + language
   switch no longer share one corner), and utility classes that replace the
   repeated per-page inline styles.

   The dashboard fold restructure that shipped with this block was reverted on
   operator feedback -- the pre-existing .dash-hero / .dash-verdicts /
   .dash-ledger mosaic is what runs.  Its rules (.masthead-verdicts,
   .dash-ledger-hero, .dash-recap-rail) are removed rather than left inert, so
   a future edit cannot mistake them for live styling.
   ========================================================================== */

/* --- fixed chrome: one stacked, palette-aware corner --- */
.x-link {
  position: fixed; bottom: 16px; right: 16px; z-index: 44;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; border: 0; border-radius: 999px;
  background: var(--surface);
  box-shadow: inset 0 0 0 0.5px var(--border), var(--shadow-sm);
  color: var(--muted); font-size: var(--fs-caption); font-weight: 500; text-decoration: none;
  transition: color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.x-link:hover { color: var(--text); box-shadow: inset 0 0 0 0.5px var(--border-strong), var(--shadow-sm); }
/* Stations page: the floating compare bar owns the bottom edge, so the
   social pill and back-to-top stack above it instead of on top of it. */
.has-compare-bar .x-link { bottom: 74px; }
.has-compare-bar .to-top { bottom: 122px; }

/* Login keeps its social link in-flow under the panel, not fixed. */
.login-x {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 12px;
  padding: 6px 12px; border: 0; border-radius: 999px;
  box-shadow: inset 0 0 0 0.5px var(--border);
  color: var(--muted); font-size: var(--fs-body); text-decoration: none;
  transition: color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.login-x:hover { color: var(--text); box-shadow: inset 0 0 0 0.5px var(--border-strong); }

/* --- utility classes (replace the repeated inline styles) --- */
.filters-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
.u-accent { color: var(--accent); }
.u-plain { text-decoration: none; }
.u-flex-between { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; }
.u-mt-0 { margin-top: 0; } .u-mt-16 { margin-top: 16px; }
.u-mb-0 { margin-bottom: 0; } .u-mb-12 { margin-bottom: 12px; }
.skel-92  { height: 92px; }  .skel-96  { height: 96px; }
.skel-120 { height: 120px; } .skel-150 { height: 150px; }
.skel-180 { height: 180px; } .skel-200 { height: 200px; }
.skel-220 { height: 220px; }

/* ==========================================================================
   DASHBOARD REDESIGN (2026-09-26)

   Measured before this pass, on the live page:
     * 439px of chrome preceded the first forecast number on a 1440x900 screen
       -- 49% of the viewport.  On a 375x812 phone it was 735px, 91%.
     * Every cell shared one padding and one hairline, so nothing led and
       nothing receded; the eye had no entry point.
     * Eleven table columns at 375px forced a horizontal scroll before a reader
       reached the number they came for.
     * .stat was defined three times and .status-line/.verdict-strip were dead.

   The pass is mostly SUBTRACTION: the masthead stops being a hero, the audit
   trail recedes, three columns step aside on a phone, and the dead layers are
   gone.  No new cells, no new features, no new JavaScript beyond a class name.
   ========================================================================== */

/* --- the removed forecast ledger ---
   `.dash-ledger .cell-body` and the phone rules below styled the 11-column
   table that used to sit at the top of the fold.  Both went with the cell;
   `.table-capped` and `.col-optional` are still live (station.js uses them). */

/* --- the meta cells recede further on a phone ---
   The audit trail is worth having and worth scrolling to; it should not
   compete with the answer for the first screen. */
@media (max-width: 720px) {
  .dash-meta { padding-block: 10px; }
  .dash-hero { padding-right: 0; border-right: 0; }
  .dash-verdicts { padding-left: 0; }
}


/* ==========================================================================
   PRO OFFER + PAYWALL (2026-09-29)
   Two server-rendered pages that exist to make one sale, and the rules they
   follow are the same: lead with the reader's own airport, show the evidence
   rather than adjectives, and answer the questions a paywall usually leaves
   the reader to guess.

   The measure is 680px because both pages are prose plus one table -- wider
   and the eye loses the line, narrower and the table wraps.
   ========================================================================== */

.pro-page {
  max-width: 680px;
  margin: 0 auto;
  padding: 40px 20px 64px;
}

.pro-hero { margin-bottom: 26px; }
.pro-hero h1 {
  font-size: clamp(26px, 5vw, 38px);
  line-height: 1.15;
  margin: 10px 0 0;
}
.pro-hero h1 span {
  display: block;
  font-size: 15px;
  font-weight: 400;
  color: var(--muted);
  margin-top: 8px;
  line-height: 1.5;
}
.pro-lede {
  margin-top: 14px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-soft);
  max-width: 62ch;
}

/* --- the price block ------------------------------------------------------ */
.pro-offer {
  margin: 24px 0 30px;
  padding: 20px;
  border: 0.5px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
}
.pro-price {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 4px;
}
.pro-amount {
  font-size: 40px;
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.pro-once { color: var(--muted); font-size: var(--fs-caption); }
.pro-offer .panel { margin-top: 14px; border: 0; background: transparent; }
.pro-offer .panel-body { padding: 0; }
.pro-reassure {
  margin-top: 12px;
  font-size: 12px;
  color: var(--muted);
}
.pro-have {
  border-color: var(--success) !important;
  background: var(--success-soft) !important;
}

/* --- the evidence table --------------------------------------------------- */
.pro-why { margin: 34px 0; }
.pro-why h2, .pro-what h2, .pro-free h2, .pro-faq h2,
.pro-locked-section h2 {
  font-size: 19px;
  margin-bottom: 10px;
}
.pro-why > p { color: var(--text-soft); margin-bottom: 14px; }

.pro-table td, .pro-table th { padding: 10px 12px; }
.pro-table .pro-stars {
  font-size: 15px;
  letter-spacing: 2px;
  color: var(--warn);
  white-space: nowrap;
}
.pro-table td.num { font-weight: 550; }
.pro-table tbody tr:first-child td { background: var(--success-soft); }

/* --- what Pro opens ------------------------------------------------------- */
.pro-what { margin: 34px 0; }
.pro-what > p { color: var(--text-soft); margin-bottom: 12px; }
.pro-list {
  margin: 0;
  padding-left: 0;
  list-style: none;
}
.pro-list li {
  position: relative;
  padding-left: 26px;
  margin-bottom: 10px;
  line-height: var(--lh-body);
  color: var(--text-soft);
}
.pro-list li::before {
  content: "";
  position: absolute;
  left: 8px;
  top: 0.62em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
}
.pro-list strong { color: var(--text); }

/* --- the live locked list ------------------------------------------------- */
.pro-locked-section { margin: 34px 0; }
.pro-locked {
  columns: 2;
  column-gap: 20px;
  margin: 12px 0;
  padding-left: 0;
  list-style: none;
}
.pro-locked li { margin-bottom: 6px; break-inside: avoid; }
.pro-locked a {
  font-weight: 550;
  font-family: var(--font);
}
.pro-note { margin-top: 12px; max-width: 62ch; }

/* --- free tier + FAQ ------------------------------------------------------ */
.pro-free { margin: 34px 0; }
.pro-free p { color: var(--text-soft); max-width: 62ch; }

.pro-faq { margin: 34px 0 0; }
.pro-faq dl { margin: 0; }
.pro-faq dt {
  font-weight: 600;
  margin-top: 16px;
  color: var(--text);
}
.pro-faq dd {
  margin: 4px 0 0;
  color: var(--text-soft);
  line-height: var(--lh-body);
  max-width: 62ch;
}

/* ==========================================================================
   THE STATION PAYWALL
   Same voice, one airport.  The reader arrived wanting THIS station, so the
   page leads with its rating and only then makes the offer.
   ========================================================================== */

.paywall .paywall-head { margin-bottom: 22px; }
.paywall .paywall-head h1 { margin-top: 10px; }
.paywall .paywall-head h1 span {
  display: block;
  font-size: 16px;
  font-weight: 400;
  color: var(--muted);
  margin-top: 6px;
}
.paywall-rating {
  margin-top: 14px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}
.paywall-rating .muted { font-size: var(--fs-caption); }

.paywall-card { border-color: var(--border-strong); }
.paywall-behind {
  margin: 16px 0 10px;
  font-weight: 600;
  font-size: var(--fs-caption);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.paywall-cta {
  margin-top: 20px;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.paywall-reassure { margin-top: 14px; font-size: 12px; }

.paywall-facts { margin-top: 26px; }
.paywall-facts p { max-width: 62ch; color: var(--text-soft); }
.paywall-facts p + p { margin-top: 12px; }

@media (max-width: 720px) {
  .pro-page { padding: 24px 16px 48px; }
  .pro-amount { font-size: 34px; }
  /* One column: two columns of station codes at 375px is a wall of text. */
  .pro-locked { columns: 1; }
  .pro-table td, .pro-table th { padding: 8px 8px; }
  .pro-table .pro-stars { font-size: 13px; letter-spacing: 1px; }
  .paywall-cta .btn { flex: 1 1 auto; text-align: center; }
}


/* ==========================================================================
   FUNNEL NAV, CLIMB CHART, CATALOG (2026-09-29, second pass)
   The two pages that exist to make one sale: /pro and a locked station.

   Three additions, each answering a specific failure of the first pass:

   1. THE NAV.  Both pages were dead ends -- a reader who arrived from a
      search result or a shared link had no way back and no way home except
      the browser button.  The site topbar is not an option here: it needs
      common.js for the clock, the station search and the display menu, and
      neither funnel page loads a script.  113 KB of JS for a clock is the
      wrong trade, so this is a slim bar that answers the only two questions
      a reader on this page has -- where am I, and how do I leave.

   2. THE CLIMB CHART.  "The rating is measured" was a claim.  This is the
      measurement: the airport's own exact-high rate at each checkpoint
      through the day, drawn from the PUBLIC board so it previews the paid
      record without leaking any of it.  The greyed 4pm bar is the argument
      -- every rating on the site excludes it, because by 4pm the day has
      settled and scoring there measures the observation, not the forecast.

   3. THE CATALOG.  A list of codes asks the reader to take the rating on
      faith.  The same list with each airport's checkpoint and rate is
      checkable against the free board, which is what makes it evidence
      rather than marketing.
   ========================================================================== */

/* --- funnel nav ----------------------------------------------------------- */
.funnel-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  max-width: 680px;
  margin: 0 auto;
  padding: 14px 20px 0;
  font-size: var(--fs-caption);
}
.funnel-brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font);
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  letter-spacing: -0.01em;
}
.funnel-brand:hover { color: var(--accent); }
.funnel-logo { border-radius: 6px; display: block; }
.funnel-links { display: inline-flex; align-items: center; gap: 6px; }
.funnel-links a {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 11px;
  border-radius: 999px;
  color: var(--text-soft);
  text-decoration: none;
  font-weight: 550;
  box-shadow: inset 0 0 0 0.5px var(--border);
  background: var(--surface);
  transition: background var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease);
}
.funnel-links a:hover {
  color: var(--text);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 0.5px var(--border-strong);
}
/* The back link is the one a reader most often wants, so it carries the
   accent on hover and the home link stays deliberately quieter. */
.funnel-back:hover { color: var(--accent) !important; }
.funnel-back span { transition: transform var(--t-fast) var(--ease); }
.funnel-back:hover span { transform: translateX(-2px); }

/* --- the climb chart ------------------------------------------------------ */
.climb { margin: 30px 0; }
.climb-title { font-size: 19px; margin-bottom: 12px; }
.climb-figure { margin: 0; }
.climb-svg {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
  border-radius: 12px;
  background: var(--surface);
  box-shadow: inset 0 0 0 0.5px var(--border);
  padding: 6px 8px 0;
  box-sizing: content-box;
}
.climb-bar { fill: var(--accent); }
/* The checkpoint the ratings deliberately exclude.  Grey, never accent: it is
   shown to be discounted, not to compete with the three that count. */
.climb-bar-excluded { fill: var(--border-strong); }
.climb-value {
  fill: var(--text);
  font: 600 11px var(--font);
  text-anchor: middle;
  font-variant-numeric: tabular-nums;
}
.climb-label {
  fill: var(--muted);
  font: 500 10px var(--font);
  text-anchor: middle;
}
.climb-guess {
  stroke: var(--warn);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.85;
}
.climb-guess-label {
  fill: var(--warn);
  font: 600 9px var(--font);
  text-anchor: end;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.climb-caption {
  margin-top: 12px;
  font-size: var(--fs-caption);
  line-height: var(--lh-body);
  color: var(--text-soft);
  max-width: 62ch;
}
.climb-caption strong { color: var(--text); }

/* --- the Pro catalog ------------------------------------------------------ */
.pro-catalog td, .pro-catalog th { padding: 9px 12px; }
.pro-catalog tbody tr:hover { background: var(--surface-2); }
.pro-catalog td.num { font-weight: 600; }
.pro-locked-count {
  margin: 12px 0;
  font-size: var(--fs-caption);
  color: var(--text-soft);
}
.pro-catalog + .pro-note { margin-top: 10px; }

/* --- offer block: give the price a little more presence ------------------- */
.pro-offer {
  background:
    radial-gradient(120% 140% at 0% 0%, var(--accent-soft), transparent 58%),
    var(--surface);
}

@media (max-width: 720px) {
  .funnel-nav { padding: 12px 16px 0; }
  .climb-svg { padding: 4px 4px 0; }
  .climb-caption { font-size: 12px; }
  /* The catalog's Region column is context; at 375px it costs the airport
     code its room.  Hidden, not dropped -- still in the DOM for the CSV and
     for screen readers. */
  .pro-catalog .col-optional { display: none; }
}

/* --- light theme: the greyed bar needs to stay legible on white ---------- */
[data-theme="light"] .climb-bar-excluded { fill: #c9c5c2; }
[data-theme="light"] .climb-value { fill: var(--text); }


/* --- the engine's public track record, as a strip ------------------------- */
/* Deliberately the same visual weight as the rating table rather than a
   bigger one: it is corroboration, not the headline claim.  It sits between
   the rating evidence and the feature list so the reader meets it while the
   "can I check this?" question is still open. */
.track { margin: 34px 0; }
.track h2 { font-size: 19px; margin-bottom: 12px; }
.track-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
  gap: 10px;
}
.track-cell {
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--surface);
  box-shadow: inset 0 0 0 0.5px var(--border);
}
.track-value {
  font-family: var(--font);
  font-size: 21px;
  font-weight: 650;
  letter-spacing: -0.015em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  line-height: 1.15;
}
.track-label {
  margin-top: 5px;
  font-size: 11px;
  font-weight: 550;
  color: var(--text-soft);
  line-height: 1.35;
}
.track-note {
  margin-top: 2px;
  font-size: 10px;
  color: var(--muted);
  line-height: 1.35;
}
.track-source { margin-top: 12px; font-size: 12px; max-width: 62ch; }

@media (max-width: 720px) {
  .track-strip { grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); }
  .track-value { font-size: 18px; }
}


/* --- buttons: the funnel pages' primary and secondary controls ------------
   Defined HERE because nothing else did.  /pro and a locked station both emit
   class="btn btn-primary" for the buy action; grepping the stylesheet and all
   18 templates found no rule and no other user, so that control had been
   rendering as plain accent text.  These follow .edge-finder-btn's idiom --
   inset hairline, accent text, soft wash on hover -- so the two pages look
   like the rest of the site instead of like an unstyled link. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 16px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--accent) 45%, var(--border));
  color: var(--accent);
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 600;
  letter-spacing: 0;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
.btn:hover {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--accent);
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The buy action: filled, because on these two pages it is the one control
   that matters and the page's whole job is to make it obvious. */
.btn-primary {
  background: var(--accent);
  color: #fff;
  box-shadow: none;
  font-weight: 650;
}
.btn-primary:hover {
  background: var(--accent);
  color: #fff;
  filter: brightness(1.08);
  box-shadow: 0 0 0 4px var(--accent-soft);
}
/* Light theme: the accent is a deep red, so white text still passes, but the
   fill needs no brightening on hover. */
[data-theme="light"] .btn-primary:hover { filter: brightness(0.94); }


/* ==========================================================================
   STATION PAGE - READABILITY (2026-09-29)
   See the block comment above the diff that raised the fold's insets: the
   horizontal geometry lives IN PLACE because tests read it, and this block
   carries only the additions.  Scoped to #station-root so no other page moves.
   ========================================================================== */

/* --- the reading measure --------------------------------------------------
   THE BIGGEST FAULT.  Prose ran to 182 characters; comfortable is 45-80, and
   past ~100 the eye loses its place on the return sweep.  That is what
   "dizzy" is, physically.

   62ch rather than the panel's existing 68ch: a `ch` cap scales with the font
   size, so raising the prose from 12.5px to 15px while holding 68 would have
   WIDENED the physical line while appearing to keep the cap.  62ch at 15px
   is about 68ch at 14px -- the same line, at a readable size. */
#station-root .sum-bias,
#station-root .sum-record,
#station-root .empty-state,
#station-root .regime-headline,
#station-root .regime-note,
#station-root .regime-foot,
#station-root .panel-body > p,
#station-root .panel-body > div > p,
/* The walk-chart legend is 12.5px prose that wraps to two lines and, at
   167 characters, was the longest line left on the page after the first
   pass.  Caught by re-measuring rather than by assuming the cap was total. */
#station-root .muted[style*="font-size:11.5px"],
#station-root details.fold > summary .fold-note {
  max-width: 62ch;
}

/* --- the type scale -------------------------------------------------------
   Real steps.  Before: title 15px, body 14px, bulk prose 12.5px -- three
   sizes inside 2.5px, so nothing read as a heading and everything read as
   fine print.  The 11px metadata stays small deliberately: it is a different
   class of information, not body copy. */
#station-root details.fold > summary h2 {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.3;
}
#station-root .sum-bias,
#station-root .empty-state,
#station-root .regime-headline {
  font-size: 15px;
  line-height: 1.7;
}
#station-root .regime-note,
#station-root .regime-foot,
#station-root .sum-record {
  font-size: 13.5px;
  line-height: 1.65;
}
/* The walk-chart legend was 11.5px prose that wraps to two lines. */
#station-root .muted[style*="font-size:11.5px"] {
  font-size: 12.5px !important;
  line-height: 1.6;
}

/* --- the summary card -----------------------------------------------------
   The figure the page is FOR keeps its weight; what it gains is air, because
   the fault here was crowding rather than scale. */
#station-root .sum-body { gap: 20px 40px; }
#station-root .sum-value { line-height: 1.05; }
#station-root .sum-value-label { margin-top: 8px; }
#station-root .sum-side { gap: 10px; }
#station-root .sum-bias { margin-top: 18px; padding: 12px 14px; }
#station-root .sum-record { margin-top: 16px; padding-top: 14px; }

/* --- de-boxing ------------------------------------------------------------
   A fold is a bordered card.  Inside it everything was ALSO a bordered card
   -- a .panel, then .stat cards -- so a reader met up to three nested frames,
   29 bordered boxes in all.  Inside a fold, content is separated by SPACE
   instead.  Only rules for elements that carry a frame are overridden. */
#station-root details.fold .stat {
  box-shadow: none;
  background: transparent;
  border-radius: 0;
  padding: 0;
}
#station-root details.fold .stat-grid { gap: 18px 32px; }
#station-root details.fold .stat .label {
  font-size: 12.5px;
  color: var(--muted);
  text-transform: none;
  letter-spacing: 0;
}
#station-root details.fold .stat .value { font-size: 22px; line-height: 1.2; margin-top: 2px; }
#station-root details.fold .stat .sub { font-size: 12px; line-height: 1.5; margin-top: 4px; }

/* --- tables: air, not size ------------------------------------------------
   Row padding only.  The face stays put: these tables fit their wrapper
   exactly at 375px, and a larger font would push columns off a phone. */
#station-root table.data td { padding: 11px 12px; }
#station-root table.data th { padding: 9px 12px; }

/* --- the walk: one sub-heading per day ------------------------------------
   Each day is a .panel with a head.  Inside the walk fold the frame is gone
   (the fold supplies it), so the day's title needs to read as a sub-heading
   rather than as a second page title. */
#station-root .fold .panel { margin-bottom: 26px; }
#station-root .fold .panel:last-child { margin-bottom: 0; }
#station-root .fold .panel-head h3 { font-size: 15px; font-weight: 600; }

@media (max-width: 720px) {
  /* On a phone the viewport already caps the measure, so only the scale
     needs adjusting. */
  #station-root details.fold > summary h2 { font-size: 18px; }
  #station-root details.fold .stat-grid { gap: 16px 20px; }
  #station-root table.data td { padding: 9px 8px; }
}


/* --- the share discount on /pro ------------------------------------------
   A second offer beside the full-price one, not a replacement for it.  The
   original button keeps its accent fill; this one is deliberately quieter so
   the page still has ONE obvious primary action and the discount reads as an
   alternative rather than as the thing we want everyone to click.

   The badge carries the accent because "35% off" is the reason to look at the
   block at all; the button does not, so the two do not compete. */
.pro-discount {
  margin-top: 20px;
  padding-top: 18px;
  border-top: 0.5px solid var(--border);
}
.pro-discount-head {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  font-weight: 600;
  color: var(--text);
}
.pro-discount-badge {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fs-label);
  font-weight: 650;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.pro-discount-body {
  margin-top: 8px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-soft);
  max-width: 62ch;
}
.pro-discount-body strong { color: var(--text); }

/* Outlined, not filled: the secondary action.  Same family as .btn so the two
   read as the same kind of control. */
.pro-buy-discount {
  border: 0;
  box-shadow: inset 0 0 0 0.5px var(--border-strong);
  color: var(--text);
  background: transparent;
  font-weight: 600;
}
.pro-buy-discount:hover {
  box-shadow: inset 0 0 0 1px var(--accent);
  background: var(--accent-soft);
  color: var(--text);
}
.pro-discount-fine {
  margin-top: 10px;
  font-size: 12px;
  line-height: 1.55;
  max-width: 62ch;
}

@media (max-width: 720px) {
  .pro-buy-discount { width: 100%; }
}
