/* NPC Weather — design ported from npclocal PrimaryWeather + SecondaryWeather
   components (Next.js/Tailwind) to pure CSS. Same visual language. */

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

/* ════════════════════════════════════════════════════════════════════════════
   TIME-OF-DAY ADAPTIVE THEME  (DESIGN-T, Round 1) — color tokens only.
   ───────────────────────────────────────────────────────────────────────────
   The page picks a phase from the selected city's REAL sunrise/sunset (the
   deterministic NOAA sun() engine in app.js) and sets <html data-tod="night |
   dawn | day | dusk">. Every color below resolves through a CSS variable, so a
   phase flip just re-points the tokens — no rule is duplicated per phase.

   DEFAULT (:root) = NIGHT, the brand's dark "storm" palette, BYTE-FOR-BYTE the
   values used before this refactor. Night-as-default means: (a) no white flash
   before JS runs, (b) the per-city sub-pages that can't yet compute the sun
   (they carry no lat/lon) stay correctly dark, (c) a no-JS visitor still gets
   the intended brand. The apex then PROMOTES to the light DAY palette in
   daylight via [data-tod="day"]. dawn/dusk reuse day/night here in Round 1;
   later rounds add their own warm shoulders.

   A11y guard: every text/surface token pair in NIGHT and DAY was checked with
   the WCAG (L1+0.05)/(L2+0.05) formula and clears 4.5:1 body / 3:1 large+UI.
   The day cyan accent that FAILS as text on white (2.1:1) is split into two
   tokens — --link (sky-700 #0369a1, 5.9:1, for text/links) and --accent-fill
   (a brighter cyan that only ever sits UNDER dark --on-accent text). Muted and
   status text darken in day so they don't wash out on white.
   ════════════════════════════════════════════════════════════════════════════ */
:root {
  /* surfaces */
  --bg: #0a1018;            /* page background */
  --bg-grad-a: #0f172a;     /* hero / card gradient start */
  --bg-grad-b: #0c1a35;     /* hero / card gradient end */
  --surface: #0f172a;       /* primary card / panel */
  --surface-2: #1e293b;     /* nested tile */
  --surface-3: #334155;     /* deepest divider tile */
  --topbar-bg: rgba(10,16,24,0.7);
  --border: rgba(255,255,255,0.07);   /* hairline boundaries */
  --border-2: rgba(255,255,255,0.05); /* faint inset boundaries */
  /* text ladder */
  --text: #fff;             /* headings */
  --text-body: #e5e7eb;     /* body */
  --text-2: #cbd5e1;        /* secondary / lede */
  --text-muted: #94a3b8;    /* labels */
  --text-faint: #64748b;    /* quietest (footnotes/zone) */
  /* accent — TWO roles, two tokens */
  --accent: #38bdf8;        /* accent hue (fills, glows, decorative) */
  --accent-fill: #38bdf8;   /* accent as a BACKGROUND (text on it is --on-accent) */
  --accent-soft: rgba(56,189,248,0.12); /* faint accent tint backgrounds */
  --accent-line: rgba(56,189,248,0.25); /* accent hairline / chip ring */
  --link: #38bdf8;          /* accent as TEXT / links (darkens in day) */
  --link-hover: #7dd3fc;
  --on-accent: #0a1018;     /* text that sits ON --accent-fill */
  /* warm "soft light" accent (golden hour line) */
  --warm: #fbbf24;
  --warm-soft: rgba(245,158,11,0.08);
  --warm-line: rgba(245,158,11,0.22);
  /* status */
  --ok: #22c55e; --ok-soft: #86efac;
  --warn: #fcd34d; --warn-strong: #fbbf24;
  --bad: #ef4444;
  /* UI affordances */
  --focus-ring: #38bdf8;
  --glow-a: rgba(56,189,248,0.08);
  --glow-b: rgba(125,211,252,0.05);
  /* theme transition (zeroed under prefers-reduced-motion, far below) */
  --tod-fade: 240ms;
}

/* ── DAY (light) — sun is up. Contrast-verified on the day surfaces. ──────────
   bg soft sky → near-white; surfaces white; text dark slate; the cyan accent
   becomes sky-700 AS TEXT (--link) while keeping a usable cyan FILL. Muted +
   status text darken so nothing drops below 4.5:1 on white. */
[data-tod="day"], [data-tod="dawn"] {
  --bg: #eef4fb;
  --bg-grad-a: #ffffff;
  --bg-grad-b: #eaf2fb;
  --surface: #ffffff;
  --surface-2: #f1f5f9;
  --surface-3: #e2e8f0;
  --topbar-bg: rgba(248,251,255,0.82);
  --border: rgba(15,23,42,0.12);
  --border-2: rgba(15,23,42,0.08);
  --text: #0f172a;          /* slate-900 headings — 17.9:1 on white */
  --text-body: #1e293b;     /* slate-800 body — 14.6:1 on white */
  --text-2: #334155;        /* slate-700 — 10.4:1 */
  --text-muted: #475569;    /* slate-600 — 7.6:1 (was slate-400, 2.6:1 FAIL) */
  --text-faint: #475569;    /* keep readable on white; no faint-grey wash */
  --accent: #0ea5e9;        /* sky-500 decorative fill hue */
  --accent-fill: #e0f2fe;   /* sky-100 chip fill — pairs with dark --on-accent */
  --accent-soft: rgba(2,132,199,0.08);
  --accent-line: rgba(2,132,199,0.30);
  --link: #0369a1;          /* sky-700 — 5.9:1 on white (THE accent-text fix) */
  --link-hover: #0c4a6e;    /* sky-900 — 9.5:1 */
  --on-accent: #0c4a6e;     /* sky-900 text on the sky-100 chip fill — 8.2:1 */
  --warm: var(--orange);          /* amber-800 — 6.8:1 on the warm tint (was #fbbf24, 1.4:1) */
  --warm-soft: rgba(180,83,9,0.08);
  --warm-line: rgba(180,83,9,0.28);
  --ok: #15803d;            /* green-700 — 5.0:1 */
  --ok-soft: #15803d;
  --warn: #b45309;          /* amber-700 — 5.0:1 (yellow is invisible on white) */
  --warn-strong: #b45309;
  --bad: #dc2626;           /* red-600 — 4.8:1 (red-500 is 3.8:1, fails body) */
  --focus-ring: #0284c7;    /* sky-600 — 4.1:1 border on white (sky-500 fails 3:1) */
  --glow-a: rgba(14,165,233,0.05);
  --glow-b: rgba(56,189,248,0.04);
}

/* Smoothly cross-fade the themable color properties on a phase flip. Scoped to
   the properties that actually change so layout never animates. Zeroed for
   prefers-reduced-motion in the existing reduced-motion block near the bottom. */
html, body,
.topbar, .primary-card, .chip, .forecast-card, .forecast-day,
.city-chip, .city-picker-dropdown, .alm-chip, .sun-cell, .sun-gold,
.sl-win, .weather-section, a {
  transition-property: background-color, color, border-color, box-shadow, fill, stroke;
  transition-duration: var(--tod-fade);
  transition-timing-function: ease;
}

html {
  -webkit-text-size-adjust: 100%;
  scrollbar-gutter: stable;
  /* Replace the default opaque-grey mobile tap-highlight (which paints a hard
     rectangle that ignores each control's border-radius and clashes with the
     navy/cyan theme) with a soft theme-cyan wash. On touch, :hover is unreliable
     (sticks after a tap or never fires), so this + the :active press states below
     are the only on-brand "I registered your tap" feedback the chips, picker,
     dropdown rows, alerts banner, and quick-nav pills get on a phone. */
  -webkit-tap-highlight-color: rgba(56, 189, 248, 0.18);
}
body {
  margin: 0;
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
.bg-storm {
  /* Two faint accent glows (tokenized) + a faint circuit-circle texture. CSS
     var() can't reach INSIDE a url() data-URI, so the SVG stroke color is fixed
     per phase by overriding the whole background-image under [data-tod="day"]
     below — keeping the night SVG here as the default. */
  background-image:
    radial-gradient(ellipse at top left, var(--glow-a), transparent 55%),
    radial-gradient(ellipse at top right, var(--glow-b), transparent 50%),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 200 200' fill='none'><g stroke='%2338bdf8' stroke-opacity='0.06' stroke-width='0.6' fill='none'><circle cx='50' cy='50' r='20'/><circle cx='50' cy='50' r='32'/><circle cx='150' cy='150' r='20'/><circle cx='150' cy='150' r='32'/></g></svg>");
}
/* DAY: a softer sky-blue circuit texture (lighter stroke, lower alpha) so the
   pattern reads as a faint watermark on the light bg instead of muddy specks.
   The glows are already tokenized above; only the SVG layer needs swapping. */
[data-tod="day"] .bg-storm, [data-tod="dawn"] .bg-storm {
  background-image:
    radial-gradient(ellipse at top left, var(--glow-a), transparent 55%),
    radial-gradient(ellipse at top right, var(--glow-b), transparent 50%),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 200 200' fill='none'><g stroke='%230ea5e9' stroke-opacity='0.05' stroke-width='0.6' fill='none'><circle cx='50' cy='50' r='20'/><circle cx='50' cy='50' r='32'/><circle cx='150' cy='150' r='20'/><circle cx='150' cy='150' r='32'/></g></svg>");
}

a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; color: var(--link-hover); }

.container { max-width: 1100px; margin: 0 auto; padding: 0 1.25rem; }

/* ───── Top bar ───── */
.topbar {
  border-bottom: 1px solid var(--border);
  background: var(--topbar-bg);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  position: sticky;
  top: 0;
  z-index: 10;
}
.topbar .container {
  display: flex; align-items: center; justify-content: space-between;
  padding-top: 0.85rem; padding-bottom: 0.85rem; flex-wrap: wrap; gap: 0.5rem;
}
.brand {
  margin: 0; font-weight: 800; font-size: 1.05rem; letter-spacing: 0.01em;
  color: var(--text);
  display: inline-flex; align-items: center; gap: 0.5rem;
  text-decoration: none;
  transition: color 120ms ease;
}
.brand:hover { color: var(--link-hover); text-decoration: none; }
.brand:hover .brand-mark path[stroke] { stroke: #fbbf24; }
.brand-mark { display: inline-block; flex-shrink: 0; }
.brand-mark path[stroke] { transition: stroke 120ms ease; }
.topbar-note { margin: 0; font-size: 0.82rem; color: var(--text-muted); }

/* ════════ Time-of-day theme toggle (Auto / Light / Dark) ════════════════════
   A 3-button segmented control in the top bar. Auto follows the sun (the live
   phase engine); Light/Dark pin the page. The pressed option is filled with the
   accent; the group has one visible focus ring (--focus-ring). Keyboard: each
   button is a real <button> with aria-pressed, so Tab + Enter/Space just work.
   On-theme in BOTH palettes because every color resolves through a token. */
.theme-toggle {
  display: inline-flex; align-items: center; gap: 0;
  border-radius: 999px; padding: 2px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
}
.theme-toggle__btn {
  appearance: none; -webkit-appearance: none; cursor: pointer;
  border: 0; background: transparent; color: var(--text-muted);
  font: inherit; font-size: 0.8rem; font-weight: 700; line-height: 1;
  letter-spacing: 0.01em;
  padding: 0.4rem 0.7rem; min-height: 32px; border-radius: 999px;
  display: inline-flex; align-items: center; gap: 0.3rem;
  transition: background-color 140ms ease, color 140ms ease;
}
.theme-toggle__btn .tt-ico { font-size: 0.95rem; line-height: 1; }
.theme-toggle__btn:hover { color: var(--text); }
.theme-toggle__btn[aria-pressed="true"] {
  background: var(--accent-fill); color: var(--on-accent);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.theme-toggle__btn:focus-visible {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .theme-toggle__btn { transition: none; }
}
/* On phones the bar is tight (brand + temp). Keep the toggle compact: hide the
   word labels, keep the icons (the aria-label still names each option for AT). */
@media (max-width: 600px) {
  .theme-toggle__btn .tt-txt { display: none; }
  .theme-toggle__btn { padding: 0.4rem 0.55rem; }
}


/* ───── Sticky-topbar live temp (apex + 17 city pages) ─────
   North-star (priority 1) addresses one scroll state no prior round reached:
   once the user scrolls past the hero, the position:sticky .topbar slides OVER
   the live data and shows nothing of the current conditions the phone user came
   for. The bar is the only persistent chrome that can carry the datum at EVERY
   scroll position, so mirror the live temp + condition glyph into it. It lives
   inside the existing ~44px bar — zero added fold height, zero CLS — and is
   revealed only once real data lands (.show added by app.js / city.js), so first
   paint shows nothing (no placeholder churn). aria-hidden: it duplicates the live
   card's temp/condition already in the a11y tree. The county aggregator pages
   carry no live card, so their (optional) span simply stays hidden. */
.topbar-temp {
  margin-left: auto;
  /* flex:0 1 auto (was 0 0 auto): the mirror now carries the live temp AND the
     condition WORD ("84° · Thunderstorms"), so the item must be allowed to
     SHRINK and ellipsise if a rare long condition would otherwise push past the
     bar. min-width:0 lets a flex item shrink below its content size so the
     ellipsis can actually engage; max-width leaves the brand its room at the
     left; white-space:nowrap + overflow:hidden + text-overflow:ellipsis clip a
     too-long string to one clean line instead of wrapping or overflowing. */
  flex: 0 1 auto;
  min-width: 0;
  max-width: calc(100% - 7rem);
  font-weight: 800;
  font-size: 0.95rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  transition: opacity 160ms ease;
}
.topbar-temp.show { opacity: 1; }

/* ───── Hero ───── */
.hero { padding: 2.5rem 1.25rem 1rem; }
.eyebrow {
  display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--link-hover);
  padding: 4px 10px; background: rgba(56, 189, 248, 0.12); border-radius: 999px;
  margin-bottom: 1rem;
}
.hero h1 {
  font-size: clamp(1.7rem, 2.6vw + 1rem, 2.5rem);
  line-height: 1.15; margin: 0 0 0.85rem; color: var(--text); letter-spacing: -0.015em;
}
.lede { color: var(--text-2); font-size: 1.025rem; margin: 0 0 1rem; max-width: 720px; }

/* ───── Quick-nav (anchor jump links under hero) ───── */
.quick-nav {
  display: flex; flex-wrap: wrap; gap: 0.5rem;
  margin-top: 1.25rem;
}
.quick-nav a {
  display: inline-flex; align-items: center;
  font-size: 0.82rem; font-weight: 600;
  padding: 0.5rem 0.9rem;
  background: rgba(56, 189, 248, 0.08);
  color: var(--link-hover);
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.18);
  text-decoration: none;
  transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease;
}
.quick-nav a:hover {
  background: rgba(56, 189, 248, 0.18);
  color: var(--text);
  box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.45);
  text-decoration: none;
}
/* Touch press-feedback: fires instantly on press on every device (unlike :hover,
   which is unreliable on touch). Placed after :hover so source order resolves it. */
.quick-nav a:active {
  background: rgba(56, 189, 248, 0.18);
  color: var(--text);
}

