/* Theme tokens. Light = direction C "instrument panel"; dark = direction A
   "ops console". data-theme (set by the toggle, persisted) overrides the OS
   preference in both directions. */
:root {
  --bg: #f4f6f9;
  --fg: #1c2430;
  --muted: #647082;
  --accent: #2563eb;
  --border: #d9dfe8;
  --card: #ffffff;
  --thead: #eef2f7;
  --star: #b45309;
  --wash-warm-rgb: 199, 58, 18;
  --wash-cool-rgb: 37, 99, 235;
  --shadow: rgba(15, 23, 42, 0.08);
  /* typography identity: light = quiet sans (instrument panel) */
  --font-data: system-ui, -apple-system, "Segoe UI", sans-serif;
  --display-font: system-ui, -apple-system, "Segoe UI", sans-serif;
  --display-size: 1.1rem;
  --display-case: none;
  --display-track: 0;
  --display-weight: 650;
  /* Animated card backgrounds: the scrim that keeps card text readable
     over a moving scene. Tuned heavier than the source design's 400px
     panels — our cards run ~229px with 0.72rem type at the bottom. */
  /* Light inverts the risk: text is dark, so the danger is a DARK scene
     pixel and the bound is a FLOOR, not dark's ceiling. What washed the
     scenes out was the mid/bot stops — at 0.80/0.92 only 20% and 8% of a
     scene's own colour survived the blend toward white, so the bottom
     two-thirds of every light card was very nearly grey. Now flat-ish at
     0.50/0.56/0.62 (50/44/38% retained). Paid for by darkening the scened
     -card text below, which drops the floor 0.4929 -> 0.3475 — the light
     analogue of the ceiling lift that dark needed.
     Derivation: docs/specs/card-backgrounds.md. Verify with
     `scripts/audit_wx_scenes.py 0.50 --light`. */
  --wx-scrim-rgb: 255, 255, 255;
  --wx-scrim-top: 0.50;
  --wx-scrim-mid: 0.56;
  --wx-scrim-bot: 0.62;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #12161d; --fg: #dce4ee; --muted: #8b98a9; --accent: #4cc2ff;
    --border: #2a3340; --card: #191f28; --thead: #1f2733; --star: #ffb454;
    --wash-warm-rgb: 232, 104, 14; --wash-cool-rgb: 79, 149, 238;
    --shadow: rgba(0, 0, 0, 0.45);
    --font-data: ui-monospace, "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, monospace;
    --display-font: ui-monospace, "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, monospace;
    --display-size: 0.85rem;
    --display-case: uppercase;
    --display-track: 0.06em;
    --display-weight: 600;
    --wx-scrim-rgb: 25, 31, 40;
    --wx-scrim-top: 0.60;
    --wx-scrim-mid: 0.66;
    --wx-scrim-bot: 0.74;
  }
}
:root[data-theme="dark"] {
  --bg: #12161d; --fg: #dce4ee; --muted: #8b98a9; --accent: #4cc2ff;
  --border: #2a3340; --card: #191f28; --thead: #1f2733; --star: #ffb454;
  --wash-warm-rgb: 232, 104, 14; --wash-cool-rgb: 79, 149, 238;
  --shadow: rgba(0, 0, 0, 0.45);
  --font-data: ui-monospace, "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --display-font: ui-monospace, "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --display-size: 0.85rem;
  --display-case: uppercase;
  --display-track: 0.06em;
  --display-weight: 600;
  /* 0.60, down from 0.78. A scrim doesn't just darken, it blends toward
     grey — at 0.78 only 22% of each scene's own colour survived, which is
     most of why the scenes read as dark tints. The trade that pays for
     0.60: no scene pixel may exceed lum 0.634, so the white bolt core and
     white sun core become SATURATED marks instead (vivid orange is lum
     0.43, electric blue 0.44 — saturation is luminance-cheap, white is
     not). Net: 40% of scene colour retained, ~2x the saturation.
     The bottom stop is flat-ish (0.74, was 0.93) because every scene's
     own gradient already darkens downward — the heavy bottom was
     protecting text against pixels that were dark anyway.
     Derivation: docs/specs/card-backgrounds.md. Verify with
     scripts/audit_wx_scenes.py before changing any of it. */
  --wx-scrim-rgb: 25, 31, 40;
  --wx-scrim-top: 0.60;
  --wx-scrim-mid: 0.66;
  --wx-scrim-bot: 0.74;
}
:root[data-theme="light"] {
  --bg: #f4f6f9; --fg: #1c2430; --muted: #647082; --accent: #2563eb;
  --border: #d9dfe8; --card: #ffffff; --thead: #eef2f7; --star: #b45309;
  --wash-warm-rgb: 199, 58, 18; --wash-cool-rgb: 37, 99, 235;
  --shadow: rgba(15, 23, 42, 0.08);
  --font-data: system-ui, -apple-system, "Segoe UI", sans-serif;
  --display-font: system-ui, -apple-system, "Segoe UI", sans-serif;
  --display-size: 1.1rem;
  --display-case: none;
  --display-track: 0;
  --display-weight: 650;
  /* Light inverts the risk: text is dark, so the danger is a DARK scene
     pixel and the bound is a FLOOR, not dark's ceiling. What washed the
     scenes out was the mid/bot stops — at 0.80/0.92 only 20% and 8% of a
     scene's own colour survived the blend toward white, so the bottom
     two-thirds of every light card was very nearly grey. Now flat-ish at
     0.50/0.56/0.62 (50/44/38% retained). Paid for by darkening the scened
     -card text below, which drops the floor 0.4929 -> 0.3475 — the light
     analogue of the ceiling lift that dark needed.
     Derivation: docs/specs/card-backgrounds.md. Verify with
     `scripts/audit_wx_scenes.py 0.50 --light`. */
  --wx-scrim-rgb: 255, 255, 255;
  --wx-scrim-top: 0.50;
  --wx-scrim-mid: 0.56;
  --wx-scrim-bot: 0.62;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--fg);
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem 1.5rem;
  background: var(--card);
  border-bottom: 1px solid var(--border);
}

/* Base link color — themed via --accent so no anchor ever falls back to
   the browser default (invisible on dark). Anything needing different
   treatment (muted footers, fg brand) overrides explicitly. */
a { color: var(--accent); }

.brand {
  font-family: var(--display-font);
  font-weight: 700;
  text-transform: var(--display-case);
  letter-spacing: var(--display-track);
  color: var(--fg);
  text-decoration: none;
}

.units-toggle a, .units-toggle .active {
  margin-left: 0.75rem;
  text-decoration: none;
}
.units-toggle .active { font-weight: 700; }
.units-toggle a { color: var(--accent); }

main {
  max-width: 72rem;
  margin: 2rem auto;
  padding: 0 1.5rem;
}

.search form {
  display: flex;
  gap: 0.5rem;
  max-width: 32rem;
}

input[type="text"] {
  flex: 1;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 1rem;
  background: var(--card);
  color: var(--fg);
}

#theme-toggle {
  margin-left: 1rem;
  padding: 0.2rem 0.6rem;
  font-size: 0.85rem;
  background: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--muted);
}

button {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 6px;
  background: var(--accent);
  color: #fff;
  font-size: 1rem;
  cursor: pointer;
}

.link-button {
  background: none;
  color: var(--muted);
  padding: 0 0.25rem;
}

.inline { display: inline; }

.favorites ul, .locations {
  list-style: none;
  padding: 0;
}

.favorites li, .locations li {
  padding: 0.35rem 0;
}

.coords { color: var(--muted); margin-left: 0.5rem; font-size: 0.9rem; }