/* ───── Section headers (h2 with subtle separator + scroll target) ─────
   ONE canonical treatment for every section header across all 18 pages.
   Previously three sizes diverged on the apex — .cities-section h2 (1.5rem)
   and .storm-section h2/.info h2 (1.35rem) both out-specify the .section-h
   class their <h2>s already carry (descendant selector beats single class),
   so sibling sections rendered at 1.5 / 1.4 / 1.35rem with the separator on
   only some, reading as assembled-from-parts. WORSE, the 17 city pages put NO
   class on their section <h2>s, so on desktop they fell back to unstyled UA
   defaults (~1.5em, default margins, no separator) with zero parity with the
   apex. Folding all of those selectors into this one rule gives every header
   on every page the same size, separator, and rhythm. The city pages are
   reached by their stable section IDs (#current-section is sr-only'd on mobile
   but its desktop header is the live-card label and must match too). This is
   desktop cohesion only: the mobile @media block already overrides every one
   of these to 1.05rem, so phones are untouched. No IDs, no JS, no fold/CLS. */
.section-h,
.cities-section h2,
.storm-section h2,
.info h2,
#current-section > h2,
#forecast-section > h2,
#storm-section > h2 {
  font-size: 1.4rem; color: var(--text); margin: 0 0 1rem;
  letter-spacing: -0.01em;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
}
/* Smooth scroll for in-page nav.
   scroll-padding-top reserves space at the top of the SCROLL CONTAINER for the
   position:sticky topbar (top:0; z-index:10) on EVERY programmatic/anchor scroll.
   The per-target `section[id], a[id] { scroll-margin-top }` rule below offsets
   in-page anchors that match those selectors, but the R25 skip-link points at
   `href="#main"` → `<main id="main" tabindex="-1">`, and a <main> is NEITHER a
   section[id] NOR an a[id] — so that rule never matched it, and activating the
   site's primary keyboard/AT bypass scrolled #main flush to y=0 where the ~50px
   (desktop) / ~44px (mobile) sticky bar overlapped the top of the weather card,
   landing the user on occluded content. scroll-padding-top is the canonical,
   element-AGNOSTIC fix: it applies to the skip-link's <main> target, to browser
   find-on-page, and to any future fragment link, regardless of the target's tag
   or id. It mirrors the same 3.25rem desktop / 3rem mobile (set in the
   max-width:600px block) split already used for scroll-margin-top, so the two
   offset systems agree. Affects only where a scroll LANDS — no initial paint,
   fold, CLS, element-ID, or JS change; shared file → all 18 pages. */
html { scroll-behavior: smooth; scroll-padding-top: 3.25rem; }
/* Offset for the sticky topbar so an anchor jump lands the section heading
   flush UNDER the bar, not behind it. This was a stale 4.5rem (72px) constant
   from before the topbar was compressed across many rounds: the desktop bar is
   now ~50px (0.85rem×2 padding + 1.05rem brand line + 1px border) and the
   mobile bar ~44px (R42/R6: 0.5rem×2 padding + ~0.95rem brand + 1px). At 72px
   every jump — the desktop quick-nav AND, most importantly, the safety-critical
   red ALERTS BANNER that jumps to #alerts / #storm-section during a live NWS
   warning — landed the heading ~20px (desktop) / ~28px (mobile) BELOW the bar,
   opening a dead void and pushing the most time-sensitive content down exactly
   when it should read fastest. 3.25rem (52px) clears the desktop bar with an
   ~8px gap; the mobile override (44px bar + ~8px gap = 3.25rem) is set in the
   max-width:600px block below. Affects only where scroll-to-anchor lands — no
   initial-paint/fold/CLS change. Shared file → all 18 pages. */
section[id], a[id] { scroll-margin-top: 3.25rem; }

/* ───── PRIMARY WEATHER CARD (port of PrimaryWeather.tsx) ───── */
.weather-section { padding: 0.5rem 1.25rem 2rem; }
.primary-card {
  background: linear-gradient(135deg, var(--bg-grad-a) 0%, var(--bg-grad-b) 100%);
  border-radius: 1rem;
  padding: 1.5rem;
  box-shadow: inset 0 0 0 1px var(--border);
  transition: opacity 220ms ease;
}
@media (min-width: 640px) {
  .primary-card { padding: 1.75rem 2rem; }
}
.primary-card.fetching { opacity: 0.5; }

/* City picker */
.city-picker-row {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 0.85rem; flex-wrap: wrap; gap: 0.5rem;
  position: relative;
}
.city-picker-btn {
  display: inline-flex; align-items: center; gap: 0.35rem;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--link);
  background: none; border: none; padding: 0; cursor: pointer;
  transition: color 150ms ease;
}
.city-picker-btn:hover { color: var(--link-hover); }
/* Touch press-feedback (see html tap-highlight note). */
.city-picker-btn:active { color: var(--link-hover); }
.city-picker-btn .caret { opacity: 0.6; transition: opacity 120ms ease; margin-left: 0.15rem; }
.city-picker-btn:hover .caret { opacity: 1; }
.city-picker-btn .pin { font-size: 0.9rem; }

.city-picker-dropdown {
  position: absolute;
  top: 100%; left: 0;
  margin-top: 0.5rem;
  z-index: 20;
  background: var(--surface-2);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.08), 0 12px 32px rgba(0,0,0,0.45);
  border-radius: 0.75rem;
  overflow: hidden;
  min-width: 220px;
  max-height: 60vh;
  overflow-y: auto;
}
.city-picker-dropdown button {
  display: flex; align-items: center; justify-content: space-between;
  /* R4 (5bdb7df) grew the picker BUTTON to >=44px but left the list it opens —
     the actual 17 city-switch targets — at ~35px tall (0.65rem padding +
     0.875rem text), under the WCAG 2.5.5 / Apple HIG 44px minimum and mis-tap-
     prone on the most-used mobile control. min-height:44px + the symmetric
     vertical padding clears the floor; align-items:center keeps the label
     centred in the taller row. The dropdown is absolutely-positioned with
     max-height:60vh + overflow-y:auto (L199-200), so the taller list just
     scrolls internally — nothing outside it (incl. the above-the-fold temp)
     moves. Defined once (no mobile override), so this fixes all 18 pickers. */
  width: 100%; padding: 0.72rem 0.85rem; min-height: 44px;
  font-size: 0.875rem; color: var(--text-2);
  background: none; border: none; cursor: pointer;
  text-align: left;
  /* Hairline divider so the now-taller rows read as discrete tap targets. */
  box-shadow: inset 0 -1px 0 rgba(255,255,255,0.04);
  transition: background 120ms ease, color 120ms ease;
}
.city-picker-dropdown button:last-child { box-shadow: none; }
.city-picker-dropdown button:hover {
  background: var(--surface-3);
  color: var(--text);
}
/* Touch press-feedback on each of the 17 city-switch rows. Placed BEFORE the
   persistent `button.active` (selected-city) rule below so that — at equal
   specificity — the selected row keeps its cyan-fill styling when not pressed,
   while a press still flashes this state. */
.city-picker-dropdown button:active {
  background: var(--surface-3);
  color: var(--text);
}
.city-picker-dropdown button.active {
  background: #0284c7;
  color: var(--text);
  font-weight: 700;
}
.detecting-pulse {
  font-size: 0.72rem; color: var(--text-2);
  animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* Big temp + emoji row */
.temp-emoji-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem;
}
.big-temp {
  font-size: clamp(4rem, 13vw, 7rem);
  font-weight: 200;
  line-height: 1;
  color: var(--text);
  letter-spacing: -0.03em;
}
.big-emoji {
  font-size: clamp(3.5rem, 11vw, 6rem);
  line-height: 1;
  user-select: none;
}

.cond-row {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 0.5rem; margin-top: 0.75rem;
  flex-wrap: wrap;
}
.cond-text {
  font-size: 1.1rem; color: var(--text-2); line-height: 1.3;
  /* Keep the condition word on the SAME row as the "Updated X min ago" stamp.
     .cond-row is flex + flex-wrap:wrap; this child had no min-width:0, so its
     default min-width:auto refused to shrink. On a 375px phone the card inner
     is ~300px and long OpenWeather strings ("Thunderstorm with heavy rain"
     ~256px) left no room for the ~90px nowrap stamp, so flex-wrap dropped the
     stamp to a 2nd line — adding ~16-18px INSIDE the above-the-fold card and
     shoving the Wind/Rain detail chips lower, precisely on storm days when
     those chips matter most. flex:1 1 auto + min-width:0 + ellipsis lets the
     condition shrink/truncate so the stamp stays inline (same R17/R26 flex-
     overflow idiom). The full word survives in the aria-live card, the topbar
     mirror, and the emoji title, so a rare ellipsis loses no scent. Reaches the
     apex #primary-card .cond-row and the .cond-row city.js injects on all 17
     city pages from one edit; height can only shrink, so the fold can't regress. */
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Error state only: the apex "Couldn't load data…" sentence is far longer than a
   one-word condition, so the ellipsis above truncated it ("…Try refre…") on narrow
   phones. Let it wrap to a readable 2 lines instead. app.js adds .cond-error on the
   selectCity catch path and removes it on the next successful renderPrimaryCard, so
   normal conditions keep the single-line + inline "Updated X min ago" stamp. Apex-
   only: city.js renders .cond-text without this class, so city pages are untouched. */
.cond-text.cond-error {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.updated-label {
  font-size: 0.72rem; color: var(--text-muted);
  white-space: nowrap;
  flex: 0 0 auto;
}

/* Detail chips grid */
.detail-chips {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem;
  margin-top: 0.85rem;
  transition: opacity 220ms ease;
}
@media (min-width: 640px) {
  .detail-chips { grid-template-columns: repeat(4, 1fr); gap: 0.75rem; }
}
.detail-chips.fetching { opacity: 0.5; }
.chip {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border);
  border-radius: 0.75rem;
  padding: 0.85rem 1rem;
}
.chip-label {
  font-size: 0.7rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 0 0 0.25rem;
  font-weight: 600;
  /* Pin to one line. On the 2-col mobile detail-chips grid the usable label
     width is ~86px at 320px, so "RAIN CHANCE" / "FEELS LIKE" wrapped to a 2nd
     line; grid rows size to the tallest cell, so the above-the-fold Feels-Like/
     Humidity row grew ~14px and read ragged. The label set is a fixed 4-string
     set ("Feels Like"/"Humidity"/"Wind"/"Rain Chance") whose longest fits one
     line, so nowrap is safe — no clipping, no ellipsis needed. Completes the
     single-line family (R17 cond-row, R19 chip-value, R26 city-name, R27). */
  white-space: nowrap;
}
.chip-value {
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--text);
  line-height: 1.1;
}

/* ───── 5-day / 7-day forecast strip (port of SecondaryWeather) ───── */
.forecast-card {
  margin-top: 0.75rem;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border);
  border-radius: 1rem;
  overflow: hidden;
  transition: opacity 220ms ease;
}
.forecast-card.fetching { opacity: 0.4; }
.forecast-card .label {
  padding: 0.65rem 1rem 0.35rem;
  font-size: 0.7rem; color: var(--text-muted);
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em;
}
.forecast-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
.forecast-day {
  display: flex; flex-direction: column; align-items: center;
  padding: 0.65rem 0.2rem;
  border-left: 1px solid rgba(255,255,255,0.05);
  gap: 0.1rem;
  min-width: 0;
}
.forecast-day:first-child { border-left: none; }
.forecast-day.is-today {
  background: rgba(56, 189, 248, 0.08);
  box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.25);
}
.forecast-day.is-today .fd-day {
  color: var(--link-hover);
  font-weight: 800;
  letter-spacing: 0.02em;
}
.fd-day {
  font-size: 0.7rem; font-weight: 700; color: var(--text-2);
}
/* R32: trimmed 1.45rem -> 1.2rem so the decorative glyph stops out-weighting the
   hi temp (see the .fd-hi note below). The emoji was the cell's dominant VERTICAL
   contributor (1.45rem * line-height 1.1 = 1.595rem); at 1.2rem it is 1.32rem, a
   -0.275rem drop that exceeds the +0.143rem the hi grows — so net cell height
   falls slightly and the R13 reserves / R20 baseline / above-the-fold win don't
   move. line-height:1.1 unchanged. */
.fd-emoji {
  font-size: 1.2rem; line-height: 1.1;
}
/* Pin the rain row to one height across all 7 cells. On rainy days this line is
   `💧X%`, whose emoji glyph inflates its line-box; on dry days (rain_chance 0)
   it's a bare `&nbsp;` that would otherwise collapse to ~7px. That height
   mismatch pushed each rainy day's .fd-temp (hi/lo) DOWN relative to the dry
   days, so the 7 temps stair-stepped across the strip (today's data alone mixes
   0%/50%/10%). min-height = the rendered droplet-line height locks the blank
   placeholder to the SAME box, so every cell's hi/lo lands on one baseline. The
   droplet line was already the tallest case, so this adds NO new strip height —
   the forecast reserves (112/132/136px) and the above-the-fold win don't move.
   flex-center keeps the % glyph vertically centered in that box at every size. */
.fd-rain {
  font-size: 0.62rem; color: var(--link); font-weight: 600; line-height: 1;
  min-height: 0.95rem;
  display: flex; align-items: center; justify-content: center;
}
/* Dry days (rain_chance 0) render a muted "0%" instead of a blank placeholder.
   R20 already pins .fd-rain to the droplet line-box height with flex centering,
   so this short label sits in the SAME reserved box — zero height/CLS/fold change.
   Dim slate (vs the cyan 💧X% of rainy days) keeps it present-but-quiet so the
   typically-mostly-dry Coastal Bend week reads as real 0%-rain info, not no-data.
   R24: lifted #64748b -> #748196 to clear WCAG 2.1 AA. The label renders at
   .fd-rain size (0.62rem desktop / 0.56rem mobile, ~9-10px, weight-600), so the
   3.0:1 large-text exception does NOT apply — it needs the 4.5:1 normal-text
   floor. #64748b measured 3.75:1 on the #0f172a forecast card (4.01:1 on page
   bg): a real contrast failure, and on the COMMON case (a typical Coastal Bend
   week is mostly dry, so most cells show "0%"), on all 18 pages. #748196 measures
   4.53:1 on the card / 4.84:1 on bg — clears AA — while staying clearly dimmer
   than the rainy-day cyan #38bdf8 (8.33:1), so the R21 present-but-quiet intent
   holds. Color-only: identical glyph/box/baseline, zero CLS/fold/height change. */
.fd-rain-dry { color: var(--text-muted); }
.fd-temp { display: flex; flex-direction: column; align-items: center; gap: 0; }
/* R32: lifted 0.82rem -> 0.95rem so the hi temp — the datum the 7-day strip exists
   to be skimmed for — is the dominant element in each cell, not the decorative
   weather glyph above it. Until now .fd-emoji (1.45rem) rendered ~1.77x the hi
   (0.82rem), so the emoji won the first fixation in all 7 cells: the same
   emoji-out-competes-the-data inversion R9 (mobile hero 2.6rem) and R31 (desktop
   hero 4rem) already corrected on the temp-vs-emoji hero, here carried to the lone
   surface that still inverted it. Pairing the trimmed .fd-emoji (now 1.2rem) above,
   the hi reads first and the glyph supports it. The lo stays 0.7rem dim slate so the
   hi/lo pair keeps its relationship. Net cell height nets DOWN (the emoji shrinks
   more than the hi grows — see .fd-emoji), so the R13 forecast reserves, R20
   baseline alignment, R28 tabular-nums, the is-today highlight, and the
   above-the-fold win are untouched. weight-700 / #fff / line-height:1.1 unchanged.
   app.js (apex) and city.js (17 city pages) inject identical .forecast-day markup,
   so this one shared rule reaches all 18 pages. Mobile bump is in the
   max-width:480px block below. */
.fd-hi { font-size: 0.95rem; font-weight: 700; color: var(--text); line-height: 1.1; }
.fd-lo { font-size: 0.7rem; color: var(--text-muted); line-height: 1.1; }

/* ───── Tabular numerals on the number-dense scan surfaces (apex + 17 city pages) ─────
   The body font stack (ui-sans-serif/system-ui/Segoe UI…) defaults to
   PROPORTIONAL digits, where "1" renders ~half the advance width of "0"/"8".
   No prior round set font-variant-numeric anywhere (grep confirms zero). On the
   site's three most-skimmed number regions that proportional jitter actively
   fights jobs prior rounds optimized:
     • .cc-temp — the 17-city grid temps, stacked in a FIXED 2-col mobile grid
       (repeat(2, minmax(0,1fr))). Equal-magnitude temps ("111°" vs "88°" vs
       "101°") render at unequal widths, so the numerals don't line up
       column-to-column — undercutting the R3/R9 "skim 17 temps fast" job on the
       horizontal/character axis that R20 attacked from the vertical one.
     • .fd-hi / .fd-lo — the 7 hi/lo pairs in the forecast strip's fixed 7-col
       grid (repeat(7, minmax(0,1fr))); proportional digits wobble the pairs
       across the cells R20/R23 fought to baseline-align.
     • .chip-value — Feels-Like/Humidity/Wind/Rain values reflow in place as
       their digits change on every 10-min refresh.
     • .topbar-temp — the sticky scrolled-state mirror ("84° · Thunderstorms"),
       the ONLY live datum visible once a phone scrolls past the hero; with
       proportional digits the leading temp shifts the whole string each refresh
       (99°→101°).
   Every one of these already lives inside a fixed-width grid track or a clamped/
   width-capped box, so giving digits one uniform advance width changes only the
   INTRA-cell digit layout: zero width growth, zero added fold height, zero CLS,
   and no element-ID, JS, clamp, line-height, weight, grid-track, or theme change.
   The forecast hi/lo and chip values are injected identically by app.js (apex)
   and city.js (17 city pages), so this one shared rule reaches every surface on
   all 18 pages. font-feature-settings:"tnum" is the fallback for older engines
   that don't honor the font-variant-numeric shorthand. */
.cc-temp,
.fd-hi,
.fd-lo,
.chip-value,
.topbar-temp {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* On mobile: smaller padding + slightly smaller type so all 7 days fit.
   We DO NOT hide days — the previous version hid 6 & 7 on mobile but the
   copy promises 7, so all 7 must be visible at every viewport. */
@media (max-width: 480px) {
  /* On a 375px phone the cities card leaves ~335px, so each of the 7
     repeat(1fr) cells is only ~48px wide and the padding above drops to ~2px/side
     — the ONLY thing separating columns is the base .forecast-day border-left
     (L363), a 5%-white hairline on the #0f172a card that is effectively invisible.
     The 7 dense cells (day/emoji/💧%/hi/lo) then run together as one undifferentiated
     wall of numbers with no column rhythm; only the cyan is-today cell is separable,
     which directly drags the strip's core mobile job — skim each day's hi/lo fast.
     Lift the divider to 12% white so each day reads as its own column, and nudge the
     horizontal padding a hair (0.12→0.16rem) so adjacent numerals don't kiss the new
     gridline. The border already occupies its 1px at every viewport (only its COLOR
     changes) and :first-child still has border-left:none, so the strip width, the
     7-col grid, the R20 baseline alignment, and the forecast min-height reserves are
     all unchanged — zero added height, zero CLS, no fold impact. Desktop (>480px)
     keeps its existing subtle 0.05 divider, untouched. */
  .forecast-day { padding: 0.55rem 0.16rem; border-left-color: rgba(255,255,255,0.12); }
  .fd-day { font-size: 0.62rem; letter-spacing: -0.02em; }
  /* R32 (mobile mirror of the desktop base): trim the glyph and lift the hi so the
     hi temp out-weights the emoji on phones too. emoji 1.25rem * 1.1 = 1.375rem ->
     1.05rem * 1.1 = 1.155rem (-0.22rem); hi 0.75rem * 1.1 -> 0.85rem * 1.1
     (+0.11rem). Net cell height nets down, so the R13 mobile reserve (112px,
     #forecast-section > #forecast just below) is not exceeded — zero CLS, and the
     strip is below the fold on mobile so the above-the-fold win is untouched. */
  .fd-emoji { font-size: 1.05rem; }
  .fd-hi { font-size: 0.85rem; }
  .fd-lo { font-size: 0.65rem; }
  .fd-rain { font-size: 0.56rem; min-height: 0.85rem; }
  /* Smaller city-page forecast reserve: the cells shrink here (smaller padding
     + type above), so the injected .forecast-card renders ~112px instead of
     ~136px. Match the reserve so phones don't over-reserve and open a gap below
     the strip. Same `#forecast-section >` structural scope as the base rule, so
     it stays city-pages-only and never touches the apex; comes later in source
     at equal specificity, so it correctly wins on small screens. */
  #forecast-section > #forecast { min-height: 112px; }
}

/* ───── 17-city overview chips ───── */
.cities-section { padding: 2rem 1.25rem 1rem; }
/* .cities-section h2 now inherits the canonical .section-h treatment (above);
   its old 1.5rem one-off was removed so every section header matches. */
/* slate-300 (not the dimmest #94a3b8): this line carries the only "Tap any city"
   tappability affordance + freshness stamp, so it must read on a dark phone. */
.last-updated { font-size: 0.78rem; color: var(--text-2); margin: 0 0 1rem; }
.city-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(165px, 1fr));
  gap: 0.6rem;
  /* Reserve the grid's rendered footprint so it doesn't collapse-then-jump on
     first paint. renderCityGrid (app.js) writes innerHTML only AFTER 17 JSON
     fetches settle, so #city-grid was 0px tall at first paint then expanded to
     its real height once data landed — a sizable below-fold CLS, and the one
     large async region prior rounds never reserved (the city-page card and the
     forecast strip both got reserves; this grid was the gap). Each ~80px chip
     in a ~5-6-col desktop layout makes ~3-4 rows: ~340px holds the box (the
     full-width skeleton line + a slight under-reserve avoids over-reserving on
     a 6-col row). The 2-col mobile value (~9 rows) is set in the
     max-width:600px block below. Apex-only (city pages have no #city-grid),
     well below the fold — zero risk to the above-the-fold win; no ID/JS/theme
     change. */
  min-height: 340px;
}
.city-chip {
  display: block;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border);
  border-radius: 0.6rem;
  padding: 0.65rem 0.85rem;
  text-decoration: none;
  color: inherit;
  transition: transform 120ms ease, background 120ms ease;
}
.city-chip:hover {
  background: var(--surface-2);
  transform: translateY(-1px);
  text-decoration: none;
}
/* Touch press-feedback: settle to baseline (cancel the hover lift) + a tiny
   scale-down so a tap on a city chip — the single most-used mobile interaction —
   reads as a deliberate press. transform is already transitioned above. */
.city-chip:active {
  background: var(--surface-2);
  transform: translateY(0) scale(0.985);
}
.city-chip .cc-name {
  font-size: 0.78rem; font-weight: 700; color: var(--text); margin-bottom: 0.15rem;
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.35rem;
}
/* Clamp the city-name span so the selected-city ".is-current" chip stops
   overflowing on mobile. .cc-name is display:flex with justify-content:
   space-between; its two children are the name <span> (app.js renderCityGrid)
   and the nowrap ".cc-you" 📍 Your city tag (flex:0 0 auto). The name span had
   no min-width:0, and a flex item's default min-width:auto refuses to shrink
   below its content width — so on the R3 fixed 2-col grid (~163px chip, ~136px
   usable) the longest + most-selected name ("Corpus Christi", ~95-100px at
   0.78rem/700) plus the ~78px tag overflowed the track and collided/clipped
   against the chip edge — on the one chip the R2 highlight exists to surface.
   min-width:0 + overflow/ellipsis lets only the NAME truncate cleanly to one
   line; .cc-you keeps flex:0 0 auto so the cyan tag is never clipped. Same
   flex-overflow fix already applied to .cc-cond below and to Wind (R17). The
   chip carries the full city in its aria-label/href and the temp+emoji below,
   so a rare ellipsis loses no scent. Apex-only by structure (#city-grid), well
   below the fold → zero fold/CLS/ID/JS impact; can only reduce chip width. */
.city-chip .cc-name > span:first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cc-temp-row {
  display: flex; align-items: baseline; gap: 0.4rem;
}
/* The grid temp is the literal payload of every chip and the whole point of the
   17-city overview (skim 17 temps fast). At 1.6rem a hairline weight-200 numeral
   is the thinnest possible white glyph and slow to read in the R3 2-col mobile
   scan; weight-400 gives it body with ZERO reflow (same size/line-height, so chip
   height/width and the grid layout are unchanged). The hero .big-temp stays
   weight-200 — it is huge (clamp 4-7rem) and is the intentional hairline. */
.cc-temp { font-size: 1.6rem; font-weight: 400; color: var(--text); line-height: 1; letter-spacing: -0.01em; }
.cc-emoji { font-size: 1.3rem; line-height: 1; margin-left: auto; }
/* slate-300 to match the canonical body tier (.cond-text/.lede/tropical body)
   instead of the dimmest #94a3b8, so the supporting condition label is readable.
   Clamp to ONE line (nowrap + overflow + ellipsis): the chip renders the raw
   OpenWeather conditions_short (app.js renderCityGrid), and while today's strings
   ("Partly cloudy", "Thunderstorms") fit, OpenWeather also emits longer values in
   other weather states ("Scattered clouds" 16ch, "Heavy intensity rain",
   "Thunderstorm with rain"). In the R3 fixed 2-column mobile grid a chip is only
   ~163px wide (~136px usable after padding), so the instant a long value lands it
   wrapped to a 2nd line — and CSS-grid rows size to the TALLEST cell, so that chip's
   neighbor gained dead whitespace and the .cc-temp numerals fell out of horizontal
   alignment across the row, making the 17-city overview read ragged. .cc-cond is a
   block, so nowrap + overflow:hidden + text-overflow:ellipsis clip any over-long
   condition to one clean line with an ellipsis instead of wrapping — guaranteeing
   uniform chip height and aligned temps at every viewport. The chip already carries
   the full city via its aria-label/href, and the emoji + temp convey the conditions,
   so the (rare) ellipsis loses no essential scent. This can only REDUCE a chip's
   height, so it adds zero fold height / CLS and the 340/720px grid reserves still
   hold. Apex-only by structure (city pages have no #city-grid). */
.cc-cond {
  font-size: 0.7rem; color: var(--text-2); margin-top: 0.2rem; text-transform: capitalize;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Highlight the user's currently-selected city among the 17 chips so they can
   re-locate it at a glance (set by app.js renderCityGrid when c.id === cityId).
   Reuses the forecast strip's .forecast-day.is-today language — cyan tint +
   inset cyan ring + cyan name accent — for one consistent "this is yours/now"
   visual system across the page. Apex-only (city pages have no grid); well
   below the fold, so zero fold/CLS impact. */
.city-chip.is-current {
  background: rgba(56, 189, 248, 0.08);
  box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.45);
}
.city-chip.is-current .cc-name { color: var(--link-hover); }
.cc-you {
  flex: 0 0 auto;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--link-hover);
  white-space: nowrap;
}

/* ───── Alerts banner (clickable, jumps to #alerts section) ───── */
.alerts-banner {
  background: linear-gradient(135deg, #7f1d1d, #991b1b);
  border-radius: 0.75rem;
  padding: 0.85rem 1.1rem;
  display: flex; align-items: center; gap: 0.85rem;
  box-shadow: inset 0 0 0 1px rgba(248,113,113,0.4);
  margin-top: 1rem;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 120ms ease;
}
.alerts-banner:hover {
  text-decoration: none;
  transform: translateY(-1px);
  box-shadow: inset 0 0 0 1px rgba(248,113,113,0.7), 0 6px 20px rgba(127,29,29,0.35);
}
/* Touch press-feedback on the safety-critical alerts banner: cancel the hover
   lift + brighten the inset ring so tapping it during a live NWS warning gives
   instant confirmation on a phone. */
.alerts-banner:active {
  transform: translateY(0);
  box-shadow: inset 0 0 0 1px rgba(248,113,113,0.7);
}
.alerts-banner.hidden { display: none; }
.alerts-banner-icon { font-size: 1.6rem; line-height: 1; flex-shrink: 0; }
.alerts-banner-content { flex: 1; }
.alerts-banner-content h3 { margin: 0 0 0.2rem; color: var(--text); font-size: 1rem; }
.alerts-banner-content p { margin: 0; color: var(--bad); font-size: 0.85rem; }
.alerts-banner-cta {
  flex-shrink: 0;
  color: var(--text);
  font-size: 0.82rem;
  font-weight: 700;
  background: rgba(255,255,255,0.12);
  padding: 0.35rem 0.7rem;
  border-radius: 999px;
  white-space: nowrap;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.18);
}

/* ───── Tropical Atlantic widget ───── */
.tropical-status {
  display: flex; align-items: center; gap: 1rem;
  padding: 0.5rem 0;
  flex-wrap: wrap;
}
.tropical-pulse {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: 1rem; font-weight: 700;
  padding: 0.5rem 0.85rem;
  border-radius: 999px;
}
.tropical-pulse.calm {
  background: rgba(134, 239, 172, 0.12);
  color: var(--ok);
  box-shadow: inset 0 0 0 1px rgba(134, 239, 172, 0.3);
}
.tropical-pulse.active {
  background: rgba(248, 113, 113, 0.15);
  color: var(--bad);
  box-shadow: inset 0 0 0 1px rgba(248, 113, 113, 0.4);
  animation: tropical-pulse-anim 1.8s ease-in-out infinite;
}
@keyframes tropical-pulse-anim {
  0%, 100% { box-shadow: inset 0 0 0 1px rgba(248,113,113,0.4); }
  50% { box-shadow: inset 0 0 0 1px rgba(248,113,113,0.4), 0 0 0 6px rgba(248,113,113,0.18); }
}
.tropical-off {
  color: var(--text-muted);
  font-size: 0.92rem;
}
.tropical-storms {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0.75rem;
  margin-top: 1rem;
}
.tropical-storm {
  background: rgba(127, 29, 29, 0.2);
  border-radius: 0.6rem;
  padding: 0.85rem 1rem;
  box-shadow: inset 0 0 0 1px rgba(248,113,113,0.3);
}
.tropical-storm h4 { margin: 0 0 0.3rem; color: var(--bad); font-size: 0.95rem; }
.tropical-storm p { margin: 0; color: var(--text-2); font-size: 0.85rem; }

/* ───── Storm / sources panels ───── */
.storm-section, .info, .disclaimer { padding: 2rem 1.25rem 1rem; }
/* .storm-section h2 / .info h2 now inherit the canonical .section-h treatment
   (above) — their old 1.35rem one-off was removed so every section header
   across all 18 pages renders at one size with one consistent separator. */
.storm-section p, .info p { color: var(--text-2); }
.storm-content {
  background: var(--surface);
  border-radius: 0.85rem;
  padding: 1rem 1.25rem;
  box-shadow: inset 0 0 0 1px var(--border);
  margin-top: 0.75rem;
}
.storm-empty { color: var(--ok); font-size: 0.95rem; line-height: 1.55; }
.storm-alert { margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border); }
.storm-alert:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
.storm-alert h3 { color: var(--bad); font-size: 1.025rem; margin: 0 0 0.25rem; }
.storm-alert p { color: var(--text-2); font-size: 0.9rem; margin: 0; }