.forecast-header {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.saved { color: var(--star); font-weight: 600; }

table.forecast {
  border-collapse: collapse;
  width: 100%;
  margin-top: 1rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

table.forecast th, table.forecast td {
  padding: 0.5rem 0.75rem;
  text-align: right;
  border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}

table.forecast td { font-family: var(--font-data); font-size: 0.95rem; }
table.forecast td.model { font-family: inherit; font-size: 1rem; }

table.forecast th.window {
  text-align: center;
  border-left: 2px solid var(--border);
  background: var(--thead);
}

table.forecast thead tr:last-child th {
  font-weight: 500;
  color: var(--muted);
  font-size: 0.85rem;
}

table.forecast td.model {
  text-align: left;
  font-weight: 600;
  white-space: nowrap;
}

table.forecast td:nth-child(8n+2) {
  border-left: 2px solid var(--border);
}

.footnote { color: var(--muted); font-size: 0.85rem; margin-top: 1rem; }

.alert-banner {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1rem;
  padding: 0.6rem 0.9rem;
  border-left: 4px solid var(--star);
  border-radius: 6px;
  background: rgba(217, 119, 6, 0.12);
  font-size: 0.92rem;
}
.alert-banner b { font-family: var(--font-data); }

.glance {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.8rem;
  margin-top: 1rem;
}

.tile {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.75rem 0.9rem;
}
.tile .t-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  margin-bottom: 0.3rem;
}
.tile .t-value {
  font-family: var(--font-data);
  font-size: 1.45rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.tile .t-value.t-hazard { font-size: 0.95rem; line-height: 1.35; }
.tile .t-sub { font-size: 0.76rem; color: var(--muted); margin-top: 0.2rem; }
.tile.hazard-alert { border-left: 4px solid var(--star); }
.tile.hazard-storm { border-left: 4px solid var(--accent); }

.tabbar { display: flex; gap: 0.25rem; border-bottom: 1px solid var(--border); margin-bottom: 0.9rem; }
.tabbar .tab {
  font: inherit;
  font-size: 0.9rem;
  padding: 0.45rem 0.9rem;
  color: var(--muted);
  background: none;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: 8px 8px 0 0;
  cursor: pointer;
}
.tabbar .tab.on {
  color: var(--fg);
  background: var(--bg);
  border-color: var(--border);
  font-weight: 600;
  position: relative;
  top: 1px;
}

.data-details {
  margin-top: 2rem;
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 0.2rem 1rem 0.6rem;
}
.data-details summary {
  cursor: pointer;
  padding: 0.6rem 0;
  color: var(--muted);
  font-size: 0.95rem;
}
.data-details[open] summary { color: var(--fg); font-weight: 600; }
.data-details .forecast { display: block; overflow-x: auto; }

.chart-section {
  margin-top: 2rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1rem 1.25rem 1.25rem;
}

.chart-section h2 {
  font-family: var(--display-font);
  font-size: var(--display-size);
  font-weight: var(--display-weight);
  text-transform: var(--display-case);
  letter-spacing: var(--display-track);
  margin: 0 0 0.75rem;
}
.chart-sub { text-transform: none; letter-spacing: 0; }
.chart-sub { font-weight: 400; color: var(--muted); font-size: 0.85rem; }

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-bottom: 0.5rem;
}

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: none;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 0.25rem 0.6rem;
  color: var(--fg);
  font-size: 0.85rem;
  cursor: pointer;
}

.legend-item:hover { background: var(--bg); }
.legend-item.selected {
  border-color: var(--border);
  background: var(--bg);
  font-weight: 700;
}

.legend-key {
  display: inline-flex;
  flex-direction: column;
  gap: 2px;
}

.key-bar {
  display: inline-block;
  width: 18px;
  height: 3px;
  border-radius: 2px;
}
.legend-item.selected .key-bar { height: 4px; }

.chart-plot { position: relative; overflow-x: auto; }

/* Per-day numbers under the temperature chart. Sits inside .chart-plot so it
   shares the chart's scroll and its columns stay under the days they describe;
   widths come from the chart's own x() and are set inline per render. */
.spread-strip {
  /* the plot's bottom margin is the gap: its date axis is suppressed when the
     strip is present, because the strip's header row is a better one */
  margin-top: 0;
  border-collapse: collapse;
  table-layout: fixed;
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  /* the section switches selection off on touch so press-and-hold inspect
     can't raise the text-selection UI over the chart — but these are numbers
     people copy, and the hold gesture lives on the plot, not down here */
  -webkit-user-select: text;
  user-select: text;
}
.spread-strip caption,
.spread-strip .ss-head span {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.spread-strip th,
.spread-strip td { padding: 0; text-align: center; font-weight: 400; }
.spread-strip thead tr { border-bottom: 1px solid var(--border); }
.spread-strip .ss-day {
  font-size: 11px;
  color: var(--muted);
  padding-bottom: 0.2rem;
}

/* the date is the day-view control; the whole column is its hit target */
.spread-strip.ss-drill tbody td,
.spread-strip.ss-drill .ss-day { cursor: pointer; }
.spread-strip .ss-daybtn {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  width: 100%;
  cursor: pointer;
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  text-decoration-color: var(--border);
}
.spread-strip .ss-daybtn:hover,
.spread-strip .ss-daybtn:focus-visible {
  color: var(--accent);
  text-decoration-color: currentColor;
}
.spread-strip .ss-daybtn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}
.spread-strip tbody td { padding-top: 0.22rem; }
.spread-strip .ss-group td { padding-top: 0.5rem; }
.spread-strip .ss-sel td { font-size: 12px; font-weight: 700; color: var(--fg); }
.spread-strip .ss-all td { font-size: 10px; color: var(--muted); }
.spread-strip .ss-tag {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--muted);
  text-align: right;
  padding-left: 0.5rem;
  white-space: nowrap;
  /* pinned to the pane's right edge: on a phone the table is wider than the
     pane, and a row label you have to scroll to the end to read is no label.
     Degrades to a plain trailing cell where sticky isn't honoured. */
  position: sticky;
  right: 0;
  background: var(--card);
}
/* Fade the scrolling numbers into the card just before the pinned labels. A
   half-scrolled cell would otherwise read as a COMPLETE short value — "93"
   is really "93°", and a clipped "103°" reads as "10" — which is worse than
   a truncated word. A box-shadow was the obvious try and is invisible in the
   dark theme (black on #191f28); fading toward --card works in both. */
.spread-strip .ss-tag::before {
  content: "";
  position: absolute;
  right: 100%;
  top: 0;
  bottom: 0;
  width: 26px;
  background: linear-gradient(to right, transparent, var(--card));
  pointer-events: none;
}

/* touch devices: press-and-hold on charts must not trigger the browser's
   text-selection/search UI on the card's labels and footnotes */
@media (pointer: coarse) {
  .chart-section:not(.discussion-card) {
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
  }
}

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

.hsearch { flex: 1; max-width: 18rem; margin-left: 1.25rem; }
.hsearch input {
  width: 100%;
  font-size: 0.85rem;
  padding: 0.35rem 0.7rem;
  background: var(--bg);
}

.page-nav {
  position: sticky;
  top: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.pn-favs { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.chip {
  font-size: 0.78rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--fg);
  text-decoration: none;
  background: var(--card);
  white-space: nowrap;
}
.chip.on { border-color: var(--accent); font-weight: 600; }

/* Reading-register toggle. `.chip` was written for anchors, so buttons need the
   font and cursor back — they inherit neither. Not a .lab-* class: this sits
   next to prose, and .lab-proto is user-select:none on coarse pointers. */
.mode-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: -0.35rem 0 0.85rem;
}
.mode-toggle .mode-label {
  font-size: 0.78rem;
  color: var(--muted);
  margin-right: 0.1rem;
}
.mode-toggle .chip {
  font-family: inherit;
  cursor: pointer;
}
.mode-toggle .chip:hover { border-color: var(--accent); }
.pn-links { display: flex; gap: 0.9rem; font-size: 0.85rem; }
.pn-links a { color: var(--muted); text-decoration: none; }
.pn-links a:hover { color: var(--accent); }

#accuracy { overflow-x: auto; }

@media (max-width: 700px) {
  header { flex-wrap: wrap; gap: 0.5rem; padding: 0.6rem 1rem; }
  .hsearch { order: 3; flex-basis: 100%; max-width: none; margin-left: 0; }
  main { margin: 1rem auto; padding: 0 0.9rem; }
  h1 { font-size: 1.35rem; }
  .page-nav { flex-wrap: wrap; gap: 0.5rem; }
  /* chips scroll in their own row so the More dropdown isn't clipped by
     a scrolling nav; the dropdown itself escapes via fixed positioning */
  .pn-favs { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; }
  .pn-more-list { position: fixed; left: 12px; right: 12px; top: auto; }
  .tile .t-value { font-size: 1.2rem; }
}

.legend-blend { cursor: default; }

.key-dash {
  display: inline-block;
  width: 18px;
  height: 0;
  border-top: 2.5px dashed var(--fg);
}

.tt-key-dash {
  display: inline-block;
  width: 14px;
  height: 0;
  border-top: 2px dashed var(--fg);
  flex: none;
}