h3 { color: var(--text-body); font-size: 1.05rem; margin: 1rem 0 0.4rem; }
ul.tools-list, ul.sources { padding-left: 1.25rem; margin: 0.5rem 0; }
ul.tools-list li, ul.sources li { margin-bottom: 0.45rem; color: var(--text-2); }
.disclaimer { font-size: 0.9rem; color: var(--text-muted); }
.disclaimer strong { color: var(--bad); }

/* ───── City-page live region: reserve the card footprint (CLS fix) ─────
   On every one of the 17 city pages, #city-current ships as a one-line
   <p class="loading"> and city.js then injects a full .primary-card (big
   temp + emoji) PLUS a 4-chip .detail-chips grid — a ~300px jump that
   shoved #forecast/#storm-section/info down on first paint, directly under
   the (Round-1) visually-hidden heading. The apex avoids this because its
   card chrome + 4 placeholder chips are static HTML; the city pages never
   got that parity. Reserving the box here makes first paint a stable
   skeleton instead of a bounce — the north-star "data above the fold" only
   pays off if the data doesn't shift after it lands. Pure additive CSS:
   no HTML/JS/ID changes (city.js still replaces #city-current's innerHTML). */
.city-current {
  display: grid;
  gap: 0.85rem;
  min-height: 300px;
}
@media (min-width: 640px) {
  /* Desktop: chips collapse to one row, so the real card is shorter. */
  .city-current { min-height: 260px; }
}
/* Skeleton: make the bare loading line occupy the card's real footprint
   with the same storm-card chrome, so the placeholder reads as a calm
   "loading card" rather than a stray italic line above empty space. */
.city-current > .loading {
  margin: 0;
  display: flex;
  align-items: flex-end;
  min-height: 160px;
  padding: 1.1rem;
  border-radius: 1rem;
  background: linear-gradient(135deg, var(--bg-grad-a) 0%, var(--bg-grad-b) 100%);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* ───── City-page 7-day forecast strip: reserve its footprint (CLS fix) ─────
   Completes the city-page CLS story. The apex forecast strip ships its
   .forecast-card chrome (#forecast-card > #forecast-grid) as STATIC HTML, so
   it holds its ~136px height at first paint — that is why the apex needed no
   reserve (the reverted R5 was redundant there). But on every one of the 17
   city pages #forecast is a BARE <div> holding only a single
   <p class="loading">Loading forecast…</p> (~24px); city.js then injects the
   whole .forecast-card (label row + 7-cell grid), producing a ~90-110px
   downward jump that shoves #storm-section / the info sections down on first
   paint. R3 + .city-current{min-height} already reserve the conditions CARD
   here, so the forecast strip was the one large async region on the city
   pages with no reserve — a direct asymmetry with the apex. Reserving the
   wrapper makes its first paint a stable skeleton like the apex.

   Scoped BY STRUCTURE to the 17 city pages only: apex #forecast is a <section>
   (index.html L175), NOT a child of #forecast-section, so `#forecast-section >
   #forecast` cannot match the apex — the apex stays byte-identical. 132px is
   sized just under the rendered card (label ~32px + 7-cell grid ~104px ≈
   136px) so it absorbs the jump without opening a gap if cells render a hair
   shorter; the card has overflow:hidden + a fixed grid, so the reserve is
   invisible once data lands. The smaller <=480px value (cells shrink there) is
   set in the max-width:480px forecast block below. No HTML/JS/ID change —
   city.js still strips .forecast-grid off the wrapper and injects the card
   normally; #forecast is preserved. */
#forecast-section > #forecast { min-height: 132px; }

/* ───── Mobile fold compression (apex + all 17 city pages) ─────
   North-star fix: at 375x667 the temperature must be visible without
   scrolling. The shared hero is pure marketing chrome that ran ~430-480px
   before any live datum. This block (mobile only — desktop untouched)
   tightens hero padding/type/spacing and visually hides the redundant
   "Current conditions" heading that sits directly above the live card,
   while keeping it for screen readers + SEO. Net: pulls the data card
   up ~200px so temp + condition + first chip row clear the fold. */
@media (max-width: 600px) {
  /* Tighten the hero's trailing whitespace so the live #weather card sits
     closer under the H1. With the eyebrow (R4), lede (R2), and quick-nav (R7)
     all hidden on mobile, the hero is now just the H1 plus bottom padding —
     that trailing gap is pure fold cost directly above the data card. Drop the
     hero bottom padding 0.5rem -> 0.25rem and (below) the H1 margin-bottom
     0.45rem -> 0.3rem to pull the card up ~8px on all 18 pages. No content
     removed; desktop (>600px) untouched. */
  .hero { padding: 1.1rem 1.25rem 0.25rem; }
  /* Hide the marketing eyebrow pill before the fold on all 18 pages.
     On apex it's "Free public utility · No login · No tracking" — a
     restatement of the topbar-note + footer; on the 17 city pages it's a
     region/county label ("Coastal Bend Weather · Nueces County"). Neither is
     live data, yet the pill is the single largest non-data block still sitting
     directly above the temperature now that Round 2 hid the lede: ~22px box +
     its bottom margin ≈ 31px of pure marketing chrome between viewport-top and
     the datum the phone user came for. Hiding it (kept in the DOM for SEO +
     desktop, which is untouched) pulls the temp AND — more importantly — the
     detail-chip rows up ~31px on every page, bringing the second chip row
     (Wind/Rain, the real information scent) measurably closer to / inside the
     667 fold. Pure additive CSS: no HTML/JS/ID changes, desktop unchanged. */
  .hero .eyebrow { display: none; }
  .hero h1 {
    /* Override the clamp floor (1.7rem) and cap the H1 to 2 lines so it stops
       eating fold height. After Rounds 1-4 hid the eyebrow + lede, this H1 is
       now the single largest non-data block above the temperature on BOTH the
       apex and the 17 city pages — and the longer city titles ("Corpus Christi,
       Texas — live weather + storm tracker") still wrap to ~3 lines here.
       Dropping the size 1.35rem -> 1.18rem and clamping to 2 lines pulls the
       temp, condition, AND the whole detail-chip grid up ~25px on every page,
       moving the 2nd chip row (Wind/Rain — the real information scent) closer
       to / inside the 667 fold. The full title text stays in the DOM + a11y
       tree (line-clamp only hides visual overflow); desktop is untouched. */
    font-size: 1.18rem; line-height: 1.22; margin-bottom: 0.3rem;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* Fallback for engines without -webkit-line-clamp: hard-cap at ~2 lines. */
    max-height: 2.9em;
  }
  /* The lede is pure marketing restatement of what the live card + sections
     already show. On a phone the user came for the temperature, not the pitch,
     and the lede is the single largest non-data block sitting directly above
     the live card — ~84px on apex, ~105px (5 lines) on the longer city-page
     ledes. Hide it before the fold; it stays in the DOM for SEO/desktop where
     it has room (desktop rules are untouched). This is the biggest remaining
     fold reclaim on the 17 city pages, which never got it in Round 1. */
  .hero .lede { display: none; }

  /* Hide the apex quick-nav before the fold (apex only — city pages have no
     .quick-nav). After Rounds 1-6 the temperature itself clears the 375x667
     fold, but this scrollable pill row is the single largest remaining
     NON-DATA block still wedged directly between the H1 and the #weather data
     card: ~44px row + 0.6rem (~10px) margin-top ≈ 54px of pure navigation
     chrome the phone user did not come for. It is also purely DUPLICATIVE —
     all 5 targets (#weather/#forecast/#cities/#alerts/#tropical) sit
     immediately below in source order, the sticky-topbar "NPC Weather" home
     link covers wayfinding, and the in-card city picker handles city choice.
     Removing it on mobile pulls the temp, condition AND the entire detail-chip
     grid up ~54px, seating the 2nd chip row (Wind/Rain — the real information
     scent) comfortably inside the 667 fold. Round 2 already converted it to a
     single horizontally-scrollable row; this supersedes that (those overflow
     rules are now moot at this breakpoint), so they're removed to avoid dead
     CSS. The nav stays in the DOM for SEO and renders normally on desktop
     (>600px, untouched). Pure additive: no HTML/JS/ID changes. */
  .quick-nav { display: none; }
  .weather-section { padding-top: 0.25rem; }
  .primary-card { padding: 1.1rem; }

  /* Tighten the in-card city-picker / pin-label row gap on mobile. This row
     (the apex picker button, and the equivalent "📍 City, TX" label city.js
     injects on all 17 city pages) is now the single largest non-data block
     left INSIDE the card between its top edge and the temperature. Cutting its
     bottom margin 0.85rem -> 0.5rem pulls the temp, condition, and the entire
     detail-chip grid up ~6px uniformly on every page — seating the 2nd chip
     row (Wind/Rain, the real information scent) further inside the 667 fold and
     adding headroom on 360px-wide / shorter viewports. Spacing-only; the picker
     tap target is unchanged (button padding untouched) and no IDs move. The
     city-page label carries an inline margin, so its value is also updated in
     city.js to keep all 18 pages in lockstep. */
  .city-picker-row { margin-bottom: 0.5rem; }

  /* Hide the redundant in-card pin label on mobile (17 city pages only).
     city.js (renderCurrent) injects `.city-pin-label` ("📍 Corpus Christi, TX")
     as the FIRST child of the .primary-card on every city page — but that text
     restates the page H1 directly above it verbatim ("Corpus Christi, Texas —
     live weather…", city HTML L89). It is the single largest non-data block
     between the card's top edge and the temperature that exists ONLY on the 17
     city pages: ~17px label + 0.5rem (~8px) margin ≈ 23px of pure duplication.
     The apex card uses that same in-card slot for the FUNCTIONAL city picker
     (index.html L136-144), so this only affects the city pages. Hiding it on
     mobile pulls the temperature, condition, AND both detail-chip rows up ~23px,
     seating the 2nd chip row (Wind / Rain — the real information scent) higher in
     the 667 fold and matching the apex card's top-of-card rhythm. The label is
     scoped to #current-section so it can't ever touch the apex; it stays in the
     DOM (and visible on desktop, untouched) for SEO + larger viewports — same
     inline-class + CSS lockstep pattern Rounds 8 and 17 used. Pure additive CSS
     plus a one-class addition in city.js; no IDs moved, no structure changed. */
  #current-section .city-pin-label { display: none; }

  /* Tame the oversized weather emoji so the TEMPERATURE is unambiguously the
     hero (apex #emoji.big-emoji + the .big-emoji city.js injects on all 17
     city pages). The base size is clamp(3.5rem, 11vw, 6rem); at 375px 11vw is
     ~41px, BELOW the 3.5rem floor, so the emoji is pinned at 56px — nearly as
     tall as the temp (its clamp(4rem,13vw,7rem) is likewise pinned at its 64px
     floor here). A 56px glyph next to a 64px number reads as a CO-EQUAL element
     competing with the live datum for first fixation, and it sets the row
     height. Override to 2.6rem (~42px): the temp now clearly dominates (priority
     2 — live data reads as the hero), the glyph stays plainly legible, and the
     shorter emoji trims ~14px off the temp row, pulling the condition line and
     the detail-chip grid up on every page. Pairing it with align-items:flex-start
     on .temp-emoji-row top-aligns the smaller emoji to the temp's cap height
     instead of floating it mid-row. Mobile only; the desktop clamp (>600px) is
     untouched. Pure CSS: no HTML/JS/ID changes. */
  .big-emoji { font-size: 2.6rem; }
  .temp-emoji-row { align-items: flex-start; }

  /* Tighten the in-card temp → condition → chips vertical rhythm on mobile.
     Inside the data card, two gaps are still at their DESKTOP values on phones —
     no prior round touched them: .cond-row margin-top (base 0.75rem ≈ 12px, L193)
     and .detail-chips margin-top (base 0.85rem ≈ 13.6px, L209). Stacked, that's
     ~26px of in-card whitespace between the 64px temperature and the chip grid,
     pushing the 2nd chip row (Wind / Rain — the real information scent) lower in
     the fold than it needs to be. Trimming them to 0.4rem + 0.5rem reclaims ~14px
     with ZERO content loss, so the live readout (temp → condition → both chip
     rows) clusters tighter as one unit and the Wind/Rain row rises measurably
     higher into the 667 fold — also buying headroom on 360px-wide / shorter
     (640px-tall) viewports. Reaches BOTH layouts: the apex #primary-card .cond-row
     + #detail-chips static HTML, and the .cond-row / .detail-chips that city.js
     injects into #city-current on all 17 city pages. NOTE: city.js writes an
     inline margin-top:0.85rem on the injected .detail-chips (city.js L79), which
     would beat this CSS rule by specificity, so that inline value is updated to
     0.5rem in lockstep — same approach Round 8 used for the pin-label margin.
     Spacing-only: no IDs, no structure, desktop (>600px) untouched. */
  .primary-card .cond-row { margin-top: 0.4rem; }
  .detail-chips { margin-top: 0.5rem; }

  /* Stop the Wind detail-chip value from wrapping to a 2nd line on phones
     (apex static chips + the identical chips city.js injects on all 17 city
     pages). The .detail-chips grid is 2-column on mobile — the 4-up layout only
     starts at min-width:640px (L316-318) — and the base .chip-value (L334, the
     ONLY definition; no other mobile override) is 1.4rem/weight-700 with NO wrap
     control. Inside the card's 1.1rem mobile padding, a 2-col chip is only
     ~163px wide (~132px usable for the value at 375px, ~124px at 360px). The
     Wind value uses the gust format "<spd> · <gust>g mph" (app.js L150-154 /
     city.js L59-63), which at high wind ("62 · 78g mph") renders ~138px at
     1.4rem — wider than the chip — so it WRAPS to a 2nd line, and CSS-grid rows
     size to the TALLEST cell, so that one wrap makes the whole Wind/Rain chip
     row ~22px taller, pushes the chips lower in the fold, and reads ragged —
     precisely on gusty/storm days when wind is the most-wanted datum. At 1.2rem
     the longest gust string renders ~119px and fits one line at both 375 and
     360px while staying clearly the chip's hero number (vs the 0.7rem label).
     Desktop (>=640px, where chips go 4-up with ample room) keeps the 1.4rem
     base, untouched. Pure type-size; no JS/ID/structure change, no added fold
     height (it can only REDUCE chip-row height), zero CLS. */
  .chip-value { font-size: 1.2rem; }

  /* Compress the sticky topbar on mobile (apex + all 17 city pages).
     The topbar is position:sticky (see base rule ~L32), so its height is the
     ONLY non-data block above the temperature that ALSO permanently occludes
     the data on every scroll — each saved pixel pays twice (initial fold +
     scroll overlay). On mobile its .container has flex-wrap:wrap + 0.85rem
     top/bottom padding, so the ~58-76-char .topbar-note wrapped BELOW the
     brand to a 2nd line, making the bar ~80px tall. Force the bar onto ONE
     line and trim the vertical padding 0.85rem -> 0.5rem (bar ~80px -> ~44px,
     reclaiming ~36px above the data AND shrinking the permanent sticky overlay
     by the same amount). Desktop (>600px) is untouched. */
  .topbar .container {
    padding-top: 0.5rem; padding-bottom: 0.5rem;
    flex-wrap: nowrap;
  }
  .brand { font-size: 0.95rem; flex: 0 0 auto; }

  /* Drop the topbar-note on mobile so the live-temp mirror owns the bar cleanly.
     The sticky bar is the ONLY chrome that carries the live datum (the R19 temp
     mirror) at EVERY scroll position on a phone, yet on a ~360-375px bar it split
     its width three ways: .brand (~110px) + #topbar-temp (~70-80px) left the note
     only ~150px for a 55-75 char string. Right-aligned + ellipsised, the note
     rendered as an unreadable fragment jammed against the temp with a 0.5rem gap —
     low-value attribution/marketing crowding the one genuinely useful datum. The
     note is fully redundant on mobile: its attribution is duplicated in the footer,
     and its wayfinding (apex: tagline; city: "All Coastal Bend cities" link) is
     already covered by the .brand home link. Hiding it leaves a clean two-item bar
     — brand pinned left, live temp+glyph pinned hard-right — so the most-wanted
     info reads instantly at any scroll depth. The note stays in the DOM (SEO) and
     renders normally on desktop (>600px, untouched). Pure CSS; the R19 mirror logic
     in app.js / city.js is unchanged. */
  .topbar-note { display: none; }
  /* With the note gone the bar is just brand + temp; pin the live temp hard-right
     (was margin-left:0.5rem when it trailed the flex:1 1 auto note). Also override
     the base width reserve: the base rule (L85) caps the mirror at
     calc(100% - 7rem)=112px, a figure sized for the DESKTOP bar that still carried
     .topbar-note. On mobile the note is display:none (above), so the bar is only
     the ~108px brand (flex:0 0 auto, 0.95rem/800) + this mirror, yet the stale 7rem
     cap still truncated longer OpenWeather conditions ("84° · Heavy intensity rain"
     -> "84° · Heavy inten…") — losing the exact "is it storming NOW?" scent the
     mirror exists to deliver, and it is the ONLY live datum visible once a phone
     scrolls past the hero. 8.5rem (136px) cleanly clears the brand + its 0.5rem flex
     gap with a small safety margin while giving the condition word ~24px more room,
     so full strings now read on 320-375px phones. The R28 tabular-nums and the
     base white-space:nowrap + overflow:hidden + text-overflow:ellipsis are
     untouched, so a genuinely over-long string still truncates to one clean line.
     Landing/width only — zero fold/CLS/element-ID/JS change. */
  .topbar-temp { font-size: 0.9rem; margin-left: auto; max-width: calc(100% - 8.5rem); }

  /* Match the anchor scroll-offset to the COMPRESSED mobile bar (~44px: 0.5rem×2
     padding + ~0.95rem brand line + 1px border, set just above). The base offset
     is 3.25rem/52px (sized for the taller ~50px desktop bar); on the shorter
     mobile bar that 52px would leave a slightly larger gap, so trim to 3rem/48px
     here — heading lands flush under the bar with a clean ~4px breathing gap.
     This matters most for the red alerts-banner jump to #alerts / #storm-section
     during a live NWS warning, where the targeted heading must read instantly.
     Affects only scroll-to-anchor landing — no fold/CLS/paint change. */
  section[id], a[id] { scroll-margin-top: 3rem; }
  /* Match the scroll container's top padding to the same compressed ~44px mobile
     bar so the R25 skip-link's #main (a <main>, unmatched by the scroll-margin
     selector above) and any browser find-on-page land flush under the bar, not
     behind it. Mirrors the 3.25rem→3rem desktop/mobile step the scroll-margin
     rules use. Landing-position only — no fold/CLS/paint change. */
  html { scroll-padding-top: 3rem; }

  /* Hide the redundant section heading immediately above the live card
     (apex #weather, city #current-section) — the in-card city pin + page
     H1 already announce it. Kept in the a11y tree + SEO, just zero visual
     cost at the fold. Visually-hidden (not display:none) preserves the
     landmark and the document outline. */
  #weather > .section-h,
  #current-section > h2 {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px; border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  /* Pull the 7-day forecast strip UP into the fold (apex + all 17 city pages).
     Rounds 1-9 won the north-star: at 375x667 the temp + condition + both chip
     rows clear the fold, with the chips ending ~y436 — leaving ~230px of EMPTY
     fold below them before anything else renders. That void is the single
     biggest remaining information-scent waste: the next real datum (tomorrow's
     forecast) sits below it, pushed down by (a) the card section's fat 2rem
     bottom padding and (b) an oversized, full-marketing-weight forecast heading.
     Per the priority order (item 1 then 2), the highest-value next move is to
     fill that whitespace with the NEXT most valuable LIVE DATA — the forecast
     tiles — so the first day cells (Today/Tomorrow hi-lo/rain) peek into the
     fold and give the user a reason not to scroll yet.

     Three mobile-only edits, all additive, no HTML/JS/ID changes:
     1) Close the dead gap between the chips and #forecast: the card section's
        trailing padding 2rem -> 0.75rem. */
  .weather-section { padding-bottom: 0.75rem; }
  /*  2) Zero the forecast section's OWN top padding so its heading couples
        tight to the card section above it (apex #forecast.weather-section). */
  #forecast.weather-section { padding-top: 0; }
  /*  3) Shrink every visible section heading on mobile so the chrome above the
        first datum costs far less fold. This reaches BOTH the apex .section-h
        (#forecast/#cities/#alerts/#tropical) AND the city pages' bare section
        <h2> (#forecast-section/#storm-section), which otherwise render at the
        UA default ~1.5em with ~0.83em top/bottom margins — a ~56px block of
        marketing-weight chrome sitting directly above the forecast tiles. We
        do NOT touch #current-section's h2 (already sr-only'd, Round 1) or the
        sr-only #weather .section-h. Desktop (>600px) is completely untouched. */
  .section-h,
  .cities-section h2,
  .storm-section h2,
  .info h2 {
    font-size: 1.05rem; margin-bottom: 0.5rem; padding-bottom: 0.35rem;
  }
  #forecast-section > h2,
  #storm-section > h2 {
    font-size: 1.05rem;
    margin: 0 0 0.5rem;
    line-height: 1.25;
  }

  /* Force the 17-city overview to a fixed 2-column grid on phones (apex only —
     city pages have no #city-grid). The base grid is
     repeat(auto-fill, minmax(165px, 1fr)) (L344-348): inside the cities
     section's 1.25rem side padding, a 375px viewport leaves ~335px of content
     width, and two 165px tracks + the 0.6rem gap need ~340px — just over — so
     auto-fill silently collapses the whole grid to ONE column on every phone,
     stranding ~half the row width as empty gutter and doubling the scroll
     needed to skim all 17 cities (the section's entire job). Pinning it to two
     equal columns here packs the chips two-up: each track becomes ~163px (the
     chips already render at that width — it's the base minmax floor), the
     overview reads in half the vertical run, and the city the user just
     highlighted (R2 .is-current) is reachable in far less scroll. Apex-only and
     well below the fold, so zero fold/CLS/a11y/ID/JS impact; desktop (>600px,
     where auto-fill correctly yields 3-4 columns) is untouched. */
  .city-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* 17 chips in 2 columns = 9 rows. At ~80px/chip + 0.6rem gaps that's
       ~800px, but the trailing odd chip leaves the 9th row half-empty, so
       reserve a slightly conservative ~720px here (overrides the 340px base
       reserve) — enough to absorb the bulk of the first-paint jump without
       opening a large empty gap if chips render a touch shorter. */
    min-height: 720px;
  }
  /* Complete the established "data out-weighs the decorative weather glyph"
     hierarchy (R9 mobile hero 2.6rem, R31 desktop hero 4rem, R32 forecast
     cells) on the LAST surface that still inverts it: the R3 fixed 2-col
     mobile 17-city grid, whose entire job is "skim 17 temps fast". The base
     rule (L642) gives .cc-emoji 1.3rem vs .cc-temp 1.6rem (L641), so on a
     phone the decorative glyph renders ~81% of the temp height AND is flung to
     the chip's far-right edge by margin-left:auto — it competes with the temp
     for first fixation and the gap between them reads as a layout artifact.
     Dropping it to 1.05rem (~66% of the temp) makes the temperature the
     unambiguous first fixation down both columns while keeping the glyph
     plainly legible as a supporting cue. ZERO CLS / ZERO fold impact: this is
     apex-only (city pages have no #city-grid), the grid is far below the fold,
     and .cc-temp-row is align-items:baseline with its height driven by the
     taller 1.6rem temp — a smaller emoji cannot grow the row. The base 1.3rem
     stays for >600px, where chips are wider and 3-4-up with room. margin-left,
     line-height, the emoji's aria-hidden, and every element ID are untouched. */
  .cc-emoji { font-size: 1.05rem; }

  /* Grow the city-picker button to a >=44px touch target on phones WITHOUT
     moving it visually (apex only — the city pages' injected #picker-label is a
     plain non-interactive label, not this button). The picker is the single
     most-used mobile control — it's the only way to change city — yet its base
     style (L165-171) is `padding:0` + `font-size:0.72rem`, so its rendered hit
     area is just the ~16px line-box: about a third of the 44px minimum (WCAG
     2.5.5 / Apple HIG), making the page's primary interaction the hardest to
     tap. It also sits in the data card ABOVE THE FOLD, so this is the worst
     ergonomics gap hiding behind the prior fold rounds. We enlarge the hit box
     with symmetric padding and cancel its layout effect with an equal negative
     margin, so the button's visual baseline, the chip row height, and therefore
     the hard-won temperature position below it are ALL unchanged (zero added
     fold height, zero CLS). inline-flex + align-items:center (base) means
     min-height vertically centers the label inside the taller box. The button
     is in a position:relative row whose dropdown anchors to top:100% of the
     row, not the button, so the larger button does not shift the dropdown.
     The negative margin is inset within the card's 1.5rem padding (it sits
     0.5rem in on each side), so it cannot collide with the card edge. */
  .city-picker-btn {
    padding: 0.55rem 0.4rem;
    margin: -0.55rem -0.4rem;
    min-height: 44px;
  }
}

/* ───── Desktop hero/forecast column (apex + all 17 city pages) ─────
   Rounds 1-15 poured nearly all effort into the mobile fold; desktop
   (>600px) has had essentially zero attention since the original base
   styles, and the live-data hero — the page's whole reason to exist —
   is the WORST-looking element on a wide screen. Every section shares
   one `.container { max-width:1100px }`, so the .primary-card renders
   ~1050px wide and `.temp-emoji-row { justify-content:space-between }`
   (base L174-177) pins the 7rem temp at the far-left edge and the 6rem
   emoji ~950px away at the far-right edge with a vast empty gulf between
   them. It reads as a layout bug, not a designed hero. The 7-day
   forecast strip has the same disease: `repeat(7,1fr)` across ~1050px
   makes each day-cell ~150px wide holding tiny centered content,
   marooned in whitespace.

   Fix (priority 2 — live data reads as the hero; sensible desktop
   layout): cap the current-conditions hero and the forecast strip to a
   tight ~680px reading column and seat the emoji directly beside the
   temperature so temp + emoji form one dense focal unit, with the chip
   row and forecast tiles aligned beneath at matching width. This is
   desktop-only (min-width:601px), placed AFTER the max-width:600px
   mobile block so it never reaches phones, and it touches NO base rule,
   NO mobile rule, and NO preserved element ID — so the Rounds 1-10 fold
   win cannot regress.

   Selectors cover both layouts:
   • apex — the card/chips are static HTML inside #weather.weather-section;
     the forecast card is static inside #forecast.weather-section.
   • city pages — city.js injects the .primary-card + .detail-chips INTO
     the #city-current wrapper (so capping that wrapper caps both), and
     injects the .forecast-card INTO #forecast (no .weather-section). */
@media (min-width: 601px) {
  /* Constrain the live-conditions hero to a readable column instead of a
     stretched full-width band, AND center that column. Round 16 capped the
     card/chips/forecast to 680px but left them at margin-left:0, so inside the
     1100px .container they pinned hard-LEFT with a ~370px dead void down the
     entire right side of both the hero and the forecast — the live-data hero
     (the page's whole reason to exist) read as a narrow band floating in empty
     space, i.e. like a layout bug on any screen wider than ~1024px. Adding
     margin-inline:auto centers the column so it reads as a deliberately
     composed focal panel at every desktop width.

     CRUCIAL cohesion detail: the matching SECTION HEADERS (.section-h, with the
     full-width bottom-border separator) must be capped + centered to the SAME
     680px so the header rule line shares the card's exact left/right edges. If
     only the card centered, its header's separator would still span the full
     1100px and start ~185px to the card's left — a worse misalignment than the
     void it replaces. Capping the headers too makes header + card + chips +
     forecast one clean centered column. Reaches both layouts: apex headers are
     #weather/#forecast > h2; city headers are #current-section/#forecast-section
     > h2 (the wrapper #city-current / #forecast holds the JS-injected card).
     Desktop-only (after the max-width:600px block), no base/mobile rule, no
     HTML, no JS, no preserved ID touched — the Rounds 1-10 fold win can't
     regress, and on city pages the mobile-sr-only #current-section h2 is
     untouched here (this block is >=601px only). */
  /* ONE shared content column: cap the SECTION CONTAINERS (not inner cards) so
     every section — the hero included — centers to the SAME width and all the
     left/right edges align top-to-bottom. Inner cards/chips/forecast then fill
     their column. This kills the old bug where the hero spanned ~1100 while the
     cards sat in a 680 island ~190px to its right. */
  .hero, #weather, #forecast, #cities, #alerts, #tropical,
  #current-section, #forecast-section, #storm-section, .info,
  .topbar .container {
    max-width: 880px;
    margin-inline: auto;
  }
  .primary-card, .detail-chips, .city-current, .forecast-card,
  #weather > h2, #forecast > h2, #current-section > h2, #forecast-section > h2 {
    max-width: none;
  }
  /* Current conditions (apex): big card on the LEFT + the 4 detail chips stacked
     on the RIGHT, filling the column so the card no longer has an empty right
     half. #weather holds .primary-card (#primary-card) + .detail-chips
     (#detail-chips) as siblings; the <h2> spans the full row above them. */
  #weather.weather-section {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(240px, 300px);
    column-gap: 1.5rem; row-gap: 0.9rem; align-items: start;
  }
  #weather.weather-section > h2 { grid-column: 1 / -1; }
  #weather > #primary-card { grid-column: 1; grid-row: 2; margin: 0; }
  /* Chips as a 2x2 block that stretches to the card's natural height (no
     align-items:stretch on the grid — that blew the card to 485px with an empty
     lower half). The card keeps its real height; the chips fill the column. */
  #weather > #detail-chips {
    grid-column: 2; grid-row: 2; margin: 0; align-self: stretch;
    display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 0.6rem;
  }
  #weather > #detail-chips .chip {
    display: flex; flex-direction: column; justify-content: center;
  }
  /* Seat the emoji beside the temperature (was flung to the far-right edge
     by the base justify-content:space-between) so they read as one unit. */
  .primary-card .temp-emoji-row {
    justify-content: flex-start;
    gap: 1.75rem;
    align-items: center;
  }
  /* Tame the DESKTOP hero emoji so the temperature is unambiguously the hero —
     carrying R9's mobile principle to desktop (apex #emoji.big-emoji + the
     .big-emoji city.js injects on all 17 city pages). R9 (L963-977) documented
     that an emoji nearly as tall as the temp "reads as a co-equal element
     competing for first fixation" and capped it to 2.6rem on mobile, but desktop
     was never addressed: the base .big-emoji is clamp(3.5rem,11vw,6rem) and
     .big-temp is clamp(4rem,13vw,7rem), so across the whole desktop range the
     glyph renders at ~82-86% of the temp height (at >=1100px both hit their caps
     -> temp ~112px, emoji ~96px). Worse, the rule just above seats that near-
     equal glyph DIRECTLY beside the temp (flex-start + 1.75rem gap + center), so
     on the widest, most-composed view the two read as co-focal twins and the
     headline number — the site's whole "live data is the hero" mandate — does not
     clearly win. Cap the desktop glyph at 4rem (~64px) so the temp's clamped max
     (~112px) is ~1.75x taller and the clear first fixation, while the emoji stays
     plainly legible as a supporting glyph. The row is align-items:center and its
     height is driven by the TALLER temp, so shrinking the emoji causes no row
     reflow — zero CLS/fold/height change. Desktop-only (this @media is
     min-width:601px); the mobile 2.6rem rule (L977) and the base clamp are
     untouched. Pure font-size: no HTML/JS/ID change; ships to all 18 pages from
     this one shared edit. */
  .big-emoji { font-size: 4rem; }
  /* Match the forecast strip to the hero column so its 7 day-cells aren't
     marooned in whitespace — aligned, denser, visually tied to the hero. */
  #forecast.weather-section .forecast-card,
  #forecast > .forecast-card { max-width: none; margin: 0; }

  /* ── Extend the centered 680px reading column to the WHOLE page ──
     R12/R16 built a tight centered 680px column for the live-data hero +
     7-day forecast (and crucially capped THOSE two section-header separators
     to 680px so the rule lines share the card's edges, above). But the column
     logic stopped there: every section BELOW the forecast — #cities, #alerts,
     #tropical, the two .info blocks (apex), and #storm-section + the two
     .info/.disclaimer blocks (city pages) — still rendered at the full
     ~1050px .container content width. Two failures resulted on any screen
     wider than ~1024px: (1) HEADER-SEPARATOR DISCONTINUITY — the forecast
     header's bottom-border is a centered ~680px rule, then the very next
     header's separator is a full ~1050px rule starting ~185px to its left,
     and every header further down likewise, so the careful rhythm R12
     established broke exactly one section down and the page read as
     "designed hero, then unstyled full-width dump." (2) PROSE MEASURE — the
     alerts/tropical/info/sources body copy ran edge-to-edge at ~1050px, far
     past a comfortable ~65-75ch measure (the design already caps .lede at
     720px for exactly this reason, but nothing below the hero did).

     Fix: cap + center to the SAME 680px token (a) every remaining section
     HEADER so its separator shares the hero column's left/right edges
     top-to-bottom, and (b) the prose/panel BODIES (alerts + tropical panels,
     the section intro <p>s, and the .info paragraphs/subheads/lists) so all
     body copy gets the hero's measure. The one genuinely full-width thing
     below the hero — the 17-city GRID, which WANTS its 3-4 columns — is
     deliberately EXCLUDED from the cap and pinned back to full width
     defensively, so its layout is untouched. The apex #cities header carries
     .section-h AND matches `.cities-section h2`; capping that header (not the
     grid) is exactly the alignment we want. The city #storm-section/.info
     headers are reached by tag within their section. Desktop-only (this whole
     block is min-width:601px, placed after the max-width:600px mobile block),
     so the Rounds 1-15 above-the-fold mobile win is structurally untouchable;
     no base rule, no mobile rule, no JS, no HTML, no preserved element ID is
     changed — pure additive CSS that ships to all 18 pages from this one
     shared edit. */
  /* Body prose stays readable within the column (left-aligned, not re-centered). */
  #alerts > p, #tropical > p, #storm-section > p,
  .info > p, .info > ul { max-width: 70ch; }
  .city-grid { max-width: none; }
}