.accuracy-status { color: var(--muted); }

/* The verdict sentences above the accuracy table. The chart section switches
   selection off on coarse pointers for chart gestures — this is prose people
   quote, so it opts back in (same reasoning as .spread-strip). */
.accuracy-verdict {
  max-width: 62ch;
  margin: 0.2rem 0 0.6rem;
  -webkit-user-select: text;
  user-select: text;
}

/* Waiting state: a radar sweep and a rotating forecaster phrase. The wait is
   real work (worksheet build, then generation), so the phrases name pipeline
   stages rather than filling space — see discussion.js for the list. */
.discussion-status {
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
/* The cold-location offer (roadmap Now #5(b)). Stacks rather than inheriting
   the status row's centred flex, because it is a sentence plus an action and
   not a spinner. Selectable at --fg: it explains why the page looks
   incomplete, so it is prose a reader may need, not chart furniture — and
   .chart-section kills selection on coarse pointers. */
.discussion-status:has(.discussion-generate) {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.7rem;
}
.discussion-cold-note {
  color: var(--fg);
  margin: 0;
  max-width: 56ch;
  user-select: text;
  -webkit-user-select: text;
}
.discussion-generate {
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.45rem 1rem;
  border-radius: 999px;
}

.disc-sweep {
  flex: none;
  width: 1.1rem;
  height: 1.1rem;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.4;
}

.disc-sweep circle { opacity: 0.3; }

.disc-sweep .sweep {
  stroke-linecap: round;
  transform-origin: 50% 50%;
  animation: disc-sweep 1.7s linear infinite;
}

@keyframes disc-sweep { to { transform: rotate(360deg); } }

.disc-phrase {
  text-transform: var(--display-case);
  animation: disc-phrase-in 0.45s ease;
}

.disc-phrase::after { content: "\2026"; }

@keyframes disc-phrase-in {
  from { opacity: 0; transform: translateY(0.15em); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .disc-sweep .sweep { animation: none; opacity: 0.75; }
  .disc-phrase { animation: none; }
}

.discussion-text {
  max-width: 46rem;
  line-height: 1.55;
}

.discussion-text h3 {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.9rem;
  letter-spacing: 0.02em;
  margin: 1.1rem 0 0.35rem;
}

.discussion-text p { margin: 0.35rem 0 0.75rem; }

.dref {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--accent);
  text-decoration: underline dotted;
  text-underline-offset: 3px;
  cursor: pointer;
}

.daycard-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 20, 30, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.daycard {
  position: relative;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
  padding: 0;
  max-width: 34rem;
  width: calc(100% - 3rem);
  /* never taller than the viewport: fixed header, scrolling body */
  max-height: min(88vh, 100dvh - 2rem);
  display: flex;
  flex-direction: column;
}

.daycard-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.75rem 1.4rem 0.55rem;
  border-bottom: 1px solid var(--border);
}
.daycard-head h4 { margin: 0; font-size: 1rem; }

.daycard-body {
  overflow-y: auto;
  padding: 0.85rem 1.4rem 1.1rem;
}

/* desktop: use the abundant horizontal space — facts beside the
   meteogram, which usually removes the need to scroll at all */
@media (min-width: 1000px) {
  .daycard { max-width: 60rem; }
  .daycard-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
    gap: 0 1.4rem;
    align-items: start;
  }
  .daycard-body .meteo {
    border-top: none;
    border-left: 1px solid var(--border);
    margin-top: 0;
    padding-top: 0;
    padding-left: 1.4rem;
  }
}

/* mobile: bottom sheet — full width, anchored, back-of-thumb close */
@media (max-width: 700px) {
  .daycard-backdrop { align-items: flex-end; }
  .daycard {
    max-width: none;
    width: 100%;
    max-height: 92dvh;
    border-radius: 14px 14px 0 0;
    border-left: none;
    border-right: none;
    border-bottom: none;
  }
}

.daycard dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.3rem 0.9rem;
  margin: 0;
  font-size: 0.9rem;
}

.daycard dt { color: var(--muted); }
.daycard dd { margin: 0; font-variant-numeric: tabular-nums; }

.daycard-close {
  flex: none;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 1rem;
  cursor: pointer;
  padding: 0.25rem;
}

table.accuracy {
  border-collapse: collapse;
  margin-top: 0.25rem;
  background: var(--card);
}

table.accuracy th, table.accuracy td {
  padding: 0.45rem 1rem;
  text-align: right;
  border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}

table.accuracy td { font-family: var(--font-data); font-size: 0.95rem; }
table.accuracy td.model { font-family: inherit; }

.chart-plot svg text { font-family: var(--font-data); }
.tt-val, .daycard dd { font-family: var(--font-data); }

table.accuracy th.lead {
  text-align: center;
  border-left: 2px solid var(--border);
  background: var(--thead);
}

table.accuracy thead tr:last-child th {
  font-weight: 500;
  color: var(--muted);
  font-size: 0.85rem;
}

table.accuracy td.model {
  text-align: left;
  font-weight: 600;
  white-space: nowrap;
}

table.accuracy td:nth-child(2n) {
  border-left: 2px solid var(--border);
}

.panel-label {
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 600;
  margin: 0.75rem 0 0.25rem;
}

.chart-tooltip {
  position: absolute;
  z-index: 10;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 2px 8px var(--shadow);
  padding: 0.5rem 0.65rem;
  font-size: 0.85rem;
  pointer-events: none;
  min-width: 11rem;
}

.tt-date { color: var(--muted); margin-bottom: 0.3rem; }

.tt-row {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.1rem 0;
}

.tt-key {
  display: inline-block;
  width: 14px;
  height: 3px;
  border-radius: 2px;
  flex: none;
}

.tt-val {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  min-width: 2.2rem;
}

.tt-name { color: var(--muted); }

/* ===== Model Lab ===== */
/* camp palette: regime character, not model identity — validated per theme
   (light "other" = gold, dark = teal; dark's band is too tight for gold) */
:root {
  --camp-warm: #c73a12; --camp-cool: #2563eb; --camp-other: #ca8a04;
  --camp-slate: #647082; --camp-olabel: #1c2430; --ens: #2563eb; --ens-op: 0.10;
}
@media (prefers-color-scheme: dark) {
  :root {
    --camp-warm: #cf4c18; --camp-cool: #3a70e2; --camp-other: #0caf83;
    --camp-slate: #8b98a9; --camp-olabel: #12161d; --ens: #3a70e2; --ens-op: 0.18;
  }
}
:root[data-theme="dark"] {
  --camp-warm: #cf4c18; --camp-cool: #3a70e2; --camp-other: #0caf83;
  --camp-slate: #8b98a9; --camp-olabel: #12161d; --ens: #3a70e2; --ens-op: 0.18;
}
:root[data-theme="light"] {
  --camp-warm: #c73a12; --camp-cool: #2563eb; --camp-other: #ca8a04;
  --camp-slate: #647082; --camp-olabel: #1c2430; --ens: #2563eb; --ens-op: 0.10;
}

/* primary surface tabs — first in the nav, deliberately louder than the
   favorites chips beside them */
.pn-tabs { display: flex; gap: 6px; flex: none; }
.pn-tabs a, .pn-tabs .on {
  font-family: var(--display-font); text-transform: var(--display-case);
  letter-spacing: var(--display-track); font-weight: var(--display-weight);
  font-size: 0.9rem; padding: 6px 16px; border-radius: 9px;
  border: 1px solid var(--border); text-decoration: none;
  color: var(--muted);
}
.pn-tabs .on { color: var(--accent); border-color: var(--accent); font-weight: 700; }
.pn-tabs a:hover { border-color: var(--accent); color: var(--accent); }

/* The tabs never got the phone treatment the chips did. `flex: none` sizes
   the row to max-content and refuses to shrink, so `flex-wrap` alone can
   never engage — the children have all the room they asked for while the
   row hangs 15px past a 390px viewport and the whole page scrolls
   sideways. In DARK only, where --display-case uppercases the labels and
   "VERIFICATION" is the widest thing on the page.

   Wrap rather than scroll: three tabs is a fixed set and all three should
   stay visible, where a scrolling row hides the third behind a swipe. The
   tighter padding keeps them on one row at the common widths, so the wrap
   is a backstop for the narrowest phones rather than the usual case.

   This block sits AFTER the base rules on purpose. The same overrides in
   the earlier @media block silently lost the padding to `.pn-tabs a`
   further down the file — media queries add no specificity, so source
   order decides. */