/* ───── Coastal Bend Outlook card (apex + all 17 city pages) ─────
   Additive block, placed AFTER the frozen "ONE shared content column"
   @media(min-width:601px) block above — it does NOT modify that locked block.
   #outlook ships as a NEW <section id="outlook" class="weather-section"> between
   #weather and #forecast (apex) / injected before #forecast-section (city pages),
   so it inherits the base .weather-section padding. The locked desktop block caps
   #weather/#forecast/#cities/etc. to the 880px reading column but does NOT list
   #outlook, so on desktop it would otherwise stretch full-width and break the
   column rhythm a human just fixed — the desktop rule below realigns it to the
   SAME 880px centered column (the prompt's exact additive hint). Uses ONLY the
   existing token palette: navy card (#0f172a/#0c1a35), cyan #38bdf8 accent, amber
   #f59e0b / red #ef4444 state accents, slate text (#e2e8f0/#cbd5e1/#94a3b8). */
.outlook-card {
  background: linear-gradient(135deg, var(--bg-grad-a) 0%, var(--bg-grad-b) 100%);
  border: 1px solid rgba(56, 189, 248, 0.22);
  border-left: 3px solid var(--accent);
  border-radius: 1rem;
  padding: 1.25rem 1.4rem;
  box-shadow: inset 0 0 0 1px var(--border-2);
}
.outlook-headline {
  font-size: 1.12rem; font-weight: 700; color: var(--text-body);
  margin: 0 0 0.55rem; line-height: 1.4;
}
.outlook-body { color: var(--text-2); line-height: 1.6; margin: 0 0 0.6rem; max-width: 70ch; }
.outlook-body:empty { display: none; }
/* The work-nudge is the cite-bait job call ("Concrete pour: GOOD — 78°F…"); give
   it a faint cyan tint + inset rule so it reads as a distinct callout, not body
   prose, while staying on-theme. Collapses cleanly when JS leaves it empty. */
.outlook-worknudge {
  color: var(--text-body); font-size: 0.92rem; line-height: 1.5;
  margin: 0 0 0.7rem; padding: 0.55rem 0.8rem;
  background: rgba(56, 189, 248, 0.06);
  border-radius: 0.6rem;
  box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.16);
}
.outlook-worknudge:empty { display: none; }
.outlook-foot { font-size: 0.78rem; color: var(--text-muted); margin: 0; line-height: 1.5; }
.outlook-foot strong { color: var(--text-2); }
/* State accents — driven by an active NWS warning / freeze / OSHA high+extreme
   heat / strong wind / rain, NOT by per-trade verdicts (a later round). Amber ties
   to the brand logo bolt (#f59e0b); red matches the alerts-banner family. */
.outlook-card.is-warn { border-left-color: var(--warn); }
.outlook-card.is-alert { border-left-color: var(--bad); }
/* Dim while the selected city's read is being fetched (mirrors .primary-card.fetching). */
.outlook-card.fetching { opacity: 0.5; transition: opacity 220ms ease; }
/* Realign to the SAME 880px centered reading column as the locked block's
   sections, so #outlook shares the hero/forecast left/right edges on desktop. */
@media (min-width: 601px) {
  #outlook { max-width: 880px; margin-inline: auto; }
}
/* Windows High Contrast: the card's cyan border + inset shadow are force-stripped
   like every other boundary; restore a real border so the Outlook stays a distinct
   panel (mirrors the forced-colors block's treatment of .primary-card/.chip). */
@media (forced-colors: active) {
  .outlook-card { border: 1px solid CanvasText; }
  .outlook-worknudge { border: 1px solid CanvasText; }
}

/* ───── Contractor work-windows grid (R2 — apex + all 17 city pages) ─────
   A compact, scannable 4-trade matrix (Concrete · Paint · Roofing · Tree work)
   inside #outlook-card, AFTER #outlook-worknudge and BEFORE #outlook-foot, so it
   inherits the card's 880px column AND sits above the disclaimer foot (the legal
   pairing stays the last thing read). Each cell is the npclocal deep-link for that
   trade+city. Verdict-first: the GOOD/CAUTION/NO-GO WORD is the high-contrast
   signal, color is a SECONDARY cue (left-border accent), never the only one
   (WCAG 1.4.1). Mobile-first 2-col → 4-col ≥601px (matching the column breakpoint).
   Additive only; placed after the locked desktop block, touches no preserved ID.
   Tokens: navy ground, cyan #38bdf8 default accent, green #22c55e/#86efac (the
   universal GOOD/safe signal — the minimal new token, reads cleanly on navy),
   amber #f59e0b/#fcd34d, red #ef4444/#fca5a5 — all already in the alerts family. */
.outlook-work { margin: 0 0 0.7rem; }
.outlook-work-h {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--text-muted); margin: 0 0 0.5rem; line-height: 1.4;
}
.outlook-work-sub { display: block; font-weight: 600; letter-spacing: 0.04em; text-transform: none; color: var(--text-faint); font-size: 0.7rem; margin-top: 0.15rem; }
.work-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
.work-cell {
  display: flex; flex-direction: column; gap: 0.25rem;
  padding: 0.6rem 0.7rem; border-radius: 0.6rem; text-decoration: none;
  background: rgba(148, 163, 184, 0.06);
  box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.18);
}
.wc-top { display: flex; align-items: center; gap: 0.4rem; }
.wc-icon { font-size: 0.95rem; line-height: 1; }
.wc-trade { font-weight: 700; font-size: 0.82rem; color: var(--text-body); }
.wc-verdict { font-size: 0.74rem; font-weight: 800; letter-spacing: 0.08em; }
.wc-reason { font-size: 0.78rem; line-height: 1.4; color: var(--text-muted); }
.wc-cta { margin-top: 0.1rem; font-size: 0.72rem; font-weight: 600; color: var(--link); }
/* UNIFORM cards (the different-colored borders confused users). The ONLY status
   cue is the colored VERDICT word inside each card — small, consistent, and still
   color-blind-safe because the word itself says GOOD / CAUTION / NO-GO. */
.work-cell.wc-good    .wc-verdict { color: var(--ok); }
.work-cell.wc-caution .wc-verdict { color: var(--warn); }
.work-cell.wc-no-go   .wc-verdict { color: var(--bad); }
.work-cell.wc-na      .wc-verdict { color: var(--text-muted); }
.work-cell.wc-na { opacity: 0.85; }
a.work-cell:hover, a.work-cell:focus-visible {
  background: rgba(56, 189, 248, 0.10);
  outline: 2px solid var(--focus-ring); outline-offset: 1px;
}
@media (min-width: 601px) { .work-grid { grid-template-columns: repeat(4, 1fr); gap: 0.6rem; } }
@media (forced-colors: active) {
  .work-cell { border: 1px solid CanvasText; }
}

/* ───── Crew heat & exposure safety panel (R3 — apex + all 17 city pages) ─────
   A distinct callout INSIDE #outlook-card, AFTER #outlook-work and BEFORE
   #outlook-foot, so the disclaimer stays the last thing read. It inherits the
   card's 880px reading column (the card itself is realigned by the additive
   #outlook desktop rule above — this block adds NO new layout breakpoint and does
   NOT touch the locked "ONE shared content column" block). Faint-cyan ground like
   .outlook-worknudge so it reads as its own panel on the navy card. The zone pill
   carries the OSHA word in a zone color; color is never the only signal — the
   zone WORD is always text, and the panel's full read is spoken via aria-label.
   Tokens reuse the alerts/work family: cyan #38bdf8, amber #fcd34d, red #ef4444,
   green #86efac, slate text — the High-zone salmon #fca5a5 is already in use by
   .work-cell.wc-no-go .wc-verdict, so no genuinely new token is introduced. */
.outlook-heat {
  margin: 0 0 0.7rem; padding: 0.6rem 0.8rem;
  background: rgba(56, 189, 248, 0.05);
  border-radius: 0.6rem;
  box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.14);
}
.outlook-heat:empty, .outlook-heat[hidden] { display: none; }
.oh-h {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--text-muted); margin: 0 0 0.4rem;
  display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
}
.oh-ico { font-size: 0.95rem; line-height: 1; }
.oh-zone {
  font-weight: 800; letter-spacing: 0.06em; font-size: 0.7rem;
  padding: 0.05rem 0.4rem; border-radius: 0.3rem;
}
.oh-lower    { color: var(--ok); }
.oh-moderate { color: var(--warn); }
.oh-high     { color: var(--bad); }
.oh-extreme  { color: var(--text); background: #ef4444; }
.oh-now   { color: var(--text-body); font-size: 0.9rem; line-height: 1.5; margin: 0 0 0.35rem; }
.oh-guide { color: var(--text-2); font-size: 0.85rem; line-height: 1.5; margin: 0 0 0.35rem; }
.oh-guide:last-child { margin-bottom: 0; }
.oh-haz {
  margin: 0; padding-left: 1.1rem; color: var(--warn);
  font-size: 0.82rem; line-height: 1.45;
}
.oh-haz li { margin: 0.1rem 0; }
@media (forced-colors: active) {
  .outlook-heat { border: 1px solid CanvasText; }
  .oh-extreme { border: 1px solid CanvasText; }
}

/* loading + error states */
.loading {
  color: var(--text-muted); font-style: italic; font-size: 0.9rem;
}
.result-error {
  color: var(--bad); padding: 0.75rem 1rem; background: rgba(127,29,29,0.3);
  border-radius: 0.6rem; box-shadow: inset 0 0 0 1px rgba(248,113,113,0.3);
}

/* ───── No-JS / failed-JS fallback panel (apex + all 17 city pages) ─────
   The 18 JS-driven pages depend ENTIRELY on app.js / city.js to swap their
   static "Loading conditions… / Loading forecast… / Loading…" placeholders for
   live data. Any visitor with JS disabled — OR, more importantly, any case where
   the script never executes (a Cloudflare 5xx on the asset, a content-blocker /
   corporate proxy stripping it, a future parse error) — was permanently stranded
   on frozen italic "Loading…" with no temperature, no explanation, and no path to
   the data. A <noscript> block (the first child of <main>) renders ONLY when
   scripting is off, so it is invisible and weightless for the JS-on majority:
   zero fold cost, zero CLS, no a11y-tree node when JS runs, no element-ID/JS
   change. It reuses the existing .result-error panel token (.ns-panel below) so
   it adds no new visual language, and — by linking weather.gov/crp + tdem.texas.gov
   — turns a dead-end into a graceful fallback that honors the site's own
   "the .gov sites are authoritative" stance. The wrapper carries .container in the
   HTML so it gets the standard side padding on every page without depending on
   section context. Defined once → ships to all 18 pages from this one shared file. */
.noscript-fallback { padding-top: 1.5rem; padding-bottom: 0.5rem; }
.noscript-fallback .ns-panel { padding: 1.1rem 1.25rem; }
.noscript-fallback h2 {
  margin: 0 0 0.5rem; color: var(--bad); font-size: 1.1rem;
}
.noscript-fallback p { margin: 0 0 0.6rem; color: var(--bad); font-size: 0.95rem; line-height: 1.55; }
.noscript-fallback p:last-child { margin-bottom: 0; }
.noscript-fallback a { color: var(--text); text-decoration: underline; font-weight: 700; }
.noscript-fallback a:hover { color: var(--bad); }

/* ───── Accessibility: skip-to-content link (WCAG 2.4.1 Bypass Blocks, Level A) ─────
   The last remaining Level-A gap, on all 18 pages: every page opens with a
   position:sticky .topbar (brand home link + attribution link) and, on desktop,
   a 5-item .quick-nav — so a keyboard or switch user tabs through 2 (mobile) to
   7 (desktop) chrome controls on EVERY page before reaching the live temperature
   the whole site exists to surface, and <main> carried no id to target. Sixteen-
   plus rounds put the data first VISUALLY; this finally makes FOCUS order honor
   the same data-first priority, letting a keyboard user jump straight to the
   weather in one keypress (href=#main → the now-id'd, tabindex=-1 <main>).

   It reuses the file's two existing idioms so it adds no new visual language and
   zero regression for pointer users: (a) the clip/1px visually-hidden pattern
   (same as the mobile sr-only section headings) keeps it OUT of the layout until
   focused — it never paints for mouse/touch, costs no fold height, and causes no
   CLS; (b) on :focus it animates into the top-left as a solid cyan-on-navy pill
   ABOVE the sticky bar (z-index:60 > the topbar's z-index:10) at a >=44px tap
   height, carrying the same #38bdf8/#0a1018 theme and the focus-ring glow used
   below. :focus (not :focus-visible) so it reliably appears for every keyboard
   path. Shared file → ships to the apex + all 17 city pages from one rule. */
.skip-link {
  position: absolute;
  left: 0.75rem;
  top: 0.75rem;
  z-index: 60;
  /* Hidden until focused: a 1px clipped box that sits in the DOM/a11y tree (so
     it's the first tab stop) but paints nothing and reserves no space. */
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  transform: translateY(-150%);
  background: var(--accent-fill);
  color: var(--on-accent);
  font-weight: 800;
  font-size: 0.9rem;
  letter-spacing: 0.01em;
  border-radius: 0.6rem;
  text-decoration: none;
  box-shadow: 0 4px 16px rgba(2, 6, 23, 0.55), 0 0 0 4px rgba(56, 189, 248, 0.25);
  transition: transform 140ms ease;
}
.skip-link:focus {
  /* Reveal: restore real size + the comfortable >=44px tap pill and slide it in. */
  width: auto;
  height: auto;
  padding: 0.75rem 1.1rem;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  overflow: visible;
  clip: auto;
  clip-path: none;
  transform: translateY(0);
  outline: none;
  text-decoration: none;
  color: var(--on-accent);
}
@media (prefers-reduced-motion: reduce) {
  .skip-link { transition: none; }
}

/* ───── Accessibility: keyboard focus + reduced motion (apex + all 17 city pages) ─────
   Priority-4 gap that no prior round touched: across all 18 pages there was
   NOT ONE :focus-visible rule and NO prefers-reduced-motion handling. With
   the north-star (data above the fold) won in Rounds 1-10, the next-weighted
   dimension is accessibility — and the two highest-leverage, lowest-regression
   wins are a visible keyboard-focus indicator on EVERY interactive element and
   honoring a user's reduced-motion preference. Both are global (not fold-
   specific), so they live outside the mobile @media block.

   1) FOCUS RING. Several interactive controls suppress their chrome on this
      dark theme — .city-picker-btn is `border:none; background:none`, the
      .quick-nav/.city-chip/.alerts-banner are styled <a>s — so the UA's thin
      default outline is all but invisible against #0a1018, leaving keyboard
      and switch users with no idea where focus is (WCAG 2.4.7 failure on every
      page). A single :focus-visible rule paints a theme-matched cyan (#38bdf8)
      2px outline with a 2px offset on every focusable element. :focus-visible
      (not :focus) means MOUSE clicks never show the ring — only keyboard/AT
      focus does — so there is zero visual regression for pointer users. The
      offset + a faint glow keep it legible on both the dark page bg and the
      lighter card/dropdown surfaces. The dropdown's own buttons get the ring
      inset so it isn't clipped by the dropdown's `overflow:hidden`. */
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 4px;
}
a:focus-visible,
.quick-nav a:focus-visible,
.city-chip:focus-visible,
.alerts-banner:focus-visible,
.city-picker-btn:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.25);
}
/* Dropdown buttons sit inside an overflow:hidden container, so an outset ring
   would be clipped — inset it instead and lift contrast on the focused row. */
.city-picker-dropdown button:focus-visible {
  outline: 2px solid #7dd3fc;
  outline-offset: -2px;
  background: var(--surface-3);
  color: var(--text);
}

/* 2) REDUCED MOTION. Users who set prefers-reduced-motion (vestibular
   sensitivity) should not get the smooth-scroll jump on every in-page anchor,
   the "Detecting location…" pulse, or the active-tropical pulsing glow.
   Neutralize them all: instant scroll, and animations collapsed to a single
   static frame (kept at full opacity / no glow so nothing disappears).
   They also should not get the .city-chip lift+scale (L592-605) or the
   safety-critical .alerts-banner lift (L698-711) — those are positional/scale
   transforms on interaction, exactly what WCAG 2.3.3 (Animation from
   Interactions) targets, and the skip-link already carries its own
   transition:none exemption (L1461) establishing this intent. Drop transform
   from their transition lists (keeping the harmless sub-200ms color/shadow
   fades for orientation) and zero the transformed states so no movement can
   fire; the on-brand background/box-shadow feedback is preserved for everyone. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .detecting-pulse,
  .tropical-pulse.active {
    animation: none;
  }
  .city-chip { transition: background 120ms ease; }
  .alerts-banner { transition: box-shadow 120ms ease; }
  .city-chip:hover,
  .city-chip:active,
  .alerts-banner:hover,
  .alerts-banner:active {
    transform: none;
  }
  /* DESIGN-T: kill the time-of-day theme cross-fade for motion-sensitive users.
     The exact-value 'reduce' query (this block) is the correct place — a phase
     flip then snaps instantly with no animated color sweep. Zeroing the token
     means the same selectors that opted into --tod-fade above get 0s here. */
  :root { --tod-fade: 0s; }
}

/* ───── Accessibility: Windows High Contrast / forced-colors mode (all 18 pages) ─────
   Priority-4 (accessibility) gap that NO prior round addressed: a grep confirms
   zero forced-colors handling and zero CSS system-color keywords anywhere in this
   file. EVERY boundary on the site — the .primary-card, the 4 .chip tiles, the
   .forecast-card + its 7 .forecast-day cell dividers, the 17 .city-chip tiles, the
   .city-picker-dropdown + its rows, the safety-critical .alerts-banner, and the
   .storm-content / .tropical-* panels — is drawn with `box-shadow: inset 0 0 0 1px …`
   (21 occurrences). When a user enables Windows High Contrast Mode (or any
   forced-colors theme), the engine STRIPS box-shadow entirely and force-overrides
   every background/border to the user's chosen system palette. The result today:
   all of those inset-shadow "borders" disappear at once and the whole UI flattens
   into one borderless wall — cards, chips, dropdown rows, and the forecast grid
   become visually indistinguishable, and the red alerts banner loses the chrome that
   sets it apart during a live NWS warning. That is a WCAG 1.4.11 (Non-text Contrast)
   structural failure for low-vision users — notably on Windows, the platform where
   HCM is most used.

   This block runs ONLY when the OS forces colors (`@media (forced-colors: active)`),
   so it is completely inert — zero bytes of effect — for the JS-on, non-HCM majority:
   no fold, CLS, layout, element-ID, JS, or normal-theme color change, and it cannot
   regress the above-the-fold win or any prior round. Inside HCM it re-expresses each
   stripped boundary as a real `1px solid` border in the user's palette (CanvasText),
   restores the forecast cell dividers (force-stripped the same way), and gives the
   alerts banner a `Highlight`-colored border so it stays visually distinct. It also
   re-asserts the keyboard focus ring in the system `Highlight` color (the base rule's
   fixed #38bdf8 outline + rgba glow are force-overridden, so spell it out to guarantee
   2.4.7 focus visibility survives). `forced-color-adjust:auto` is the default, so we
   only ADD borders — the system still controls fills/text, keeping the user's theme
   authoritative. The 1px borders sit where 1px inset shadows already sat, so even if
   borders were honored normally (they're not) there'd be no reflow. Shared file →
   all 18 pages from one edit. */
@media (forced-colors: active) {
  /* Re-draw every boundary that relied on a now-stripped inset box-shadow. */
  .primary-card,
  .chip,
  .forecast-card,
  .city-chip,
  .city-picker-dropdown,
  .storm-content,
  .tropical-storm,
  .tropical-pulse,
  .quick-nav a,
  .alerts-banner-cta {
    border: 1px solid CanvasText;
  }
  /* The 7 forecast cells are separated only by a left inset/border hairline that
     HCM strips; restore it as a real divider so the strip doesn't read as one block. */
  .forecast-day { border-left: 1px solid CanvasText; }
  .forecast-day:first-child { border-left: none; }
  /* Dropdown rows are divided by an inset hairline (stripped); restore a bottom rule. */
  .city-picker-dropdown button { border-bottom: 1px solid CanvasText; }
  .city-picker-dropdown button:last-child { border-bottom: none; }
  /* Keep the safety-critical alerts banner visually distinct in HCM: a Highlight
     border (vs the neutral CanvasText used elsewhere) sets it apart during a warning. */
  .alerts-banner { border: 2px solid Highlight; }
  /* Re-assert keyboard focus visibility (the fixed-cyan outline + glow are stripped). */
  :focus-visible,
  a:focus-visible,
  .quick-nav a:focus-visible,
  .city-chip:focus-visible,
  .alerts-banner:focus-visible,
  .city-picker-btn:focus-visible,
  .city-picker-dropdown button:focus-visible {
    outline: 2px solid Highlight;
    outline-offset: 2px;
  }
}

/* ───── Footer ───── */
.footer {
  border-top: 1px solid var(--border);
  padding: 1.5rem 1.25rem;
  background: var(--topbar-bg);
  margin-top: 2rem;
}
.footer p { margin: 0.25rem 0; color: var(--text-muted); font-size: 0.85rem; }
.footer .footer-sub { font-size: 0.78rem; }

/* ───── Coastal Bend Almanac (moon · plant-now zone 9a/9b · fortune) ─────
   Additive, AFTER the locked desktop block — aligns #almanac to the same 880px
   content column; on-theme cyan accents (npcweather palette, not emerald). */