@media (max-width: 700px) {
  .pn-tabs { flex: 1 1 auto; min-width: 0; flex-wrap: wrap; }
  .pn-tabs a, .pn-tabs .on { padding: 6px 10px; }
}

.pn-more { position: relative; }
.pn-more summary { list-style: none; cursor: pointer; display: inline-block; }
.pn-more summary::-webkit-details-marker { display: none; }
.pn-more-list {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 70;
  display: flex; flex-direction: column; gap: 6px; min-width: 190px;
  background: var(--card); border: 1px solid var(--border);
  border-radius: 10px; padding: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
}

.lab-caption { color: var(--muted); font-size: 0.82rem; margin: 0 0 10px; max-width: 76ch; }
.lab-intro { margin: 6px 0 14px; }
.lab-legend { display: flex; flex-wrap: wrap; gap: 12px 22px; align-items: center;
              margin: 0 0 6px; font-size: 0.82rem; color: var(--muted); }
.lab .sw { display: inline-block; width: 12px; height: 12px; border-radius: 50%;
           vertical-align: -2px; margin-right: 6px; }
.sw-warm { background: var(--camp-warm); } .sw-cool { background: var(--camp-cool); }
.sw-other { background: var(--camp-other); } .sw-slate { background: var(--camp-slate); }
.lab-size-note { margin-left: auto; }
.lab-model-sel { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
                 margin: 8px 0 4px; font-size: 0.8rem; color: var(--muted); }
.lab-model-sel button {
  font: inherit; font-size: 0.75rem; color: var(--fg, #1c2430); background: var(--card, transparent);
  border: 1px solid var(--border); border-radius: 999px; padding: 3px 12px; cursor: pointer;
}
.lab-model-sel button:hover { border-color: var(--accent); }
.lab-model-sel button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); font-weight: 650; }