@media (min-width: 601px) { #almanac { max-width: 880px; margin-inline: auto; } }
.almanac-card {
  background: linear-gradient(135deg, var(--bg-grad-a) 0%, var(--bg-grad-b) 100%);
  border-radius: 1rem;
  padding: 1.25rem 1.5rem;
  box-shadow: inset 0 0 0 1px var(--border);
  display: grid;
  gap: 0;
}
/* Each almanac row is its own block with a divider above it, so Moon / Plant /
   Fortune / Sun / Fishing read as distinct sections instead of running together. */
.alm-row { display: grid; gap: 0.4rem; padding: 0.95rem 0; border-top: 1px solid var(--border); }
.alm-row:first-child { padding-top: 0.15rem; border-top: none; }
.alm-row:last-child { padding-bottom: 0.15rem; }
.alm-label {
  margin: 0; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--link-hover);
}
.alm-zone, .alm-fun { color: var(--text-faint); font-weight: 600; letter-spacing: 0.02em; }
.alm-moon { margin: 0; font-size: 1.05rem; color: var(--text-body); }
.alm-moon-emoji { font-size: 1.35rem; }
.alm-moon-name { font-weight: 600; color: var(--text); }
.alm-moon-illum { color: var(--text-muted); font-size: 0.92rem; }
.alm-plant-crops { margin: 0 0 0.45rem; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.alm-chip {
  background: var(--accent-soft); color: var(--link-hover);
  border-radius: 999px; padding: 0.22rem 0.65rem;
  font-size: 0.85rem; font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.alm-plant-note { margin: 0; color: var(--text-2); font-size: 0.92rem; line-height: 1.5; }
.alm-fortune { margin: 0; color: var(--text-body); font-style: italic; font-size: 1.02rem; line-height: 1.55; }
.alm-foot { margin: 0.25rem 0 0; color: var(--text-faint); font-size: 0.78rem; line-height: 1.45; }

/* ── Solunar fishing & feeding times (4th almanac row) ──────────────────────
   On-theme cyan, dark storm palette. MAJOR windows read heavier than MINOR via
   a left cyan bar + brighter type so the two best windows win first fixation. */
.alm-solunar { display: grid; gap: 0.5rem; }
.sl-head {
  margin: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem;
  font-size: 1.02rem; font-weight: 600; color: var(--text);
}
.sl-badge {
  background: var(--accent-soft); color: var(--link-hover);
  border-radius: 999px; padding: 0.16rem 0.6rem;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.sl-why { margin: -0.15rem 0 0; color: var(--link-hover); font-size: 0.86rem; font-weight: 600; }
.sl-wins { display: grid; gap: 0.4rem; }
.sl-win {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  padding: 0.5rem 0.7rem; border-radius: 0.6rem;
  background: rgba(255,255,255,0.03);
  box-shadow: inset 0 0 0 1px var(--border-2);
}
.sl-major {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  border-left: 3px solid var(--accent);
}
.sl-when { font-size: 1.0rem; font-weight: 700; color: var(--text-body); font-variant-numeric: tabular-nums; }
.sl-major .sl-when { color: var(--text); }
.sl-tag {
  font-size: 0.74rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text-muted); white-space: nowrap;
}
.sl-major .sl-tag { color: var(--link-hover); }
.sl-fish { font-size: 0.9rem; }
.sl-note { margin: 0.1rem 0 0; color: var(--text-muted); font-size: 0.85rem; line-height: 1.45; }

/* Additive, AFTER the locked "ONE shared content column" desktop block — aligns
   the solunar row to the same 880px column. Does NOT modify the frozen block. */
@media (min-width: 601px) { #almanac-solunar { max-width: 880px; margin-inline: auto; } }

/* ───── UX uniformity pass (2026-06-26) ─────────────────────────────────────
   Two composed-not-scattered fixes:
   (1) The fishing-window rows were collapsing to a content-width, right-shifted
       column — .sl-wins is display:grid with NO column template, so it gets an
       auto track instead of stretching. Pin a single full-width 1fr track (and
       stretch the solunar children) so the windows share the SAME left edge as
       every other almanac row (Moon / Plant / Sun) and the section heading.
   (2) Micro-label letter-spacing ranged 0.03–0.08em panel-to-panel; unify the
       almanac/sun/solunar eyebrows to 0.06em so they match the water-panel
       labels (already 0.06em) and read as one intentional system. */
.alm-solunar { justify-items: stretch; }
#almanac-solunar { max-width: none; margin-inline: 0; }
.sl-head { justify-content: flex-start; text-align: left; }
.sl-wins { grid-template-columns: 1fr; }
.sl-win { width: 100%; box-sizing: border-box; }
.alm-label, .sl-tag, .sun-lab, .sl-badge { letter-spacing: 0.06em; }

/* ── Sun today (5th almanac row, sits above solunar) ────────────────────────
   Dark storm palette, cyan #38bdf8 accent on the sunrise/sunset values; a warm
   amber tint on the golden-light line (the brand-mark amber) to read as "soft
   light" without breaking the cyan-forward theme. Tabular numerals so the times
   line up with the temps. Responsive: 3 even pills that wrap cleanly on mobile. */
.alm-sun { display: grid; gap: 0.5rem; }
.sun-times {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.45rem;
}
.sun-cell {
  display: flex; flex-direction: column; gap: 0.15rem;
  padding: 0.5rem 0.6rem; border-radius: 0.6rem;
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.sun-lab {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
  color: var(--text-muted); white-space: nowrap;
}
.sun-ico { font-size: 0.82rem; }
.sun-val {
  font-size: 1.12rem; font-weight: 700; color: var(--link);
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.sun-val-muted { color: var(--text-body); }
.sun-gold {
  margin: 0; color: var(--text-2); font-size: 0.9rem; line-height: 1.5;
  padding: 0.5rem 0.6rem; border-radius: 0.6rem;
  background: var(--warm-soft);
  box-shadow: inset 0 0 0 1px var(--warm-line);
}
.sun-gold-win { color: var(--warm); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The plain-language live phase line that opens the Sun row ("It's daytime in
   Corpus Christi right now…"). One short 5th-grade sentence, the same phase the
   page color is driven by, so the words and the theme always agree. The leading
   sun/moon/dawn glyph is decorative (aria-hidden); the sentence carries the
   meaning. Body text uses --text (passes 4.5:1 in both phases). */
.sun-phase {
  margin: 0 0 0.1rem; display: flex; align-items: baseline; gap: 0.45rem;
  font-size: 0.98rem; line-height: 1.45; color: var(--text-body); font-weight: 600;
}
.sun-phase .sun-phase-ico { font-size: 1.1rem; line-height: 1; flex-shrink: 0; }
.sun-phase b { color: var(--text); font-weight: 800; }
.sun-phase .sun-phase-tail { color: var(--text-muted); font-weight: 600; }

/* Additive, AFTER the locked "ONE shared content column" desktop block — aligns
   the sun row to the same 880px column. Does NOT modify the frozen block. */
@media (min-width: 601px) { #almanac-sun { max-width: 880px; margin-inline: auto; } }

/* ───── Water & Drought Tracker (apex, backlog #7) ─────────────────────────────
   Live reservoir read from /content/water.json. Same dark-storm card surface as
   .almanac-card / .outlook-card (navy #0f172a→#0c1a35, cyan #38bdf8 accents). The
   #water section ships hidden in markup and app.js un-hides it ONLY when the feed
   has real data, so a 404/empty payload leaves no empty card. The Stage badge
   carries the WORD always (color is never the only signal). Fill bars are a track
   + gradient fill whose width is set inline as a %. Mobile-first; the desktop
   880px column rule below is ADDITIVE, placed after the frozen block — it does NOT
   modify the locked "ONE shared content column" block. */
.water-card {
  background: linear-gradient(135deg, var(--bg-grad-a) 0%, var(--bg-grad-b) 100%);
  border-radius: 1rem;
  padding: 1.25rem 1.5rem;
  box-shadow: inset 0 0 0 1px var(--border);
  display: grid;
  gap: 0.9rem;
}
.water-top { display: grid; gap: 0.5rem; }
.water-badge {
  justify-self: start;
  display: inline-block;
  font-size: 0.82rem; font-weight: 700; letter-spacing: 0.02em;
  padding: 0.3rem 0.75rem; border-radius: 999px;
  /* default (cyan) until a tier class lands; the JS always sets a class */
  color: var(--link-hover); background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px rgba(56,189,248,0.35);
}
.water-badge.is-critical { color: var(--bad); background: rgba(239,68,68,0.16); box-shadow: inset 0 0 0 1px rgba(239,68,68,0.55); }
.water-badge.is-stage2   { color: var(--warn); background: rgba(245,158,11,0.16); box-shadow: inset 0 0 0 1px rgba(245,158,11,0.50); }
.water-badge.is-stage1   { color: var(--warn-2); background: rgba(234,179,8,0.14);  box-shadow: inset 0 0 0 1px rgba(234,179,8,0.45); }
.water-badge.is-normal   { color: var(--ok); background: rgba(34,197,94,0.14);   box-shadow: inset 0 0 0 1px rgba(34,197,94,0.45); }
.water-headline { margin: 0; font-size: 1.08rem; font-weight: 600; color: var(--text); line-height: 1.4; }

/* Fill-bar primitives (track + gradient fill), reused by combined + each lake. */
.water-bar { height: 8px; border-radius: 4px; background: var(--accent-soft); overflow: hidden; }
.water-bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, #38bdf8, #0ea5e9); }

/* Combined "both lakes together" block — the headline number, larger. */
.water-combined { display: grid; gap: 0.4rem; padding: 0.7rem 0.8rem; border-radius: 0.7rem;
  background: rgba(56,189,248,0.06); box-shadow: inset 0 0 0 1px rgba(56,189,248,0.16); }
.wt-combined-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
.wt-combined-lab { font-size: 0.74rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); }
.wt-combined-pct { font-size: 1.7rem; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; line-height: 1; }

/* Two per-lake rows. */
.water-lakes { display: grid; gap: 0.6rem; }
.wt-lake { display: grid; gap: 0.35rem; padding: 0.6rem 0.75rem; border-radius: 0.6rem;
  background: rgba(255,255,255,0.03); box-shadow: inset 0 0 0 1px var(--border-2); }
.wt-lake-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
.wt-lake-name { font-size: 0.98rem; font-weight: 600; color: var(--text-body); }
.wt-lake-pct { font-size: 1.15rem; font-weight: 700; color: var(--link); font-variant-numeric: tabular-nums; }
.wt-lake-detail { margin: 0; font-size: 0.82rem; color: var(--text-muted); line-height: 1.45; font-variant-numeric: tabular-nums; }

.water-rain-note { margin: 0; color: var(--text-2); font-size: 0.92rem; line-height: 1.5; }
.water-gloss { margin: 0; color: var(--text-faint); font-size: 0.8rem; line-height: 1.45; font-style: italic; }
.water-updated { margin: 0; color: var(--text-faint); font-size: 0.78rem; }
.water-foot { margin: 0.1rem 0 0; color: var(--text-faint); font-size: 0.78rem; line-height: 1.45; }

@media (forced-colors: active) {
  .water-card, .water-combined, .wt-lake { border: 1px solid CanvasText; }
  .water-bar-fill { background: Highlight; }
}

/* Additive, AFTER the locked "ONE shared content column" desktop block — aligns
   the water card to the same 880px column. Does NOT modify the frozen block. */
@media (min-width: 601px) { #water-card { max-width: 880px; margin-inline: auto; } }

/* OFFICIAL Stage 3 rules block (#water-rules). Sits INSIDE #water-card, after the
   gloss and before the freshness stamp. app.js un-hides it ONLY under Stage 3. The
   can't/can marks reuse the SAME critical-red / normal-green as the .water-badge
   tiers (no new color system). Mobile: can't column FIRST (the urgent answer), can
   second, stacked. Desktop side-by-side handled by the additive @media below. */
.water-rules {
  display: grid;
  gap: 0.75rem;
  padding: 0.9rem 1rem;
  border-radius: 0.7rem;
  background: rgba(255,255,255,0.03);
  box-shadow: inset 0 0 0 1px var(--border);
}
.water-rules-h { margin: 0; font-size: 1rem; font-weight: 700; color: var(--text); letter-spacing: 0.01em; }
.water-rules-lead { margin: 0; font-size: 0.92rem; color: var(--text-2); line-height: 1.5; }
.water-rules-cols { display: grid; gap: 0.7rem; }
.wt-cant, .wt-can { display: grid; gap: 0.35rem; padding: 0.6rem 0.7rem; border-radius: 0.55rem; }
/* Reuse the badge tier colors so the rules match the Stage badge at a glance. */
.wt-cant { background: rgba(239,68,68,0.10); box-shadow: inset 0 0 0 1px rgba(239,68,68,0.30); }
.wt-can  { background: rgba(34,197,94,0.09); box-shadow: inset 0 0 0 1px rgba(34,197,94,0.28); }
.wt-rules-cap { margin: 0 0 0.15rem; font-size: 0.74rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; display: flex; align-items: center; gap: 0.4rem; }
.wt-cant .wt-rules-cap { color: var(--bad); }
.wt-can  .wt-rules-cap { color: var(--ok); }
.wt-mark { font-size: 0.95rem; line-height: 1; font-weight: 900; }
.wt-rules-list { margin: 0; padding-left: 1.15rem; display: grid; gap: 0.4rem; }
.wt-rules-list li { font-size: 0.9rem; color: var(--text-body); line-height: 1.45; }
.water-rules-fine { margin: 0; font-size: 0.9rem; color: var(--orange); line-height: 1.5; }
.water-rules-myth { margin: 0; font-size: 0.88rem; color: var(--text-2); line-height: 1.5; }
.water-rules-scope { margin: 0; font-size: 0.84rem; color: var(--text-muted); line-height: 1.5; }
.water-rules-src { margin: 0; font-size: 0.82rem; color: var(--text-muted); line-height: 1.6; }
.water-rules-src a { color: var(--link-hover); }
@media (forced-colors: active) {
  .water-rules, .wt-cant, .wt-can { border: 1px solid CanvasText; }
}

/* Additive, AFTER the locked "ONE shared content column" block — on desktop the
   can't/can lists sit side by side; can't stays first (left). Does NOT modify the
   frozen block. */
@media (min-width: 601px) {
  .water-rules-cols { grid-template-columns: 1fr 1fr; gap: 0.8rem; align-items: start; }
}

/* Semantic status colors — THEMED so good/caution/no-go stay readable in BOTH
   the dark (night) and light (day/dawn) palettes. Night = bright on dark; day = deep on light. */
:root { --ok: #86efac; --ok-strong: #4ade80; --bad: #fca5a5; --warn: #fcd34d; --warn-2: #fde68a; --orange: #fdba74; }
[data-tod="day"], [data-tod="dawn"] { --ok: #15803d; --ok-strong: #15803d; --bad: #b91c1c; --warn: #b45309; --warn-2: #b45309; --orange: #9a3412; }

/* ── Day/dawn contrast fixes: chips/badges/sun-row that hardcode dark-theme
   accent fills get routed through the light-palette tokens so they stay readable
   on white. Surgical, [data-tod] scoped — night is untouched. ── */
[data-tod="day"] .alm-chip, [data-tod="dawn"] .alm-chip,
[data-tod="day"] .sl-fish, [data-tod="dawn"] .sl-fish,
[data-tod="day"] .sl-when, [data-tod="dawn"] .sl-when,
[data-tod="day"] .eyebrow, [data-tod="dawn"] .eyebrow,
[data-tod="day"] .quick-nav a, [data-tod="dawn"] .quick-nav a {
  background: var(--accent-soft); color: var(--link); box-shadow: inset 0 0 0 1px var(--accent-line);
}
[data-tod="day"] .sun-row, [data-tod="dawn"] .sun-row,
[data-tod="day"] .sun-cell, [data-tod="dawn"] .sun-cell,
[data-tod="day"] .sl-cell, [data-tod="dawn"] .sl-cell {
  background: var(--surface-2);
}
[data-tod="day"] .sun-val, [data-tod="day"] .sun-lab, [data-tod="day"] .sun-ico,
[data-tod="dawn"] .sun-val, [data-tod="dawn"] .sun-lab, [data-tod="dawn"] .sun-ico { color: var(--text-body); }
[data-tod="day"] .sun-val-muted, [data-tod="dawn"] .sun-val-muted { color: var(--text-muted); }
[data-tod="day"] .sun-gold-win, [data-tod="dawn"] .sun-gold-win { color: var(--orange); }
[data-tod="day"] .wt-mark, [data-tod="day"] .wt-combined-lab,
[data-tod="dawn"] .wt-mark, [data-tod="dawn"] .wt-combined-lab { color: var(--text-body); }

/* ── 17-city heat-map tiles: each tile's background + text color are COMPUTED
   per city in app.js (feels-like vs your city) and set inline. Name, temp, and
   condition follow that computed text color so every tile stays readable
   whatever its hue. No fixed colors here — the ring uses the accent token. ── */
.city-chip { position: relative; }
.city-chip .cc-name { padding-right: 15px; }
.cc-dot { position: absolute; top: 0.62rem; right: 0.7rem; width: 11px; height: 11px; border-radius: 50%; box-shadow: 0 0 0 2px var(--surface); }
.city-chip.is-current .cc-dot { box-shadow: 0 0 0 2px var(--surface-2); }
.city-chip .cc-cond { opacity: 0.88; }
.city-chip .cc-you { opacity: 0.82; }
.city-chip.is-current { box-shadow: inset 0 0 0 2px var(--accent); }

/* ── Temperature trend (scrubbable SVG: past 12h + Now + forecast ahead). All
   tile/line/text colors come from data (computed HSL) or theme tokens. ── */
@media (min-width: 601px) { #trend { max-width: 880px; margin-inline: auto; } }
.trend-sub { margin: 0 0 0.75rem; color: var(--text-2); font-size: 0.92rem; line-height: 1.5; }
.trend-card {
  background: linear-gradient(135deg, var(--bg-grad-a) 0%, var(--bg-grad-b) 100%);
  border-radius: 1rem; padding: 1rem 1.1rem 0.85rem; box-shadow: inset 0 0 0 1px var(--border);
}
.trend-readout { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; min-height: 1.9rem; margin-bottom: 0.35rem; }
.trend-ro-temp { font-size: 1.75rem; font-weight: 700; line-height: 1; }
.trend-ro-when { font-size: 0.82rem; color: var(--text-muted); font-weight: 600; }
.trend-ro-cond { font-size: 0.82rem; color: var(--text-2); }
.trend-chart { width: 100%; }
.trend-svg { width: 100%; height: auto; display: block; touch-action: pan-y; }
.trend-svg text { fill: var(--text-muted); }
.trend-tick { font-size: 9px; }
.trend-now-lab { font-size: 9px; font-weight: 800; letter-spacing: 0.1em; fill: var(--text-2); }
.trend-hl { font-size: 11px; font-weight: 700; fill: var(--text-body); }
.trend-foot { margin: 0.5rem 0 0; color: var(--text-faint); font-size: 0.76rem; line-height: 1.45; }

/* Trend chart keyboard focus ring (WCAG 2.4.7) — visible when tabbed to for arrow-key scrub. */
.trend-svg:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }

/* Trend R1 council additions: round-temp gridlines, forecast "ahead" wash label,
   the visible citable plain-language lede, scrub-dot grow. All tokenized. */
.trend-grid-lab { font-size: 8px; fill: var(--text-faint); }
.trend-ahead-lab { font-size: 8px; fill: var(--text-faint); letter-spacing: 0.08em; text-transform: uppercase; }
.trend-summary { margin: 0.2rem 0 0.7rem; color: var(--text); font-size: 1rem; line-height: 1.5; font-weight: 600; }
#trend-cur-dot { transition: r 0.08s ease; }
#trend-hit { touch-action: pan-y; }

/* ═══════════════════════════════════════════════════════════════════════════
   LEGAL / POLICY PAGES — /privacy/, /terms/, /accessibility/
   Shared prose styles for the footer-linked legal pages. TOKENS ONLY, so these
   inherit the time-of-day theme exactly like every other surface (the inline
   DESIGN-T proxy script themes the static pages; no app.js is loaded there).
   ═══════════════════════════════════════════════════════════════════════════ */
.legal { max-width: 760px; padding-bottom: 2rem; }
.legal .legal-updated { font-size: 0.82rem; color: var(--text-muted); margin: 0 0 1.5rem; }
.legal .lead-note {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  border-radius: 12px; padding: 0.9rem 1.1rem; margin: 0 0 1.75rem;
}
.legal .lead-note p { color: var(--text-body); margin: 0; }
.legal-toc {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border);
  border-radius: 12px; padding: 1rem 1.25rem; margin: 0 0 2rem;
}
.legal-toc h2 {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-muted); margin: 0 0 0.6rem; font-weight: 800;
}
.legal-toc ol { margin: 0; padding-left: 1.1rem; columns: 2; column-gap: 1.75rem; }
.legal-toc li { margin-bottom: 0.35rem; font-size: 0.9rem; break-inside: avoid; }
.legal h2 { font-size: 1.15rem; color: var(--text); margin: 1.9rem 0 0.5rem; scroll-margin-top: 1rem; }
.legal h3 { font-size: 1rem; color: var(--text); margin: 1.1rem 0 0.3rem; }
.legal p { color: var(--text-2); margin: 0 0 0.85rem; line-height: 1.65; }
.legal ul, .legal ol.legal-list { margin: 0 0 0.95rem; padding-left: 1.25rem; }
.legal li { color: var(--text-2); margin-bottom: 0.45rem; line-height: 1.6; }
.legal dl.legal-defs dt { font-weight: 700; color: var(--text); margin-top: 1rem; }
.legal dl.legal-defs dd { margin: 0.2rem 0 0; color: var(--text-2); }
.legal address { font-style: normal; color: var(--text-2); line-height: 1.6; }
@media (max-width: 560px) { .legal-toc ol { columns: 1; } }