.lab-proto { margin: 18px 0; }
.lab-proto h2 .tag { color: var(--accent); margin-right: 8px; }
.lab-replay {
  font: inherit; font-size: 0.72rem; color: var(--muted); background: transparent;
  border: 1px solid var(--border); border-radius: 6px; padding: 2px 10px;
  cursor: pointer; margin-left: 10px; vertical-align: 2px;
}
.lab-replay:hover { border-color: var(--accent); color: var(--accent); }
.lab .chart-pane { overflow-x: auto; }
.lab .chart-pane svg { display: block; min-width: 760px; width: 100%; height: auto; }
.lab svg text { font-family: var(--font-data); }
.lab .axis text { fill: var(--muted); font-size: 11px; }
.lab .gridline { stroke: var(--border); stroke-width: 1; }
.lab .halo { stroke: var(--card, #fff); stroke-width: 3.5px; paint-order: stroke; stroke-linejoin: round; }
.lab .cell-hit { fill: transparent; cursor: pointer; }
.lab .col-sel { fill: var(--accent); opacity: 0.07; }
.lab .col-sel-bar { fill: var(--accent); }
.lab .bub-warm { fill: var(--camp-warm); } .lab .bub-cool { fill: var(--camp-cool); }
.lab .bub-other { fill: var(--camp-other); } .lab .bub-slate { fill: var(--camp-slate); }
.lab .bub { stroke: var(--card, #fff); stroke-width: 2; }
.lab .cnt { fill: #fff; font-size: 10px; font-weight: 650; text-anchor: middle; pointer-events: none; }
.lab .cnt-other { fill: var(--camp-olabel); }
.lab .conn { stroke: var(--muted); stroke-opacity: 0.4; stroke-width: 1.5; fill: none; }
.lab .ens-band { fill: var(--ens); opacity: var(--ens-op); }
.lab .ens-med { stroke: var(--ens); stroke-opacity: 0.5; stroke-width: 1.2; stroke-dasharray: 2 4; fill: none; }
.lab .strip-text { fill: var(--muted); font-size: 10px; text-anchor: middle; }
.lab .strip-narrow { fill: var(--fg, #1c2430); }
.lab .ribbon-warm { fill: var(--camp-warm); } .lab .ribbon-cool { fill: var(--camp-cool); }
.lab .ribbon-other { fill: var(--camp-other); } .lab .ribbon-slate { fill: var(--camp-slate); }
.lab .raw-dot { fill: var(--muted); opacity: 0.55; }
/* a model that didn't move between steps: present, but visibly not fresh */
.lab .raw-dot.carried { opacity: 0.2; }
/* an archiver gap — a window with no line at all, never fused shut */
.lab .axis-hole { stroke: var(--border); stroke-width: 1.5; stroke-dasharray: 2 5; }
.lab .event-flag line { stroke: var(--muted); stroke-dasharray: 3 3; }
.lab .event-flag text { fill: var(--fg, #1c2430); font-size: 10px; }
.lab-events { font-size: 0.82rem; color: var(--muted); margin: 8px 0 0; }
.lab-events b { color: var(--fg, #1c2430); font-weight: 600; }

.lab .pop { transform-box: fill-box; transform-origin: center; transform: scale(0);
            transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) var(--d, 0s); }
.lab .in .pop { transform: scale(1); }
.lab .fdim { opacity: 0; transition: opacity 0.5s ease var(--d, 0s); }
.lab .in .fdim { opacity: var(--o, 1); }
.lab .follow-under { fill: none; stroke: var(--fg, #1c2430); stroke-width: 2; opacity: 0.3; }
.lab .follow-flow { fill: none; stroke: var(--fg, #1c2430); stroke-width: 2.2; stroke-linecap: round;
                    stroke-dasharray: 7 9; animation: labflow 0.55s linear infinite; }
@keyframes labflow { to { stroke-dashoffset: -16; } }
.lab .follow-dot { fill: var(--fg, #1c2430); stroke: var(--card, #fff); stroke-width: 1.5; }
.lab .bub.followed { stroke: var(--fg, #1c2430); stroke-width: 2.5; }
@media (prefers-reduced-motion: reduce) {
  .lab .pop, .lab .fdim { transition: none; }
  .lab .fdim { opacity: var(--o, 1); }
  .lab .pop { transform: scale(1); }
  .lab .follow-flow { animation: none; }
}
@media (pointer: coarse) {
  .lab-proto { user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
}

#lab-tip {
  position: absolute; pointer-events: none; z-index: 10;
  background: var(--card, #fff); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 10px; font-size: 0.78rem; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
  max-width: 260px;
}
#lab-tip .lt-date { font-weight: 650; margin-bottom: 4px; }
#lab-tip .lt-line { display: flex; gap: 6px; align-items: baseline; }
#lab-tip .lt-line .sw { width: 8px; height: 8px; flex: none; border-radius: 50%; align-self: center;
                        display: inline-block; }
#lab-tip .muted { color: var(--muted); }

.regime-pulse { color: var(--muted); font-size: 0.85rem; margin: 4px 0 10px; }
.regime-pulse a { color: var(--accent); text-decoration: none; }

/* ===== Verification page ===== */
.vf-window { font-size: 0.8rem; color: var(--muted); }
.vf-window a { color: var(--accent); text-decoration: none; margin-left: 8px; }
.vf-window .on { color: var(--fg); font-weight: 650; margin-left: 8px; }
.vf-leads { display: flex; gap: 6px; margin: 0 0 8px; }
.vf-leads button { font: inherit; font-size: 0.75rem; color: var(--fg); background: var(--card);
  border: 1px solid var(--border); border-radius: 999px; padding: 3px 12px; cursor: pointer; }
.vf-leads button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); font-weight: 650; }
.vf-zero { stroke: var(--fg); stroke-width: 1.2; opacity: 0.5; }
.vf-line { fill: none; stroke: var(--muted); stroke-width: 1.6; opacity: 0.55; cursor: pointer; }
svg.hovering .vf-line { opacity: 0.15; }
svg.hovering .vf-line.hot { stroke: var(--fg); stroke-width: 2.6; opacity: 1; }
.vf-label { fill: var(--muted); font-size: 10.5px; cursor: pointer; }
svg.hovering .vf-label { opacity: 0.25; }
svg.hovering .vf-label.hot { fill: var(--fg); opacity: 1; font-weight: 650; }
.vf-table { border-collapse: collapse; font-size: 0.82rem; margin: 4px 0 8px; }
.vf-table th, .vf-table td { padding: 4px 12px; text-align: right; border-bottom: 1px solid var(--border); }
.vf-table th:first-child, .vf-table td:first-child { text-align: left; }
/* These are numbers someone may want out of the page, and .lab-proto turns
   selection off on coarse pointers so a chart drag can't raise the selection
   UI. Opt the tables back in — they are not chart furniture. */
.vf-table { user-select: text; -webkit-user-select: text; }
.vf-card { margin: 6px 0; }
.vf-card summary { cursor: pointer; font-weight: 600; font-size: 0.9rem; padding: 4px 0; }

/* Scorecard: every model at one lead, in the ladder's order, so the rows read
   against the rungs above them. One tbody per lead ships and verify.js hides
   all but the selected one; with scripting off they all stay, each under its
   own lead heading, which is why the heading is markup and not a JS caption. */
.vf-scroll { overflow-x: auto; }
/* Shrink-wrapped, not stretched to the pane: at 1440 a full-width table puts
   800px between a model's name and its scatter, and the row is the thing you
   are meant to read across. */
.vf-scorecard th, .vf-scorecard td { padding: 4px 9px; }
/* "0.52 / 0.9" is one value, not two — wrapped across two lines it reads as
   a column boundary and makes every row a different height. Scroll instead.
   Spanning cells (lead headings, the empty-state row) are prose and wrap. */
.vf-scorecard td { white-space: nowrap; }
.vf-scorecard td[colspan] { white-space: normal; }
.vf-scorecard tbody[hidden] { display: none; }
.vf-scorecard thead th { font-weight: 600; white-space: nowrap; }
.vf-scorecard thead th[colspan] { text-align: center; }
.vf-scorecard thead tr:last-child th { font-weight: 500; color: var(--muted); font-size: 0.76rem; }
.vf-scorecard .vf-split { border-left: 1px solid var(--border); }
.vf-leadrow td { font-size: 0.74rem; font-weight: 650; letter-spacing: 0.05em;
  text-transform: var(--display-case, none); color: var(--muted);
  padding-top: 12px; border-bottom: none; }
/* The model name stays put when the stat columns scroll under it on a phone;
   the gradient says "there is more to the right" without a shadow, which is
   invisible on the dark theme (black on #191f28 does nothing).
   Keyed on .vf-model, NOT :first-child — the calibration table spans its model
   cell over that model's probability bins, so in a continuation row the first
   td is "Model says" and :first-child would pin the wrong column. */
.vf-scorecard .vf-model {
  position: sticky; left: 0; z-index: 1; background: var(--bg);
  white-space: nowrap; text-align: left; }
.vf-scorecard .vf-model::after {
  content: ""; position: absolute; left: 100%; top: 0; bottom: 0; width: 18px;
  background: linear-gradient(to right, var(--bg), transparent); pointer-events: none; }
/* Only the calibration table spans its model cell, and only there can the
   label carry two model names. Let it wrap and cap it: nowrap would pin a
   230px sticky column in front of a 390px phone, leaving no room for the
   numbers it is labelling. */
.vf-scorecard td.vf-model[rowspan] {
  vertical-align: top; white-space: normal; max-width: 11rem; }
.vf-calib-note { margin-top: 14px; }
.vf-shared { font-size: 0.7rem; font-weight: 500; color: var(--muted);
  border: 1px solid var(--border); border-radius: 999px; padding: 0 6px;
  margin-left: 4px; white-space: nowrap; }
.vf-hit { fill: none; stroke: transparent; stroke-width: 12; cursor: pointer; }
.vfl-name { fill: var(--fg); font-size: 11.5px; }
.vfl-name.vfl-blend { font-weight: 700; }
.vfl-whisker { stroke: var(--muted); stroke-width: 3; stroke-linecap: round; opacity: 0.45; }
.vfl-dot { stroke: var(--card, #fff); stroke-width: 2; }
.vfl-warm { fill: var(--camp-warm); }
.vfl-cool { fill: var(--camp-cool); }
.vfl-neutral { fill: var(--camp-slate); }
.lab .climo-line { fill: none; stroke: var(--muted); stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.8; }
.lab .climo-label { fill: var(--muted); font-size: 10px; }

/* day-card hourly meteogram */
.meteo { margin-top: 0.8rem; border-top: 1px solid var(--border); padding-top: 0.7rem; }
.meteo-title { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); margin-bottom: 0.4rem; }
.meteo-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 0.5rem; }
.meteo-chips button {
  font: inherit; font-size: 0.72rem; padding: 2px 9px; border-radius: 999px;
  background: none; color: var(--fg); border: 1.5px solid var(--border); cursor: pointer;
}
.meteo-svg { width: 100%; height: auto; display: block; }
.meteo-svg .m-cap { fill: var(--muted); font-size: 9.5px; }
.meteo-svg .m-tick { fill: var(--muted); font-size: 9px; }
.meteo-svg .m-grid { stroke: var(--border); stroke-width: 1; }
.meteo-svg .m-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

.daycard-head .dc-group { display: flex; align-items: center; gap: 0.15rem; min-width: 0; }
.daycard-arrow {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 1.15rem; line-height: 1; padding: 0.1rem 0.5rem; border-radius: 6px;
}
.daycard-arrow:hover:not(:disabled) { color: var(--accent); }
.daycard-arrow:disabled { opacity: 0.25; cursor: default; }

/* /day page */
.day-nav { display: flex; align-items: center; gap: 0.35rem; margin: 0.3rem 0 1rem; }
.day-nav .day-date { font-family: var(--font-data); font-weight: 650; font-size: 1.05rem; }
.day-nav .day-crumb { color: var(--muted); font-size: 0.82rem; margin-left: 0.6rem; }
.day-nav .day-crumb a { color: var(--accent); text-decoration: none; }
.day-facts dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.3rem 0.9rem;
  margin: 0;
  font-size: 0.9rem;
}
.day-facts dl dt { color: var(--muted); }
.day-facts dl dd { margin: 0; font-family: var(--font-data); }

.site-footer {
  margin: 2.5rem 0 1rem;
  padding: 0.8rem 1.5rem 0;
  border-top: 1px solid var(--border, rgba(128, 128, 128, 0.25));
  color: var(--muted);
  font-size: 0.8rem;
}
.site-footer a { color: var(--muted); }

.changes {
  margin: 1rem 0;
  border: 1px solid var(--border, rgba(128, 128, 128, 0.25));
  border-radius: 8px;
  padding: 0.4rem 0.9rem;
}
.changes summary {
  cursor: pointer;
  font-weight: 600;
  padding: 0.3rem 0;
}
.changes-badge {
  display: inline-block;
  min-width: 1.4em;
  text-align: center;
  margin-left: 0.4em;
  padding: 0.05em 0.4em;
  border-radius: 999px;
  background: var(--accent, #c73a12);
  color: #fff;
  font-size: 0.75rem;
}
.changes-list {
  list-style: none;
  margin: 0.4rem 0 0.5rem;
  padding: 0;
}
.change-item {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.15rem 0.8rem;
  padding: 0.45rem 0.6rem;
  border-left: 3px solid transparent;
}
.change-item.notable { border-left-color: var(--accent, #c73a12); }
.change-item.linked { cursor: pointer; }
.change-item.linked:hover { background: rgba(128, 128, 128, 0.08); }
.change-when {
  color: var(--muted);
  font-size: 0.75rem;
  font-family: var(--font-data);
  grid-row: span 2;
  align-self: start;
  padding-top: 0.15rem;
}
.change-detail { color: var(--muted); font-size: 0.85rem; grid-column: 2; }

.daycard-full { text-decoration: none; font-weight: 600; }

.hero { text-align: center; margin: 2.2rem auto 1.6rem; max-width: 44rem; }
.hero .tagline {
  font-size: 1.9rem;
  margin: 0 0 0.4rem;
  letter-spacing: -0.01em;
}
.hero-sub { color: var(--muted); margin: 0 0 1.3rem; }
button.ghost {
  background: none;
  border: 1px solid var(--border);
  color: var(--accent);
}

/* --- hero picker: one place, two verbs -------------------------------
   The product has two jobs, and a watcher checks the forecast far more
   often than they pin a day — so Forecast and Watch are peers, styled
   identically. What tells them apart is the date input bound to Watch,
   not visual rank. Two place fields used to sit here, which was just
   the same question asked twice. */
.hero-pick {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.65rem;
}
.pick-place { position: relative; width: min(26rem, 100%); }
.pick-place input {
  width: 100%;
  box-sizing: border-box;
  padding-right: 2.5rem;      /* clears the locate target */
}
.pick-locate {
  position: absolute;
  right: 0.3rem;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: 0;
  border-radius: 6px;
  padding: 0.3rem;
  line-height: 0;
  color: var(--muted);
  cursor: pointer;
}
.pick-locate svg {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
}
.pick-locate:hover, .pick-locate:focus-visible { color: var(--accent); }
.hero-pick.locating .pick-locate { opacity: 0.45; }
/* a located field holds a position, not a query — say so quietly */
.hero-pick.located .pick-place input { color: var(--accent); }

/* proximity does the grouping: the date sits tight against Watch, and the
   gap to Forecast is wide enough that the pairing reads without chrome */
.pick-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.4rem 1.5rem;
}
.pick-watch-group { display: inline-flex; align-items: center; gap: 0.4rem; }
.pick-btn { font-size: 0.95rem; padding: 0.5rem 1.1rem; white-space: nowrap; }
#pick-date {
  font: inherit;
  font-size: 0.9rem;
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.42rem 0.5rem;
}
/* The only cue for where Enter goes, and it appears exactly when the
   answer changes. A ring, never a fill change — a fill change would rank
   two actions that are meant to read as equals. */
.hero-pick.dated .pick-watch {
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
}

@media (max-width: 560px) {
  .pick-actions {
    flex-direction: column;
    align-items: stretch;
    width: min(26rem, 100%);
    gap: 0.5rem;
  }
  .pick-btn { width: 100%; }
  /* Date above Watch rather than beside it, so both buttons stay exactly
     the same size. Sharing a row leaves Watch visibly narrower than
     Forecast (measured 219px vs 361px at 390px wide), and unequal buttons
     read as a ranking — the one thing this layout must not do. Stacking
     keeps the date adjacent to the verb it belongs to. */
  .pick-watch-group { display: flex; flex-direction: column; gap: 0.5rem; }
  .pick-watch-group #pick-date { width: 100%; box-sizing: border-box; }
}

/* --- home sections ---------------------------------------------------
   Watched dates and places were unlabelled sibling grids, and the watched
   grid disappeared entirely when empty. Both are named; the empty one now
   says what a watch is for. */
.home-sec { margin-top: 1.9rem; }
.home-sec-head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.35rem;
}
.home-sec-head h2 {
  margin: 0;
  font-size: 1.02rem;
  font-family: var(--display-font);
  text-transform: var(--display-case);
  letter-spacing: var(--display-track);
  font-weight: var(--display-weight);
}
.home-sec-sub { color: var(--muted); font-size: 0.8rem; }
.home-empty {
  color: var(--muted);
  font-size: 0.9rem;
  margin: 0.9rem 0 0;
  max-width: 62ch;
  border-left: 3px solid var(--border);
  padding-left: 0.85rem;
  line-height: 1.55;
}
.home-empty b { color: var(--fg); }

.home-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: 0.9rem;
  margin-top: 1rem;
}
.wxcard {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.85rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.wxcard:hover { border-color: var(--accent); }
/* Cards carrying an animated scene: the scene needs vertical room, and
   short cards in a row would otherwise crop it to a strip. The canvas and
   scrim are injected by wx-backgrounds.js and sit behind the content.

   Text over a moving scene needs more weight than text on a flat card:
   measured, --muted at 11px clears only 5.65:1 (dark) / 5.02:1 (light) on
   the plain card, so ANY scene pushes it under AA. Scrim alone can't buy
   it back — at the strength it would take, the scene is gone and the
   ratio still misses. So the tokens lift, scoped to scened cards only. */
.wxcard[data-wx-scene] {
  min-height: 13rem;
  /* Dark scened cards run the card's own --fg for BOTH tokens. That looks
     like it throws away hierarchy, and it buys the thing the scenes needed
     most: the AA ceiling on how bright the background may get is set by the
     dimmest text over it, so lifting these from #b9c4d1 to #dce4ee raises
     that ceiling 0.082 -> 0.132 — 60% more light to spend on the scene.
     Hierarchy moves to size and weight, which the card already varies. */
  --muted: #dce4ee;
  --accent: #b8e6ff;
}
:root[data-theme="light"] .wxcard[data-wx-scene] {
  --muted: #2f3742;
  --accent: #0e3475;
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .wxcard[data-wx-scene] {
    --muted: #2f3742;
    --accent: #0e3475;
  }
}
.wxcard-head { display: flex; justify-content: space-between; align-items: baseline; }
.wxcard-name { font-weight: 700; text-decoration: none; font-size: 1.05rem; }
.wxcard-now { font-family: var(--font-data); font-size: 1.6rem; font-weight: 600; }
.wxcard-unit { color: var(--muted); font-size: 0.8rem; margin-left: 0.35rem; font-weight: 400; }
.wxcard-rain { color: var(--muted); font-size: 0.95rem; margin-left: 0.7rem; font-weight: 400; }
.wxcard-spread { font-size: 0.87rem; }
/* Divergence line: --fg, and deliberately NOT a dimmer "no news" variant for
   the quiet case. Two reasons, both from the scene work above. The card's AA
   ceiling is set by the dimmest text drawn over the scene, so a new dimmer tier
   would cost scene brightness for decoration. And on scened cards --muted is
   lifted to equal --fg precisely to raise that ceiling, so a colour-based quiet
   tier renders identically on every card that has a scene — an implied
   distinction nobody sees. The words carry it instead. */
.wxcard-diverge { font-size: 0.83rem; }
.wxcard-event {
  font-size: 0.83rem;
  text-decoration: none;
  border-left: 3px solid var(--accent);
  padding-left: 0.5rem;
}
.wxcard-event.notable { border-left-color: var(--star); }
.wxcard-age { color: var(--muted); font-size: 0.72rem; }
.wxcard-empty .wxcard-spread { color: var(--muted); }

.wxicon { width: 46px; height: 46px; display: block; }
.wxcard-now { display: flex; align-items: center; gap: 0.4rem; }
.tile { position: relative; }
.t-icon { position: absolute; top: 0.35rem; right: 0.5rem; }
.t-icon .wxicon { width: 44px; height: 44px; }

.wxcard-precip { font-size: 0.8rem; color: var(--muted); }

/* --- watch a date ------------------------------------------------- */
.watch-sub { color: var(--muted); display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.watch-status { font-size: 0.72rem; border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.watch-status-active,
.watch-status-upcoming { color: var(--accent); border-color: var(--accent); }
.watch-status-verified { color: var(--ok, #15803d); border-color: var(--ok, #15803d); }

/* Share action (roadmap `share-affordance`). It sits in .watch-header rather
   than in a .chart-section deliberately: those are user-select:none on coarse
   pointers, which is what made the old "copy this page's URL" footnote
   impossible to act on precisely where sharing happens. */
/* Its own row in the header's flex, not trailing the status pill. Outlined and
   adjacent, it read as a second badge beside UPCOMING — same shape, same
   accent border, one interactive and one not. Caught in a screenshot; every
   assertion passed either way. */
.watch-share { display: flex; align-items: baseline; gap: 10px;
  flex-wrap: wrap; flex-basis: 100%; margin-top: 10px; }
/* Filled, so the control is not the same object as the state label beside it.
   `--card` is the text colour because it inverts with the theme exactly as
   needed: white on the light accent (#2563eb, ~5.2:1) and near-black on the
   dark one (#4cc2ff, ~11:1). A fixed white would fail AA in dark. */
.ws-btn {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--card);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 4px 14px;
  cursor: pointer;
}
.ws-btn:hover { filter: brightness(1.08); }
/* The no-JS baseline and the last-resort reveal. It must stay selectable:
   this is the one string on the page a reader may need to copy by hand, and
   it can be put back inside a coarse-pointer context by reveal(). */
.ws-url {
  color: var(--muted);
  font-size: 0.78rem;
  word-break: break-all;
  -webkit-user-select: text;
  user-select: text;
}
.ws-said { color: var(--muted); font-size: 0.78rem; }

/* The panel's spark. Same marks as the card's, at a size that can carry a
   caption naming them — and its own viewBox, because `preserveAspectRatio:
   none` across this width would stretch the card's 240x40 into ellipses. */
.watch-spark { margin-top: 10px; }
/* min-width:0 is what keeps this off the phone's horizontal scrollbar: a
   flex/grid child defaults to min-content width, and an SVG at width:100%
   inside one refuses to shrink below its intrinsic size. */
.watch-spark-plot { position: relative; min-width: 0; }
/* aspect-ratio, not a fixed height: with preserveAspectRatio:none a fixed
   200px against a phone's ~360px width scales x by 0.4 and y by 1.0, which
   squeezes every marker circle into a narrow ellipse — the distortion
   `watch_track.geometry` warns about, arriving through the CSS instead of
   the viewBox. Matching the box to the viewBox makes the scale uniform. */
.watch-spark-plot svg {
  width: 100%; aspect-ratio: 900 / 200; height: auto;
  display: block; min-width: 0;
}
.watch-spark-y {
  position: absolute; left: 0; font-size: 0.72rem; color: var(--muted);
  background: var(--card); padding: 0 3px; pointer-events: none;
}
.watch-spark-hi { top: 0; }
.watch-spark-lo { bottom: 0; }
.watch-spark-dates {
  display: flex; justify-content: space-between;
  font-size: 0.72rem; color: var(--muted); margin-top: 2px;
}
.watch-spark .lab-caption { margin-top: 4px; }
/* the criteria does not move and stays quiet; the verdict is the answer */
.watch-outcome { margin-top: 10px; font-size: 0.95rem; }
.watch-outcome-criteria { color: var(--muted); }
.watch-outcome-verdict { color: var(--fg); font-weight: 600; }
/* Best estimate: two stat columns, temp and rain at equal weight, each
   owning the qualifiers that describe it. The qualifiers used to be bare
   .footnote spans in one flex row — and .footnote carries margin-top:1rem
   for its block-level use, which shoved them below the centre line while
   the rain span stayed put. Blocks inside a column, not flex items. */
.watch-est-row { display: flex; gap: 28px; align-items: stretch; flex-wrap: wrap; }
.watch-stat { display: flex; flex-direction: column; gap: 6px; }
.watch-stat-rain { border-left: 1px solid var(--border); padding-left: 28px; }
/* min-height matches the icon: without it the temp column's 44px icon
   makes its value row taller than the rain column's text glyph, and the
   two columns' notes start at different heights */
.watch-stat-value { display: flex; align-items: center; gap: 10px; min-height: 44px; }
.watch-stat .wxicon { width: 44px; height: 44px; }
.watch-big { font-size: 1.7rem; font-weight: 650; font-family: var(--font-data); line-height: 1; }
.watch-rain-glyph { font-size: 1.5rem; color: var(--muted); line-height: 1; }
.watch-stat-notes { display: flex; flex-direction: column; gap: 2px; color: var(--muted); font-size: 0.85rem; }
@media (max-width: 560px) {
  .watch-est-row { gap: 16px; }
  .watch-stat-rain { border-left: 0; padding-left: 0; border-top: 1px solid var(--border); padding-top: 16px; width: 100%; }
}

/* Confidence across three axes: temperature, rain, and the user's alert
   line. One word per axis — a single headline implied one confidence for
   the whole day, which is exactly what it is not. */
.watch-axes { margin: 0 0 4px; display: flex; flex-direction: column; gap: 12px; }
.watch-axis { display: grid; grid-template-columns: 8.5rem 1fr; gap: 4px 16px; align-items: baseline; }
.watch-axis dt { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; padding-top: 0.15rem; }
.watch-axis dd { margin: 0; display: flex; flex-direction: column; gap: 3px; }
.watch-axis-note { color: var(--muted); font-size: 0.82rem; max-width: 76ch; }
@media (max-width: 560px) {
  .watch-axis { grid-template-columns: 1fr; gap: 2px; }
}
.watch-word { font-size: 1.05rem; font-weight: 650; text-transform: uppercase; letter-spacing: 0.04em; }
.watch-word-settled { color: var(--ok, #15803d); }
.watch-word-leaning { color: var(--accent); }
.watch-word-genuinely-uncertain { color: var(--warm, #c73a12); }
/* trigger proximity reads on its own scale: tripped is the loud one */
.watch-word-trigger-tripped { color: var(--warm, #c73a12); }
.watch-word-trigger-within-range { color: var(--accent); }
.watch-word-trigger-far-off { color: var(--muted); }
.watch-verdict { font-weight: 600; margin: 8px 0 0; }
.watch-form { display: flex; flex-direction: column; gap: 10px; max-width: 440px; margin-top: 8px; }
.watch-form label { display: flex; flex-direction: column; gap: 4px; color: var(--muted); font-size: 0.85rem; }
.watch-form-row { flex-direction: row; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 0; }
/* the editable condition is its own object, not loose text in the card */
.watch-trigger-set {
  display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
  border: 1px solid var(--border); border-radius: 8px;
  padding: 12px 14px; margin: 12px 0; max-width: max-content;
}
.watch-trigger-set .watch-form { margin-top: 0; max-width: none; }
.watch-trigger-set input[type="number"] { width: 5.5rem; }
/* --surface/--ink were never defined tokens, so these two declarations
   resolved to the UA default — a white field with black text, which in
   dark mode was a pair of glaring boxes on the watch-creation form. The
   real tokens are --card/--fg, matching the global input[type=text] rule
   this selector outranks by source order. */
.watch-form input, .watch-form select { font: inherit; background: var(--card); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; }
.watch-btn { font: inherit; background: var(--accent); color: var(--bg); border: 0; border-radius: 6px; padding: 7px 16px; cursor: pointer; align-self: flex-start; }
.watch-events { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
.watch-events li.notable { font-weight: 600; }
.watch-cta { margin-left: auto; color: var(--accent); text-decoration: none; font-size: 0.85rem; white-space: nowrap; }
/* the forecast header's copy carries a border so it reads as an action
   next to "Save to favorites" rather than as another crumb */
.watch-cta-btn { border: 1px solid var(--accent); border-radius: 6px; padding: 0.35rem 0.75rem; }
.watch-cta-btn:hover { background: var(--accent); color: var(--bg); }

.watch-new-place { font-size: 1.02rem; font-weight: 600; margin: 0 0 0.55rem; }
.watch-new-redo { font-size: 0.8rem; font-weight: 400; margin-left: 0.5rem; color: var(--muted); }
.watch-err {
  border-left: 3px solid var(--star);
  background: var(--card);
  padding: 0.55rem 0.85rem;
  margin: 0 0 0.9rem;
  font-size: 0.9rem;
  max-width: 62ch;
}
.watch-pin { fill: var(--accent); font-size: 12px; }
a#watchDay { text-decoration: none; }
/* No longer an <a> wrapping the card — a delete form cannot nest inside
   an anchor, so the label is the link and the ✕ is its sibling, matching
   the place cards on Your places. */
.watchcard { color: inherit; display: block; }
.watchcard-when { color: var(--muted); font-size: 0.8rem; }
/* Past dates. Dimmed as a set rather than per-element: the whole card is
   a record of something finished, and a receipt that competes visually
   with a live watch inverts the triage order the sort just established. */
.watchcard-verified, .watchcard-unverified, .watchcard-awaiting {
  border-style: dashed;
}
.watchcard-actual {
  display: flex; align-items: center; gap: 8px; margin-top: 6px;
  flex-wrap: wrap;
}
.watchcard-big { font-size: 1.5rem; font-weight: 650; line-height: 1; }
.watchcard-precip { color: var(--muted); font-size: 0.95rem; }
.watchcard-past-note {
  font-size: 0.85rem; color: var(--muted); margin-top: 5px;
}
/* the criteria is constant and quiet; the outcome is the live half. Two
   elements, never one sentence — same split the panel already makes, and
   the reason for `block`: inline, "rain chance ≥ 60%" ran straight into
   "no measurable rain" and read as a single run-on clause. */
.watchcard-outcome { color: var(--fg); display: block; }
.watchcard-conf { font-size: 0.92rem; text-transform: uppercase; letter-spacing: 0.04em; margin-top: 4px; }
/* Two channels, never ranked against each other in one slot. At the
   current size both axes stack in practice, which is the better read
   anyway — the labels align into a column you can scan down. flex-wrap
   stays as the safety net: it is what keeps a long phrase off the
   card's edge instead of overflowing it. */
.watchcard-axes {
  display: flex; flex-wrap: wrap; gap: 2px 12px; margin-top: 5px;
  font-size: 0.92rem;
}
.watchcard-axis { display: inline-flex; gap: 5px; align-items: baseline; }
/* the label is the structural marker and stays a bare status word; the
   phrase reads as prose, so uppercasing it only made it wider */
.wc-axis-label {
  color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.76rem; flex: none;
}
.wc-axis-value { white-space: nowrap; }
/* watch cards carry more than a city card — give them room before the
   grid packs a fourth column and starts breaking phrases */
.home-watches { grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); }
/* The trend spark. `preserveAspectRatio: none` lets it fill the card's
   width exactly; every stroke carries vector-effect so the non-uniform
   scale can't turn a 1px line into a wedge. */
.watchcard-spark {
  display: flex; align-items: center; gap: 8px; margin-top: 7px;
}
.watchcard-spark svg { flex: 1 1 auto; width: 100%; height: 34px; }
.spark-line { fill: none; stroke: var(--accent); stroke-width: 1.6; }
.spark-last { fill: var(--accent); }
/* the user's alert line, and where they started watching — both are
   context for the series, so neither competes with it for ink */
.spark-rule {
  stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.75;
}
.spark-pin { stroke: var(--border); stroke-width: 1; }
/* what actually happened: hollow, so it reads as a different KIND of mark
   than the forecast line it sits against */
.spark-observed { fill: none; stroke: var(--fg); stroke-width: 1.6; }
.watchcard-trend {
  font-size: 0.82rem; color: var(--muted); white-space: nowrap; flex: none;
}
.watchcard-trend-up { color: var(--warm, #c73a12); }
.watchcard-trend-down { color: var(--accent); }

/* The criteria half stays muted in every state — it is the line you set,
   and it does not move. Tone rides the outcome, which is the only half
   that changes; source order puts these after .watchcard-outcome so they
   win its neutral --fg at equal specificity. */
.watchcard-alert { font-size: 0.85rem; margin-top: 5px; color: var(--muted); }
.watchcard-alert-tripped { color: var(--warm, #c73a12); font-weight: 600; }
.watchcard-alert-within-range { color: var(--accent); }
.watchcard-alert-far-off { color: var(--muted); }
.home-watches { margin-bottom: 6px; }

/* --- privacy notice -------------------------------------------------
   Deliberately NOT reusing .lab-proto / .lab-caption. Those are chart
   furniture: .lab-proto is user-select:none on coarse pointers (so a
   drag on a chart doesn't select text), which would make a legal notice
   uncopyable on a phone, and .lab-caption is muted 0.82rem, which is
   right for a chart aside and wrong for the one page a visitor may
   actually need to read carefully. Body text here runs at --fg. */
.privacy { max-width: 62rem; margin: 0 auto; padding: 0 4px 32px; }
.privacy-section { margin: 26px 0; }
.privacy-lede { font-size: 1rem; max-width: 68ch; margin: 6px 0 18px; }
.privacy-p { font-size: 0.95rem; max-width: 68ch; margin: 0 0 12px; }
.privacy-list {
  font-size: 0.95rem; max-width: 68ch; margin: 0 0 12px;
  padding-left: 1.2em;
}
.privacy-list li { margin: 0 0 8px; }
.privacy-list code, .privacy-p code {
  font-size: 0.88em; padding: 1px 5px; border-radius: 4px;
  background: var(--card); border: 1px solid var(--border);
}
/* Secondary detail inside a list item — a share link under the watch it
   belongs to. Muted, but only one step: this is a prose page, so it stays
   readable rather than dropping to the chart-caption grey. */
.privacy-quiet { font-size: 0.86rem; color: var(--muted); }
.privacy-form { margin: 6px 0 18px; }
.privacy-form button {
  font: inherit; font-size: 0.9rem; color: var(--fg);
  background: var(--card); border: 1px solid var(--border);
  border-radius: 8px; padding: 8px 16px; cursor: pointer;
}
.privacy-form button:hover { border-color: var(--accent); color: var(--accent); }
.privacy-saved {
  font-size: 0.9rem; color: var(--accent);
  border-left: 3px solid var(--accent); padding: 6px 0 6px 12px;
  margin: 0 0 18px;
}

/* Receipt pages (public who-was-right). Own prose classes at --fg,
   selectable — someone quotes these into a thread, so they are the
   opposite of chart furniture (see the .lab-* note above .privacy-*'s
   reasoning): .lab-caption still does captions, .receipt-p does prose. */
.receipt-p { font-size: 0.95rem; max-width: 68ch; margin: 0 0 12px; }
.receipt-p b, .receipt-verdict b { font-family: var(--font-data); }
.receipt-section { margin: 26px 0; }
.receipt-figure { margin: 22px 0 26px; max-width: 720px; }
.receipt-figure img {
  max-width: 100%; height: auto; display: block;
  border: 1px solid var(--border); border-radius: 10px;
}
.receipt-list { list-style: none; padding: 0; margin: 8px 0 0; max-width: 68ch; }
.receipt-list li { margin: 0 0 12px; }
.receipt-line { display: block; color: var(--muted); font-size: 0.86rem; }

/* Home roundup (daily-roundup): recorded change headlines, one per city.
   Prose someone reads and quotes — own classes at --fg, selectable, same
   reasoning as .receipt-* above. */
.roundup-list { list-style: none; padding: 0; margin: 4px 0 0; max-width: 74ch; }
.roundup-line { margin: 0 0 10px; font-size: 0.95rem; }
.roundup-city {
  font-family: var(--font-data); font-weight: 600; margin-right: 8px;
  white-space: nowrap;
}
.roundup-line:not(.roundup-notable) .roundup-headline { color: var(--muted); }
.roundup-tail, .roundup-empty {
  color: var(--muted); font-size: 0.86rem; max-width: 74ch; margin: 4px 0 0;
}
.roundup-sep { color: var(--muted); }
.roundup-deg { color: var(--fg); font-family: var(--font-data); margin-left: 5px; }

/* A grouped line carries a city list AND a sentence; run together they
   read as one run-on, so the sentence takes its own line under them. */
.roundup-group .roundup-headline { display: block; margin-top: 1px; }

/* National discussion (/national). Prose classes of its own at --fg —
   .lab-* are chart furniture and unselectable on coarse pointers. */
.nat-discussion { max-width: 68ch; margin: 18px 0 6px; }
.nat-p { font-size: 1rem; line-height: 1.62; margin: 0 0 14px; }
.nat-issued, .nat-quiet {
  color: var(--muted); font-size: 0.86rem; max-width: 68ch; margin: 0 0 6px;
}
.nat-quiet {
  border-left: 3px solid var(--border); padding: 6px 0 6px 12px; margin: 18px 0;
}
.nat-section { margin: 30px 0; }
.nat-list { list-style: none; padding: 0; margin: 8px 0 0; max-width: 68ch; }
.nat-list li { margin: 0 0 8px; font-family: var(--font-data); font-size: 0.92rem; }

/* The map: markers only, never a field. An outlined marker is a spread
   with one camp; filled means the models split into competing answers. */
.nat-map-wrap { overflow-x: auto; margin: 14px 0 10px; }
.nat-map { width: 100%; min-width: 620px; height: auto; display: block; }
.nat-marker circle {
  fill: none; stroke: var(--accent); stroke-width: 1.6px; opacity: 0.85;
}
.nat-marker.nat-split circle {
  fill: var(--accent); fill-opacity: 0.18; stroke-opacity: 1;
}
.nat-marker text {
  font-family: var(--font-data); font-size: 12.5px; fill: var(--fg);
  paint-order: stroke; stroke: var(--bg); stroke-width: 3px;
}
.roundup-more { margin: 12px 0 0; font-size: 0.92rem; }
.roundup-more-sub { color: var(--muted); font-size: 0.86rem; }

/* The coastline: a container for the markers, deliberately quiet. It
   carries no data — no value is interpolated between cities — so it must
   never compete with the markers that do. */
.nat-coast {
  /* Land takes the card colour so it separates from the page ground in
     BOTH themes — --thead on --bg is nearly the same value in light and
     the coast vanished. The stroke is --muted rather than --border for
     the same reason: --border on a light background is invisible. */
  fill: var(--card); fill-opacity: 0.75;
  stroke: var(--muted); stroke-opacity: 0.45;
  stroke-width: 1.1px; stroke-linejoin: round;
}
