/* Tokens */
:root {
  --bg: #f6f7fb;
  --bg-card: #ffffff;
  --bg-card-2: #fbfcff;
  --fg: #16202c;
  --fg-muted: #5a6577;
  --fg-faint: #8a93a3;
  --border: #e3e7ee;
  --accent: #0b2545;
  --accent-2: #134074;
  --good: #0f9b54;
  --warn: #c98214;
  --bad: #c0392b;
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 4px 12px rgba(16, 24, 40, 0.06);
  --icon-color: #475569;
  --radius: 14px;
  --radius-sm: 10px;
  --pad: clamp(0.875rem, 0.6rem + 1vw, 1.25rem);
  /* Inter-card gap (and outer page padding). Tightened so a typical desktop
     viewport fits the full dashboard without scrolling. */
  --gap: clamp(0.5rem, 0.35rem + 0.5vw, 0.85rem);

  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Inter, sans-serif;
  --font-num: ui-rounded, "SF Pro Rounded", "Inter", system-ui, sans-serif;
}
@media (prefers-color-scheme: dark) {
  :root {
    /* Charcoal — neutral, professional, no blue tint. */
    --bg: #1a1a1a;
    --bg-card: #242424;
    --bg-card-2: #2e2e2e;
    --fg: #e6e6e6;
    --fg-muted: #a8a8a8;
    --fg-faint: #707070;
    --border: #3a3a3a;
    /* Brighter accent than would be needed on a PC monitor — TV gamma
       crushes mid-blues, and the accent drives precip-bar visibility. */
    --accent: #8cc0f5;
    --accent-2: #b0d4f8;
    --shadow: 0 1px 0 rgba(0, 0, 0, 0.4), 0 6px 16px rgba(0, 0, 0, 0.35);
    --icon-color: #c8c8c8;
  }
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f6f7fb; --bg-card: #ffffff; --bg-card-2: #fbfcff; --fg: #16202c;
  --fg-muted: #5a6577; --fg-faint: #8a93a3; --border: #e3e7ee;
  --accent: #0b2545; --accent-2: #134074;
  --icon-color: #475569;
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #1a1a1a; --bg-card: #242424; --bg-card-2: #2e2e2e; --fg: #e6e6e6;
  --fg-muted: #a8a8a8; --fg-faint: #707070; --border: #3a3a3a;
  --accent: #8cc0f5; --accent-2: #b0d4f8;
  --icon-color: #c8c8c8;
}

/* ============================================================
   Wall display mode
   Same layout, typography, and proportions as desktop — just with the
   root font-size doubled so everything reads from across a room. The
   prior approach (~100 lines of per-element font-size overrides) drifted
   visually from the desktop look; this approach keeps the two modes
   identical in *structure* and lets the rem inheritance do all the
   scaling. The only deliberate exceptions are noted inline.

   Side margins and inter-card gaps are tightened in wall mode to recover
   the screen real-estate that the prior layout was wasting on padding.
   ============================================================ */

/* Root font-size doubles whatever the user picked in settings. Combined
   selectors out-rank the plain :root[data-text-size=...] rules further
   down, so wall mode wins regardless of source order. */
:root[data-display="wall"]                              { font-size: 32px; }   /* default 16 × 2 */
:root[data-display="wall"][data-text-size="large"]      { font-size: 36px; }   /* 18 × 2 */
:root[data-display="wall"][data-text-size="xlarge"]     { font-size: 40px; }   /* 20 × 2 */

/* Tighten inter-card and outer padding — at 32px root the default
   clamp()-based --gap/--pad would hand back ~21/~38px, which wastes
   screen real-estate on a wall display. These small rem values still
   scale with the chosen text-size. */
:root[data-display="wall"] {
  --gap: 0.25rem;   /* ~8px @ 32px root */
  --pad: 0.5rem;    /* ~16px @ 32px root */
  /* Plane markers don't auto-scale with rem — bump separately so
     they stay readable on the radar map. */
  --plane-marker-scale: 2;
}
:root[data-display="wall"][data-text-size="large"]  { --plane-marker-scale: 2.3; }
:root[data-display="wall"][data-text-size="xlarge"] { --plane-marker-scale: 2.7; }

/* Edge-to-edge cards: zero side padding on the grid so the dashboard
   uses the full screen width. Inter-card gap stays at the (small) --gap.
   No max-width or auto-margins — the grid spans whatever the viewport
   gives us. */
:root[data-display="wall"] .grid {
  padding: 0 0 var(--gap);
  max-width: 100%;
  margin: 0;
}

/* Topbar becomes the clock-and-date banner; everything else hides. */
:root[data-display="wall"] .topbar {
  flex-direction: column;
  align-items: center;
  padding: 0.4rem 0;
  background: transparent;
  border-bottom: none;
  position: static;
}
:root[data-display="wall"] .brand {
  flex-direction: column;
  align-items: center;
  gap: 0.05rem;
}
:root[data-display="wall"] .brand-name { display: none; }
:root[data-display="wall"] .brand-clock {
  /* Capped at 220px so the clock remains the dominant headline against
     the feels-like temp (currently capped at 160px after the radar-
     button-gone size bump). Absolute-pixel max keeps the cap stable
     across the wall text-size variants. */
  font-size: clamp(5rem, 3rem + 6vw, 220px);
  font-weight: 700;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.04em;
  line-height: 0.95;
  color: var(--fg);
}
/* Seconds dimmed (same size as the H:MM digits) so the live tick fades
   into the background but the clock stays balanced. Used in both modes. */
.clock-seconds {
  color: var(--fg-faint);
  font-weight: 400;
}
:root[data-display="wall"] .brand-date {
  font-size: clamp(1.2rem, 0.7rem + 1.2vw, 1.8rem);
  color: var(--fg-muted);
  font-weight: 500;
}
/* Topbar's toggle/freshness cluster moves to a footer at page bottom
   (sits below content, doesn't overlay). The footer always exists in
   the DOM but stays hidden outside wall mode. */
:root[data-display="wall"] .topbar-actions { display: none; }

.wall-footer { display: none; }
:root[data-display="wall"] .wall-footer {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.85rem;
  padding: 0.4rem var(--gap) 0.6rem;
}
:root[data-display="wall"] .wall-footer .freshness {
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
:root[data-display="wall"] .wall-footer .display-toggle-btn {
  width: 1rem; height: 1rem;
  opacity: 0.45;
  background: transparent;
  transition: opacity 0.15s, background 0.15s;
}
:root[data-display="wall"] .wall-footer .display-toggle-btn:hover,
:root[data-display="wall"] .wall-footer .display-toggle-btn:focus-visible {
  opacity: 1;
  background: var(--bg-card-2);
}

/* Hide the wall-mode toggle on phones — wall displays are never phones,
   and removing it removes the accidental-tap risk. */
@media (max-width: 720px) {
  .topbar-btn.display-toggle-btn { display: none; }
}

/* The "feels like" big-number is one deliberate exception to the 2× scale.
   The clock is already the headline number; a doubled feels-like temp
   competes with it visually. Pin to an absolute-pixel clamp so the value
   is stable regardless of which wall text-size variant is selected.
   Sized to be visually balanced with the giant clock above and the
   matched compass + condition-icon siblings on the same row. */
:root[data-display="wall"] .big-number {
  font-size: clamp(96px, 48px + 5vw, 160px);
}

/* Lift the body font-size cap in wall mode so elements that don't set
   their own font-size (.obs-table tbody th, .current-desc, .aircraft-grid
   dd, etc.) inherit the doubled root scale instead of getting stuck at
   the 17px desktop cap. */
:root[data-display="wall"] body {
  font-size: 1rem;
}

/* Compass diameter matches the wall-mode `.big-number` pixel clamp so
   the temp digits and the compass ring render at the same vertical size
   on TV displays. Without this override the rem-based clamp() on the
   base rule would compute against wall mode's 32px root and balloon
   past the temp. */
:root[data-display="wall"] .compass-ring {
  width: clamp(96px, 48px + 5vw, 160px);
  height: clamp(96px, 48px + 5vw, 160px);
  /* The default border (1px var(--border) = #444 in wall+dark) almost
     vanishes against the pure-black background. Bump width and use a
     mid-tone color so the ring is clearly visible from across the room
     without competing with the bright needle inside. */
  border-width: 3px;
  border-color: var(--fg-muted);
}
/* Pixel clamps for the inner text — sized to stay inside the same 40%
   inner-radius zone the needle tip reaches in viewBox units (48px
   diameter inside the 120px ring), so the digits don't collide with the
   needle at intercardinal headings. The speed cap of 32px keeps the
   stacked digits + unit just under that 48px height; pushing further
   would require growing the ring (and the matching big-number temp) in
   step. */
:root[data-display="wall"] .compass-speed {
  font-size: clamp(22px, 11px + 1.1vw, 32px);
}
:root[data-display="wall"] .compass-unit {
  font-size: clamp(10px, 5px + 0.45vw, 14px);
  font-weight: 600;
}
:root[data-display="wall"] .compass-speed.calm {
  font-size: clamp(14px, 7px + 0.7vw, 20px);
}

/* Hide the "more details" content (rain totals + astro grid) by default in
   wall mode. There's no toggle accessible from a wall display, and the
   extra content forces the forecast card off-screen. */
:root[data-display="wall"] #more-details { display: none; }
/* Radar button is a tap target — there's no pointer on a wall TV so the
   button just steals visual weight without earning it. The radar card
   itself still pops up automatically on severe weather (see the wall-mode
   auto-expand setting). */
:root[data-display="wall"] .radar-toggle-btn { display: none; }

/* ----- Wall-mode tweaks for the 3-column current-block -----
   The grid template (temp / middle / compass) is shared with desktop
   now; wall mode just bumps the column-gap, breathing-room below the
   headline, and the big-icon size. */
:root[data-display="wall"] .current-block {
  column-gap: clamp(0.75rem, 0.25rem + 2vw, 2rem);
  /* Extra breathing room below the headline row so the description text
     ("Partly Cloudy") doesn't visually blur into the Now/Today/Yest data
     rows immediately below it. */
  margin-bottom: clamp(1rem, 0.5rem + 1vw, 2rem);
}
:root[data-display="wall"] .current-icon-wall {
  /* Bigger middle icon at wall sizes — balances the giant temp digit
     and compass on either side. */
  width:  clamp(80px, 40px + 4.5vw, 144px);
  height: clamp(80px, 40px + 4.5vw, 144px);
}

/* Heavier weights make small/medium text legible from across a room
   without bumping size — keeps the layout tight even after lifting the
   body cap above. Targets thin text in the current and aircraft cards
   that previously washed out on a TV. */
:root[data-display="wall"] .current-desc,
:root[data-display="wall"] .aircraft-grid dt {
  font-weight: 600;
}
:root[data-display="wall"] .obs-table .row-now td,
:root[data-display="wall"] .obs-table .row-today td,
:root[data-display="wall"] .obs-table .row-yest td,
:root[data-display="wall"] .obs-table tbody th,
:root[data-display="wall"] .aircraft-grid dd {
  font-weight: 500;
}
/* Callsign / operator label above the photo. The base rule's
   1.4–1.8rem clamp doubles in wall mode to ~45–58px, which wraps long
   names like "Quanta Aviation Services 10" to two lines. Pin to an
   absolute-pixel clamp here so wall + the wall text-size variants
   all keep it on one line while still being legible from across the
   room. */
:root[data-display="wall"] .aircraft-callsign {
  font-size: clamp(28px, 16px + 1.2vw, 38px);
}
/* Column-label row at the bottom of the obs-table ("Hi/Lo · Hum · Wind ·
   Precip · UV · AQI" + the "history →" link in the leftmost cell). The
   default --fg-faint color washes out against the pure-black dark-mode
   wall background; bump weight + color so the labels read from across
   the room. */
:root[data-display="wall"] .obs-table .row-labels td {
  font-weight: 700;
  color: var(--fg-muted);
}
:root[data-display="wall"] .obs-table td.cell-history-trigger .history-trigger {
  color: var(--fg-muted);
  font-weight: 700;
}

/* Pure-black palette for wall + dark — keeps the page background at true
   black (OLED contrast / power), but lifts the card surfaces and uses a
   light inner-top bezel to give the cards the same "floating" depth the
   light-desktop palette gets from drop shadows. Drop shadows are invisible
   against #000, so we replace them with a subtle highlight on the top
   edge of each card (`--card-elevation` below). */
@media (prefers-color-scheme: dark) {
  :root[data-display="wall"]:not([data-theme="light"]) {
    --bg: #000;
    --bg-card: #1c1c1c;
    --bg-card-2: #2a2a2a;
    --fg: #fff;
    --fg-muted: #cfcfcf;
    --fg-faint: #8a8a8a;
    /* Border sits visibly above the card luminance so the card reads as
       a distinct framed surface from across the room (the light-desktop
       palette gets the same effect from a soft drop shadow, which is
       invisible against #000). */
    --border: #3a3a3a;
    --icon-color: #f0f0f0;
    /* Match the regular dark-mode accent so chart elements driven by
       --accent (precip bars, temp lines, etc.) stay visible on the
       near-black wall background. Without these the precip bars use the
       light-mode dark navy default and disappear into the bg. */
    --accent: #8cc0f5;
    --accent-2: #b0d4f8;
    /* See `.card` rule further down — replaces the unusable black-on-black
       drop shadow with a top-edge highlight (1px inset) plus a faint outer
       ring. Tuned to read from across a room while still feeling subtle
       up close. */
    --card-elevation:
      inset 0 1px 0 rgba(255, 255, 255, 0.10),
      0 0 0 1px rgba(255, 255, 255, 0.04);
  }
}
:root[data-display="wall"][data-theme="dark"] {
  --bg: #000;
  --bg-card: #1c1c1c;
  --bg-card-2: #2a2a2a;
  --fg: #fff;
  --fg-muted: #cfcfcf;
  --fg-faint: #8a8a8a;
  --border: #3a3a3a;
  --icon-color: #f0f0f0;
  --accent: #8cc0f5;
  --accent-2: #b0d4f8;
  --card-elevation:
    inset 0 1px 0 rgba(255, 255, 255, 0.10),
    0 0 0 1px rgba(255, 255, 255, 0.04);
}

/* ============================================================
   High-contrast surface boost for dim displays.
   ============================================================
   The default dark palettes are tuned for a TV/monitor at normal
   brightness. When the user keeps the display very dim (5–15%),
   the bottom of the gamma curve compresses near-black gradations
   together — #000, #1c1c1c, and #3a3a3a all converge perceptually
   toward black, and the card/border structure becomes invisible.

   This block widens the absolute pixel-value gap so the structure
   still reads at low display brightness. Opt-in via the settings
   drawer ("High contrast for dim displays") — written to
   localStorage['lowBrightness']='1' and applied as the
   data-low-brightness=1 attribute on <html>. Scoped to dark
   modes only; light mode doesn't have the same gamma collapse
   problem.

   Tuning notes:
     - --bg stays at #000 (OLED true-black and shadow base).
     - --bg-card jumps from #1c1c1c → #333 (roughly 2× the absolute
       gap from #000). Still feels "dark" but visibly distinct.
     - --border jumps from #3a3a3a → #6a6a6a so the card outline
       reads even when the card itself is closer to mid-gray.
     - Inner top highlight goes from 0.10 → 0.22 alpha so the
       bezel cue survives the brightness compression. */
:root[data-low-brightness="1"][data-display="wall"][data-theme="dark"],
:root[data-low-brightness="1"][data-theme="dark"] {
  --bg-card: #333;
  --bg-card-2: #404040;
  --border: #6a6a6a;
  --card-elevation:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 0 0 1px rgba(255, 255, 255, 0.08);
}
@media (prefers-color-scheme: dark) {
  :root[data-low-brightness="1"][data-display="wall"]:not([data-theme="light"]),
  :root[data-low-brightness="1"]:not([data-theme="light"]) {
    --bg-card: #333;
    --bg-card-2: #404040;
    --border: #6a6a6a;
    --card-elevation:
      inset 0 1px 0 rgba(255, 255, 255, 0.22),
      0 0 0 1px rgba(255, 255, 255, 0.08);
  }
}

/* Reset */
*, *::before, *::after { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  /* Defense in depth: no matter what a child element does, the page itself
     never scrolls horizontally. Mobile viewport rescaling stays well-behaved. */
  overflow-x: clip;
}
html { -webkit-text-size-adjust: 100%; }
/* Make the `hidden` HTML attribute reliably win over any `display:` rule we
   later set on the same element (e.g. .hazard sets display:flex). Without
   this the tornado banner and outlook chip render even when their `hidden`
   attribute is present, since CSS specificity ties go to source order. */
[hidden] { display: none !important; }
body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  line-height: 1.45;
  font-size: clamp(15px, 0.92rem + 0.2vw, 17px);
  min-height: 100vh;
  min-height: 100dvh;
}
img, svg { max-width: 100%; display: block; }
button { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

.visually-hidden {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.skip {
  position: absolute; left: -9999px; top: 0;
}
.skip:focus { left: 0.5rem; top: 0.5rem; background: var(--accent); color: #fff; padding: 0.5rem 1rem; border-radius: 6px; z-index: 100; }
.muted { color: var(--fg-muted); }

/* Header */
.topbar {
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  padding: var(--pad) calc(var(--pad) + env(safe-area-inset-right))
           var(--pad) calc(var(--pad) + env(safe-area-inset-left));
  background: var(--bg-card);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 10;
}
/* Phones: the 36px buttons already set the minimum row height, so 14px of
   vertical padding on each side just steals viewport from the dashboard
   content. Trim it without touching the button size or fonts. Also drop
   the clock (the OS already shows the time in the status bar) and tighten
   the brand-row gap so the name + date sit close together on one line. */
@media (max-width: 720px) {
  .topbar { padding-top: 0.35rem; padding-bottom: 0.35rem; }
  .brand { gap: 0.5rem; flex-wrap: nowrap; min-width: 0; }
  .brand-clock { display: none; }
  .brand-name { flex-shrink: 0; }              /* keep neighborhood name intact */
  .brand-date {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
.brand { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.brand-name {
  font-weight: 600; letter-spacing: -0.01em;
  font-size: clamp(1.05rem, 1rem + 0.4vw, 1.25rem);
}
.brand-clock {
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
  font-family: var(--font-num);
}
.brand-date {
  color: var(--fg-faint);
  font-size: 0.85rem;
}
.topbar-actions { display: flex; align-items: center; gap: 0.75rem; }
.freshness { color: var(--fg-faint); font-size: 0.85rem; }
/* Stale state: bolder, warning color, and a warning glyph so it's
   unmistakable that the data hasn't refreshed in a while (LAN agent down,
   tunnel broken, or just a fresh tab opened during an outage). */
.freshness.stale { color: var(--warn); font-weight: 600; }
.freshness.stale::before { content: "\26A0  "; }
.topbar-btn {
  background: var(--bg-card-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  width: 36px; height: 36px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--fg);
  padding: 0;
}
.topbar-btn:hover { background: var(--bg); }
.display-icon {
  width: 20px; height: 20px; display: block;
  background-color: currentColor;
  -webkit-mask: var(--display-icon-url) center / contain no-repeat;
          mask: var(--display-icon-url) center / contain no-repeat;
  /* Default (normal mode): a small "expand" icon hinting at wall mode. */
  --display-icon-url: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5'/></svg>");
}
:root[data-display="wall"] .display-icon {
  /* In wall mode, a "compress" icon to indicate "click to leave wall mode". */
  --display-icon-url: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5'/></svg>");
}
.theme-icon {
  width: 20px; height: 20px; display: block;
  background-color: currentColor;
  -webkit-mask: var(--theme-icon-url) center / contain no-repeat;
          mask: var(--theme-icon-url) center / contain no-repeat;
  /* Default (no manual override): follow OS preference. */
  --theme-icon-url: url("/static/icons/wx/clear-day.svg");
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-icon {
    --theme-icon-url: url("/static/icons/wx/clear-night.svg");
  }
}
/* Manual override wins over OS preference. */
:root[data-theme="light"] .theme-icon { --theme-icon-url: url("/static/icons/wx/clear-day.svg"); }
:root[data-theme="dark"]  .theme-icon { --theme-icon-url: url("/static/icons/wx/clear-night.svg"); }

/* Layout */
.grid {
  display: grid;
  gap: var(--gap);
  padding: var(--gap) calc(var(--gap) + env(safe-area-inset-right))
           calc(var(--gap) + env(safe-area-inset-bottom))
           calc(var(--gap) + env(safe-area-inset-left));
  max-width: 1400px;
  margin: 0 auto;
  grid-template-columns: 1fr;
  grid-template-areas:
    "current"
    "aircraft"
    "radar"
    "history"
    "wx";
}
/* Tablet and up: top row is current ↔ aircraft, the unified forecast panel
   spans the full width below. The forecast panel needs horizontal real-estate
   for the meteogram's 72-hour time axis to breathe; squeezing it into a
   quadrant would defeat the whole point. Radar slots full-width between the
   top row and forecast when shown. */
@media (min-width: 720px) {
  .grid {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "current   aircraft"
      "radar     radar"
      "history   history"
      "wx        wx";
  }
}
@media (min-width: 1100px) {
  .grid {
    grid-template-columns: 1.6fr 1fr;
    grid-template-areas:
      "current   aircraft"
      "radar     radar"
      "history   history"
      "wx        wx";
  }
}
.current  { grid-area: current; }
.aircraft { grid-area: aircraft; }
.radar    { grid-area: radar; }
.history  { grid-area: history; }
.wx       { grid-area: wx; }

/* Plane card visibility / position modifiers. Toggled via the settings drawer
   and persisted in localStorage. "bottom" reorders the grid so the aircraft
   card moves under the forecast and the current card spans the top row. */
:root[data-aircraft="hidden"] .aircraft { display: none; }
:root[data-aircraft="hidden"] .grid {
  grid-template-areas:
    "current"
    "radar"
    "wx";
}
@media (min-width: 720px) {
  :root[data-aircraft="hidden"] .grid {
    grid-template-columns: 1fr;
    grid-template-areas:
      "current"
      "radar"
      "history"
      "wx";
  }
}
:root[data-aircraft="bottom"] .grid {
  grid-template-areas:
    "current"
    "radar"
    "wx"
    "aircraft";
}
@media (min-width: 720px) {
  :root[data-aircraft="bottom"] .grid {
    grid-template-columns: 1fr;
    grid-template-areas:
      "current"
      "radar"
      "history"
      "wx"
      "aircraft";
  }
}
@media (min-width: 1100px) {
  :root[data-aircraft="bottom"] .grid {
    grid-template-columns: 1fr;
  }
}
/* Collapsed: hide the card body, keep the header strip with the chevron so
   the user can re-expand inline without re-opening the settings drawer. */
:root[data-aircraft="collapsed"] .aircraft .aircraft-body { display: none; }
:root[data-aircraft="collapsed"] .aircraft-collapse-btn .chev { transform: rotate(-90deg); }

/* Cards. Flex-column so the row stretch from CSS Grid (default align-items)
   propagates inwards: a designated child (e.g. forecast/hourly list) can
   take flex:1 and absorb the extra vertical space, distributing its rows
   evenly so the visual balance matches the card's row-mate. */
.card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--pad);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  /* Allow grid cells to shrink below their content's "natural" width on
     narrow viewports — the default `min-width: auto` would force the card
     to be at least as wide as the obs-table's widest row. */
  min-width: 0;
}
/* Wall + dark: drop shadows are invisible against #000, so swap in a 1px
   inset top-edge highlight + faint outer ring (defined in --card-elevation).
   This restores the "raised surface" feel that the light-desktop palette
   gets from --shadow. Other modes are unaffected because --card-elevation
   is only set in the wall+dark palette. */
:root[data-display="wall"][data-theme="dark"] .card,
:root[data-display="wall"]:not([data-theme="light"]) .card {
  box-shadow: var(--card-elevation, var(--shadow));
}
.card h2 {
  margin: 0 0 0.75rem 0;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* Current */
.current-temp {
  /* Top-align children so the degree glyph sits at the top of the digits
     instead of riding the baseline (which put it visually near the
     vertical middle of the big number). */
  display: flex; align-items: flex-start; gap: 0.1rem;
  /* Now a <button>: clicking surfaces the actual measured temp in a popup
     (the big number itself is feels-like). Strip native button chrome and
     keep the visual identical to the previous div. */
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.current-temp:focus { outline: none; }
.current-temp:focus-visible {
  outline: 2px solid var(--fg-muted);
  outline-offset: 4px;
}
.big-number {
  font-family: var(--font-num);
  font-size: clamp(3.5rem, 2rem + 8vw, 6rem);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 0.95;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
}
.degree {
  font-size: clamp(2rem, 1rem + 4vw, 3rem);
  color: var(--fg-muted);
  line-height: 1;
}
.current-desc {
  display: flex; align-items: center; gap: 0.5rem;
  color: var(--fg-muted);
  min-width: 0;   /* allow text to truncate inside grid cell instead of overflowing */
}
/* Two-line stack: kind on top ("Light Rain") + relative-time
   "started 25 min ago" on the second line, smaller and muted. The
   stack stays a single block so the icon-and-text flex layout keeps
   the icon vertically centred against both lines together.
   align-items: center horizontally centers the two lines relative
   to each other — so "started 25 min ago" sits under "Light Rain"
   on a shared centerline, not flush-left mis-aligned. */
.current-desc-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  line-height: 1.15;
}
.current-desc #current-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.current-desc .current-started {
  font-size: 0.78em;
  color: var(--fg-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.current-desc .current-started[hidden] { display: none; }

/* AQI pill — sits inline in the now/yest table cell, EPA color band per
   category, high-contrast text. The .aqi-* classes only set CSS variables;
   the pill itself takes its layout from .aqi-pill. Tooltip on hover/long-press
   surfaces the category name + pollutant + source. */
.aqi-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  padding: 0.18rem 0.55rem;
  border-radius: 999px;
  background: var(--aqi-bg, transparent);
  color: var(--aqi-fg, inherit);
  font-family: var(--font-num);
  font-weight: 600;
  font-size: 0.95rem;
  cursor: help;
  border: 1px solid color-mix(in srgb, var(--aqi-bg, var(--fg-muted)) 55%, transparent);
}
.cell-aqi { text-align: right; }
.aqi-good          { --aqi-bg: #c5edcb; --aqi-fg: #14532d; }
.aqi-moderate      { --aqi-bg: #fef3a8; --aqi-fg: #5a4a09; }
.aqi-usg           { --aqi-bg: #ffd7a8; --aqi-fg: #7a3e0b; }
.aqi-unhealthy     { --aqi-bg: #ffb3b3; --aqi-fg: #7a0a0a; }
.aqi-very-unhealthy{ --aqi-bg: #d6bbe0; --aqi-fg: #4a1758; }
.aqi-hazardous     { --aqi-bg: #c89094; --aqi-fg: #3b0a14; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    .aqi-good          { --aqi-bg: #1c4e2b; --aqi-fg: #c5edcb; }
    .aqi-moderate      { --aqi-bg: #6b5d10; --aqi-fg: #fef3a8; }
    .aqi-usg           { --aqi-bg: #7a4216; --aqi-fg: #ffd7a8; }
    .aqi-unhealthy     { --aqi-bg: #6b1818; --aqi-fg: #ffd0d0; }
    .aqi-very-unhealthy{ --aqi-bg: #4a1758; --aqi-fg: #d6bbe0; }
    .aqi-hazardous     { --aqi-bg: #3b0a14; --aqi-fg: #c89094; }
  }
}
:root[data-theme="dark"] {
  .aqi-good          { --aqi-bg: #1c4e2b; --aqi-fg: #c5edcb; }
  .aqi-moderate      { --aqi-bg: #6b5d10; --aqi-fg: #fef3a8; }
  .aqi-usg           { --aqi-bg: #7a4216; --aqi-fg: #ffd7a8; }
  .aqi-unhealthy     { --aqi-bg: #6b1818; --aqi-fg: #ffd0d0; }
  .aqi-very-unhealthy{ --aqi-bg: #4a1758; --aqi-fg: #d6bbe0; }
  .aqi-hazardous     { --aqi-bg: #3b0a14; --aqi-fg: #c89094; }
}
.icon-lg {
  display: inline-block;
  width: 2rem; height: 2rem;
  background-color: var(--icon-color);
  -webkit-mask: var(--ic) center / contain no-repeat;
          mask: var(--ic) center / contain no-repeat;
}

.obs-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem 1rem;
  margin: 0; padding: 0.75rem 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
@media (min-width: 480px) { .obs-grid { grid-template-columns: repeat(4, 1fr); } }
.obs-grid div { display: flex; flex-direction: column; }
.obs-grid dt { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-faint); margin: 0; }
.obs-grid dd { margin: 0; font-family: var(--font-num); font-variant-numeric: tabular-nums; font-size: 1.05rem; }

/* 3-column layout for the head of the current card:
       [ TEMP ][ icon (centered) ][ COMPASS ]
       [  .   ][ desc (centered) ][   .     ]
   The big condition icon between temp and compass visually balances
   the row from across the room (wall mode) and fills the horizontal
   void on desktop. On narrow viewports a container query collapses
   to the single-column stack pattern so mobile keeps the original
   shape. The radar button lives at the card's top-right corner —
   absolute-positioned so it doesn't disturb the headline grid. */
.card.current {
  container-type: inline-size;
  position: relative;
}
.current-block {
  display: grid;
  /* Three columns, ONE row — the middle column wraps icon + desc in a
     flex-column so the icon-to-description gap is a fixed 0.25rem
     regardless of how tall the temp digit makes the row. The previous
     2-row layout had the desc in row 2 and the gap was governed by
     grid row-gap math against the much-taller temp number, which
     produced a noticeable visual void between the icon and desc. */
  grid-template-columns: auto 1fr auto;
  grid-template-areas: "temp middle compass";
  align-items: center;
  column-gap: 1rem;
  margin-bottom: 0.6rem;
}
.current-block > * { min-width: 0; }
.current-block .current-temp     { grid-area: temp; }
.current-block .current-middle   { grid-area: middle; }
.current-block .wind-compass     { grid-area: compass; }
.current-middle {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;       /* fixed tight gap between icon and description */
  min-width: 0;
  /* Now a <button>: clicking surfaces the latest WFO Area Forecast
     Discussion in a popup. Strip native button chrome so the row
     looks identical to the previous div. Same pattern as
     .current-temp above. */
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  text-align: center;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.current-middle:focus { outline: none; }
.current-middle:focus-visible {
  outline: 2px solid var(--fg-muted);
  outline-offset: 4px;
}
.current-middle .current-desc {
  justify-self: center;
  max-width: 100%;
  min-width: 0;
}
/* The big middle icon. Same masked-SVG pattern as .icon-lg but sized
   to visually balance the headline temp and compass on either side.
   Hidden by default; CSS below switches it on for desktop and wall. */
.current-icon-wall {
  display: block;
  width:  clamp(3rem, 1.5rem + 5vw, 5rem);
  height: clamp(3rem, 1.5rem + 5vw, 5rem);
  background-color: var(--icon-color);
  -webkit-mask: var(--ic) center / contain no-repeat;
          mask: var(--ic) center / contain no-repeat;
}
/* With the big middle icon doing the visual work in the headline row,
   the small inline icon inside .current-desc is redundant — hide it.
   The narrow-viewport container query below restores it for mobile. */
.current-desc #current-icon { display: none; }

/* Mobile keeps the same 3-column temp/icon/compass shape as desktop,
   just with the middle icon sized down a bit so the row fits a 414px
   viewport without crowding. */
@container (max-width: 540px) {
  /* Shrink the middle icon a notch — the clamp ceiling was tuned
     for desktop card widths; on a 414px viewport the icon was
     visually competing with the compass for space. */
  .current-block #current-icon-wall {
    width:  clamp(2.25rem, 1.25rem + 3.5vw, 3.5rem);
    height: clamp(2.25rem, 1.25rem + 3.5vw, 3.5rem);
  }
}
@container (max-width: 340px) {
  /* Very narrow phones (sub-340px card width) — temp + compass don't
     fit alongside the icon anymore. Collapse to a vertical stack so
     the wind ring's 3.5rem minimum diameter doesn't blow out the row. */
  .current-block {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "temp"
      "icon"
      "desc"
      "compass";
  }
  .current-block .wind-compass { justify-self: start; }
  .current-block .current-desc { justify-self: start; }
}

/* Wind compass.
   The ring diameter uses the SAME clamp() formula as `.big-number`'s
   font-size so the compass and the temperature track each other visually
   across breakpoints. They render at approximately the same vertical
   height (temp's line-box ≈ compass diameter) in default and mobile;
   wall mode overrides both with a matching pixel clamp. */
.wind-compass {
  justify-self: end;
}
.compass-ring {
  position: relative;
  width: clamp(3.5rem, 2rem + 8vw, 6rem);
  height: clamp(3.5rem, 2rem + 8vw, 6rem);
  border: 2px solid var(--border); border-radius: 50%;
  background: radial-gradient(circle at center, var(--bg-card-2) 60%, transparent 61%);
  flex-shrink: 0;
}
.compass-mark {
  position: absolute; font-size: 0.7rem; color: var(--fg-faint); font-weight: 600;
}
.compass-mark.n { top: 4px; left: 50%; transform: translateX(-50%); }
.compass-mark.s { bottom: 4px; left: 50%; transform: translateX(-50%); }
.compass-mark.e { right: 4px; top: 50%; transform: translateY(-50%); }
.compass-mark.w { left: 4px; top: 50%; transform: translateY(-50%); }
/* Wind direction indicator: a wedge pointer that sits on the rim of the
   compass and rotates around the center. The wedge's tip points INWARD
   toward the speed readout, marking the direction the wind is coming FROM
   (meteorological convention). The base is an arc on the same radius as
   the inner rim, so it visually hugs the ring rather than sitting on a
   straight chord. */
.compass-needle {
  position: absolute;
  inset: 0;             /* covers the full ring; rotates around its center */
  pointer-events: none;
  transform-origin: 50% 50%;
  transform: rotate(0deg);
  transition: transform 0.4s ease;
}
.compass-needle-svg {
  display: block;
  width: 100%;
  height: 100%;
}
.compass-needle-svg path {
  fill: var(--fg);
}
@media (prefers-reduced-motion: reduce) {
  .compass-needle { transition: none; }
}
/* Speed readout centered inside the ring's inner disc (the 60% radial-
   gradient stop above). Stacked vertically so the number reads as the
   primary value and the unit sits underneath without crowding it. */
.compass-center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  pointer-events: none;
  line-height: 1;
}
/* Speed font is capped so the rendered text bbox (digits + stacked unit)
   fits inside a 20-viewBox-unit radius from the ring center — that's the
   distance the needle tip reaches from the rim toward center (40% of the
   radius). Going larger causes the digit corners to clip the needle tip
   at NE/NW/SE/SW headings. Same clamp() shape as `.compass-ring` width
   so the inside text scales with the ring. */
.compass-speed {
  font-family: var(--font-num);
  font-size: clamp(0.7rem, 0.4rem + 1.3vw, 1.2rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
}
.compass-unit {
  color: var(--fg-muted);
  font-size: clamp(0.5rem, 0.35rem + 0.4vw, 0.55rem);
}
.compass-speed.calm {
  font-size: clamp(0.65rem, 0.4rem + 0.7vw, 0.95rem);
  font-weight: 600;
  color: var(--fg-muted);
}
/* "Calm" already reads as a complete value — the "mph" unit next to it is
   redundant and visually awkward ("Calm mph"). Sibling selector keeps the
   hide rule colocated with the .calm modifier on its trigger element. */
.compass-speed.calm + .compass-unit { display: none; }

/* Phone layout: keep wind compass to the right of the temp instead of
   wrapping below it (the wrap was wasting a lot of vertical space). Must
   come AFTER the unconditional .current-top / .compass-* rules above so
   it actually wins the cascade at narrow widths. */
@media (max-width: 720px) {
  .current-block { column-gap: 0.6rem; }
  .current-temp { gap: 0.3rem; }
  /* +10% bump on the current-conditions block at narrow widths — the
     elements were reading small on phones. Each formula's `a` and `b`
     coefficients are scaled by 1.1; the upper cap stays at the desktop
     value so there's no discontinuity at the 720px breakpoint. Ring and
     speed text are bumped together with the temp so the temp↔compass
     symmetry holds and the digits stay clear of the needle. */
  .big-number { font-size: clamp(3.85rem, 2.2rem + 8.8vw, 6rem); }
  .degree { font-size: clamp(2.2rem, 1.1rem + 4.4vw, 3rem); }
  .compass-ring {
    width: clamp(3.85rem, 2.2rem + 8.8vw, 6rem);
    height: clamp(3.85rem, 2.2rem + 8.8vw, 6rem);
  }
  .compass-speed { font-size: clamp(0.77rem, 0.44rem + 1.43vw, 1.2rem); }
  .compass-mark { font-size: 0.6rem; }
}

/* Unified hazards block: tornado warning > other warnings > watches > outlook.
   Each item is a clickable link to the NWS / SPC details page. Compact text
   keeps the dashboard from feeling cluttered when nothing's active. */
.hazards { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.6rem; }
.hazard {
  text-decoration: none;
  display: flex; gap: 0.6rem; align-items: center;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-sm);
  font-size: 0.92rem;
}
.hazard:hover { filter: brightness(1.05); }
.hazard:focus-visible { outline-offset: 2px; }
.hazard::after { content: "↗"; margin-left: auto; opacity: 0.6; }

/* Tornado: red filled, bold, animated pulse (reduced-motion respected). */
.hazard-tornado {
  background: #cc0000;
  color: #fff;
  font-weight: 700;
  border: 2px solid #ff5252;
}
.hazard-tornado .hazard-icon {
  background: #fff; color: #cc0000;
  width: 1.9rem; height: 1.9rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.25rem; font-weight: 900; line-height: 1;
  flex-shrink: 0;
}
.hazard-tornado .hazard-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.hazard-tornado .hazard-title { letter-spacing: 0.05em; font-size: 0.95rem; }
.hazard-tornado .hazard-text {
  font-size: 0.82rem; font-weight: 500; opacity: 0.95;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@keyframes hazard-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.55); }
  50%      { box-shadow: 0 0 0 8px rgba(255, 82, 82, 0); }
}
.hazard-tornado:not([hidden]) { animation: hazard-pulse 1.6s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .hazard-tornado:not([hidden]) { animation: none; }
}

/* Other warnings: filled red, less loud than tornado. */
.hazard-list:not([hidden]) { display: flex; flex-direction: column; gap: 0.3rem; }
#hazard-warnings .hazard {
  background: color-mix(in srgb, var(--bad) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--bad) 35%, transparent);
  color: var(--bad);
  font-weight: 600;
}
/* Watches: outlined orange. */
#hazard-watches .hazard {
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--warn) 50%, transparent);
  color: var(--warn);
  font-weight: 500;
}

/* SPC outlook: subtle chip with category color. */
.hazard-outlook {
  background: var(--outlook-bg, transparent);
  border: 1px solid var(--outlook-border, var(--border));
  color: var(--outlook-fg, var(--fg-muted));
  font-size: 0.85rem;
  font-weight: 500;
}
.hazard-outlook strong { font-weight: 700; }

/* Site-wide tornado-active treatment: red top bar so the warning is
   unmistakable even when scrolled past the hazards block. */
:root[data-tornado-active="1"] .topbar {
  background: #cc0000;
  border-bottom-color: #ff5252;
}
:root[data-tornado-active="1"] .brand-name,
:root[data-tornado-active="1"] .brand-sub,
:root[data-tornado-active="1"] .freshness {
  color: #fff;
}
:root[data-tornado-active="1"] .theme-toggle {
  background: rgba(255,255,255,0.15);
  border-color: rgba(255,255,255,0.4);
  color: #fff;
}
/* The clock digits inherit white from .brand-clock's .topbar context,
   but .clock-seconds (normally var(--fg-faint)) and #now-date (normally
   var(--fg-muted)) carry their own muted gray that turns to mud on the
   red bar. Lift them to a white tint at reduced alpha so they read as
   "secondary text on red" without competing with the bold main digits. */
:root[data-tornado-active="1"] .clock-seconds,
:root[data-tornado-active="1"] #now-date {
  color: rgba(255, 255, 255, 0.78);
}
:root[data-tornado-active="1"] .brand-clock { color: #fff; }

/* Lightning ticker (observed real-time, distinct from hazards). Each row is
   a clickable link to lightningmaps.org centred on the station, but styled
   so the existing visual is preserved — no blue, no underline, just cursor
   feedback on hover. */
.lightning:empty { display: none; }
.lightning {
  margin-top: 0.5rem;
  display: flex; flex-direction: column; gap: 0.2rem;
  font-size: 0.9rem; color: var(--warn);
}
.lightning .lightning-strike-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.lightning .lightning-strike-link:hover { text-decoration: none; filter: brightness(1.15); }
.lightning .lightning-strike-link:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--warn) 50%, transparent);
  outline-offset: 2px;
  border-radius: 2px;
}
.lightning > a::before,
.lightning > div::before { content: "⚡"; margin-right: 0.4rem; }

/* Aircraft */
.aircraft.is-empty { padding-block: 0.85rem; }
.aircraft.is-empty h2 { margin-bottom: 0.4rem; }
#aircraft-empty { font-size: 0.92rem; color: var(--fg-muted); }
#aircraft-empty .last-seen { display: block; margin-top: 0.25rem; font-size: 0.82rem; color: var(--fg-faint); }
.aircraft-card:not([hidden]) { display: grid; gap: 0.6rem; }
.aircraft-staleness {
  font-size: 0.85rem;
  font-style: italic;
  color: var(--fg-muted);
}
.aircraft-card.is-stale .aircraft-photo img { opacity: 0.7; }
.aircraft-card.is-stale .aircraft-callsign { color: var(--fg-muted); }
.aircraft-head {
  display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; justify-content: space-between;
}
/* Inner wrap holds callsign + flags as a single unit so the seen-time can
   sit on the right via space-between without ending up sandwiched. */
.aircraft-head-main {
  display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
  min-width: 0;
}
.aircraft-seen {
  font-size: 0.78rem;
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex-shrink: 0;
}
.aircraft-card.is-stale .aircraft-seen { color: var(--fg-faint); }
.aircraft-callsign {
  margin: 0; font-family: var(--font-num); font-size: clamp(1.4rem, 1rem + 2vw, 1.8rem);
  font-weight: 600; letter-spacing: 0.02em;
}
.flags { display: inline-flex; gap: 0.4rem; }
.flag {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em;
  padding: 0.15rem 0.5rem; border-radius: 999px; font-weight: 700;
}
.flag.mil { background: color-mix(in srgb, var(--bad) 18%, transparent); color: var(--bad); border: 1px solid color-mix(in srgb, var(--bad) 40%, transparent); }
.flag.int { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent); }
/* High-altitude fallback chip — muted blue/neutral so it reads as "info"
   rather than "warning" (mil/int both lean red/orange). The viewer should
   feel "ah, that's just the closest plane right now", not "alert". */
.flag.hi-alt {
  background: color-mix(in srgb, var(--fg-muted) 12%, transparent);
  color: var(--fg-muted);
  border: 1px solid color-mix(in srgb, var(--fg-muted) 28%, transparent);
}
/* Subtle card-level differentiation for the high-altitude fallback case:
   the card content is rendered at slightly reduced opacity so it visually
   recedes vs. a "real" low-flying overhead pick. Not loud enough to look
   broken or stale — just enough that at a glance you can tell this isn't
   the headline event the card normally surfaces. */
.aircraft .aircraft-card.is-high-altitude { opacity: 0.88; }
.aircraft .aircraft-card.is-high-altitude .aircraft-photo { opacity: 0.78; }
/* Aircraft photo block: fixed-aspect-ratio frame with the actual image
   rendered inside via object-fit: contain (so the plane is never
   cropped). When the image's aspect ratio differs from the frame, the
   exposed background is the card's own surface color — a clean letter-
   box rather than a flat empty bar, because the card around it is the
   same color. The image gets its own rounded corners so it reads as a
   centered photo inside the letterbox rather than a clipped fill. */
.aircraft-photo {
  margin: 0;
  position: relative;
  width: 100%;
  max-height: 240px;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: var(--radius-sm);
  /* No background — the photo sits directly on the parent card's
     surface, and any letterbox space when the image's aspect ratio
     differs from 3:2 reveals that card color. */
  /* Flex-center the image so it renders at its natural aspect (capped
     by max-width / max-height) instead of stretching to fill the
     letterbox. Without this the <img> element is 100%×100% and its
     border-radius rounds the letterbox corners, not the photo's. */
  display: flex;
  align-items: center;
  justify-content: center;
}
.aircraft-photo img {
  display: block;
  /* Natural sizing within the figure's box. width/height attrs from JS
     give the browser an aspect ratio to reserve space with while the
     image loads; max-* fits the largest dimension to the figure. */
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  /* Pronounced rounded corners on the actual photo edges. */
  border-radius: 16px;
}
.aircraft-photo figcaption { font-size: 0.75rem; margin-top: 0.25rem; }
@media (max-width: 540px) {
  /* Slightly smaller cap on phones so the photo doesn't dominate the screen. */
  .aircraft-photo { max-height: 180px; }
}
.aircraft-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.4rem 1rem;
  margin: 0;
}
@media (min-width: 540px) { .aircraft-grid { grid-template-columns: repeat(3, 1fr); } }
/* Grid items default to min-width:auto = min-content, which equals the
   longest unbreakable run of content. Combined with the nowrap+ellipsis
   spans inside the Type cell, that lets a long manufacturer or model
   name force the column wider than its 1fr share — squeezing other
   columns. min-width:0 on the wrapping div opts out of min-content
   sizing so the 1fr constraint actually constrains. */
.aircraft-grid > div { min-width: 0; }
.aircraft-grid dt { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-faint); margin: 0; }
.aircraft-grid dd { margin: 0; font-variant-numeric: tabular-nums; min-width: 0; }
/* Type cell renders as a two-line stack: manufacturer on top, model
   below (see setAircraftType in display.js). Without these rules, long
   technical names like "Bombardier CL-600-2D24 Challenger 850" wrap to
   4–5 lines and explode the row height. Each child span clamps to one
   line via ellipsis; the dd itself flexes vertically so the two lines
   stack predictably. min-width:0 is the load-bearing bit — without it
   the children won't shrink below their content width inside a grid
   cell and the ellipsis never triggers. */
#aircraft-type {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
#aircraft-type > span {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  max-width: 100%;
}

/* Now / Yesterday table — labels under the values, like the office-tv-display.
   Wrapped in .obs-table-wrap which provides horizontal scroll-as-needed on
   very narrow viewports without spilling into the rest of the page. */
.obs-table-wrap {
  margin: 0.5rem 0 0.75rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.obs-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.obs-table th, .obs-table td {
  padding: 0.45rem 0.4rem;
  text-align: right;
}
.obs-table tbody th {
  text-align: left;
  color: var(--fg-muted);
  font-weight: 500;
  white-space: nowrap;
}
.obs-table .row-now td,
.obs-table .row-today td,
.obs-table .row-yest td {
  font-family: var(--font-num);
  font-size: 1rem;
}
/* Now + Today read at full strength (current relevance); Yest. is the
   historical reference and gets the muted treatment so the eye lands on
   the live values first. */
.obs-table .row-yest td, .obs-table .row-yest th { color: var(--fg-muted); }
.obs-table .row-labels td {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-faint);
  border-top: 1px solid var(--border);
  padding-top: 0.4rem;
}
/* Faint vertical hairlines between columns. Best-practice for narrow data
   tables: the rules let the eye trace a value down to its column header
   without crowding cells with extra padding. Subtle enough at default
   text size, load-bearing once the user picks large/xlarge on a phone. */
.obs-table tbody td + td,
.obs-table tbody th + td {
  border-left: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
/* Wall + dark: the transparent-mixed hairline above collapses to ~#262626
   on the #1c1c1c card bg — invisible from across a room. Use the full
   --border value (#3a3a3a) so the column grid actually reads, and add
   horizontal hairlines between the data rows so Now / Today / Yest sit
   as distinct bands instead of one mushy block. */
:root[data-display="wall"][data-theme="dark"] .obs-table tbody td + td,
:root[data-display="wall"][data-theme="dark"] .obs-table tbody th + td,
:root[data-display="wall"]:not([data-theme="light"]) .obs-table tbody td + td,
:root[data-display="wall"]:not([data-theme="light"]) .obs-table tbody th + td {
  border-left-color: var(--border);
}
:root[data-display="wall"][data-theme="dark"] .obs-table .row-now td,
:root[data-display="wall"][data-theme="dark"] .obs-table .row-now th,
:root[data-display="wall"][data-theme="dark"] .obs-table .row-today td,
:root[data-display="wall"][data-theme="dark"] .obs-table .row-today th,
:root[data-display="wall"]:not([data-theme="light"]) .obs-table .row-now td,
:root[data-display="wall"]:not([data-theme="light"]) .obs-table .row-now th,
:root[data-display="wall"]:not([data-theme="light"]) .obs-table .row-today td,
:root[data-display="wall"]:not([data-theme="light"]) .obs-table .row-today th {
  border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
@media (max-width: 540px) {
  .obs-table th, .obs-table td { padding: 0.4rem 0.2rem; }
  .obs-table .row-now td,
  .obs-table .row-today td,
  .obs-table .row-yest td {
    font-size: 0.85rem;
    /* Force nowrap on data cells so multi-word values like "22 mph" and
       "0.10 in/hr" never wrap mid-cell. Without nowrap, narrow columns
       break the string into "22" + "mph" on two lines — looks broken
       even at the default text size on a 414px phone. */
    white-space: nowrap;
  }
  .obs-table .row-labels td { font-size: 0.6rem; }
  .aqi-pill {
    min-width: 1.5rem; padding: 0.12rem 0.4rem; font-size: 0.85rem;
  }
  /* Stack hi/lo vertically in temp + humidity cells at ALL mobile
     widths (not just large/xlarge). Both lines render at the same
     font-size for legibility and visual consistency — the lo gets
     a muted color so the eye still picks out which is which, but
     it's never smaller than the hi. The freed horizontal space lets
     the data font stay at 0.85rem instead of the 0.78rem cramp we'd
     need with the inline "70°/58°" pair. */
  .obs-table .hilo {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-end;
    line-height: 1.1;
    vertical-align: middle;
  }
  .obs-table .hilo-sep { display: none; }
  .obs-table .hilo-lo {
    color: var(--fg-muted);
  }
}

/* hi/lo cell composition: default rendering is inline ("70°/58°"). The
   structured spans only matter at large/xlarge mobile, where the rules
   below switch the layout to a vertical stack so the data font can stay
   readable without the column shrinking to fit a wide horizontal pair. */
.hilo, .hilo-only { display: inline; }
.hilo-hi, .hilo-sep, .hilo-lo { display: inline; }
.hilo-sep { margin: 0 0.05em; }
/* Trigger lives in the row-labels' first cell. The table-wide
   `text-align: right` is meant for numeric columns — flip it back to
   left here so the link visually aligns with the "Now:"/"Yest.:" row
   headers above it. */
.obs-table td.cell-history-trigger {
  text-align: left;
  padding-left: 0;
}
/* Larger text sizes blow out the obs-table on phone widths and force a
   horizontal scrollbar. Three coordinated fixes for large/xlarge mobile:
     1. Squeeze cell padding so columns can sit closer together.
     2. Force `white-space: nowrap` on every data cell so multi-word
        values like "22 mph" and "0.10 in/hr" stay on one line — without
        nowrap they wrap mid-cell and the units sit on a second row,
        making the table read as a noisy stack of fragments.
     3. Pin the data font-size to a smaller absolute value than the rest
        of the page picks up from the xlarge root bump. The accessibility
        ask of "make text larger" doesn't really apply to a dense 7-col
        numeric table — the column headers (TEMP/HUM/WIND/PRECIP/UV/AQI)
        below stay readable, and the data fits inside the card instead
        of pushing AQI off-screen. */
@media (max-width: 720px) {
  :root[data-text-size="large"] .obs-table th,
  :root[data-text-size="large"] .obs-table td,
  :root[data-text-size="xlarge"] .obs-table th,
  :root[data-text-size="xlarge"] .obs-table td {
    padding-left: 0.12rem;
    padding-right: 0.12rem;
  }
  :root[data-text-size="large"] .obs-table tbody th,
  :root[data-text-size="xlarge"] .obs-table tbody th {
    padding-right: 0;
  }
  /* Pin data-cell font-size + force nowrap. Two-decimal values like
     "78%/48%" still fit; longer values like "0.10 in/hr" don't wrap
     mid-cell. Yesterday's row stays muted as before via the existing
     .row-yest rule. */
  /* large/xlarge mobile bumps the data font on top of the stacked
     hi/lo layout the base @media (max-width: 540px) block already
     provides — the stack rules cascade through, this block only sets
     font-size. AQI pill sized to match below. */
  :root[data-text-size="large"] .obs-table .row-now td,
  :root[data-text-size="large"] .obs-table .row-today td,
  :root[data-text-size="large"] .obs-table .row-yest td {
    font-size: 0.85rem;
    white-space: nowrap;
  }
  :root[data-text-size="xlarge"] .obs-table .row-now td,
  :root[data-text-size="xlarge"] .obs-table .row-today td,
  :root[data-text-size="xlarge"] .obs-table .row-yest td {
    /* 0.82rem at root 20px ≈ 16.4px — substantially larger than the
       cramped 0.62rem we'd otherwise need. Stacking (inherited from
       base mobile rules) frees the horizontal space. Pushed higher
       than this and the AQI pill falls off the right edge of the
       card on 414px viewports. */
    font-size: 0.82rem;
    white-space: nowrap;
  }
  /* Row labels (Now: / Today: / Yest.:) and column labels
     (TEMP/HUM/WIND/PRECIP/UV/AQI) at large/xlarge mobile. Sized to
     match the bumped data font without becoming headlines. */
  :root[data-text-size="large"] .obs-table tbody th,
  :root[data-text-size="xlarge"] .obs-table tbody th {
    font-size: 0.85rem;
  }
  :root[data-text-size="large"] .obs-table .row-labels td,
  :root[data-text-size="xlarge"] .obs-table .row-labels td {
    font-size: 0.65rem;
  }
  /* AQI pill — sized to match the row's data font at large/xlarge mobile
     so it doesn't visually shrink while the rest of the row grows. */
  :root[data-text-size="large"] .obs-table .aqi-pill,
  :root[data-text-size="xlarge"] .obs-table .aqi-pill {
    min-width: 1.6rem;
    padding: 0.12rem 0.4rem;
    font-size: 0.85rem;
  }
}
/* Rain totals row — multiple small stats laid out evenly across the card. */
.rain-totals {
  display: grid;
  /* Three columns: rain-7-day | rain-30-day | radar pill. Radar gets
     auto width so it doesn't squeeze the rain-stat columns; it
     right-aligns within its own column to sit cleanly against the
     card edge. On mobile the desktop radar pill is hidden (see
     .radar-toggle-desktop rules below) and rain-totals visually
     reverts to 2 stat columns. */
  grid-template-columns: 1fr 1fr auto;
  gap: 1rem;
  margin-top: 0.6rem;
  padding-top: 0.55rem;
  border-top: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
  align-items: center;
}
.rain-totals .radar-toggle-desktop {
  justify-self: end;
  align-self: center;
  margin: 0;
}
.rain-stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
}
.rain-stat-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-faint);
}
.rain-stat > :last-child {
  font-family: var(--font-num);
  font-weight: 600;
  font-size: 1rem;
}
/* Mobile-only "More details" toggle — collapses rain-totals + astro-grid
   so the at-a-glance view (current conditions + forecast) fits without
   scrolling on a phone. Hidden on desktop, where #more-details always
   renders inline at full size. */
.more-details-toggle {
  display: none;   /* desktop hides; the mobile @media block flips this on */
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  margin: 0.5rem auto 0;
  padding: 0.2rem 0.4rem;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--fg-faint);
  font: inherit;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
}
.more-details-toggle:hover { color: var(--fg-muted); }
.more-details-toggle:focus-visible {
  outline: 1px solid var(--fg-faint); outline-offset: 2px;
}
.more-details-toggle .chev {
  display: inline-block;
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform 0.15s ease;
}
:root[data-current-details="expanded"] .more-details-toggle .chev {
  transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
  .more-details-toggle .chev { transition: none; }
}
/* Mobile-only bottom-of-card action row: two half-width buttons —
   "Show details" (expand the current-conditions card) on the left,
   "Radar" (open the full-screen radar overlay) on the right. Visually
   consistent so they read as a pair. On desktop the wrapper acts as
   a `display: contents` shim (its children flow as direct siblings
   of .card.current) so the more-details-toggle stays hidden and the
   desktop radar pill lives inside the rain-totals row instead. */
.card-actions { display: contents; }
/* The mobile twin of the radar pill — hidden on desktop. Two-class
   selector (.radar-toggle-btn.radar-toggle-mobile) gives 0,0,2,0
   specificity so it wins over the single-class .radar-toggle-btn
   rule's display:inline-flex defined later in the file. */
.radar-toggle-btn.radar-toggle-mobile { display: none; }
@media (max-width: 720px) {
  /* Card actions becomes a real grid container at mobile widths.
     Half-width buttons side by side, right one (Radar) sits under
     the thumb of a right-handed phone hold. iOS-action-sheet
     treatment: subtle top hairline frames the row as a distinct
     actions area, and a vertical hairline between the two halves
     signals "tap targets" without giving each button heavy chrome.
     gap: 0 so the hairline is a single shared edge, not floating in
     space between gapped children. */
  .card-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0;
    margin-top: 0.5rem;
    padding-top: 0.4rem;
    border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
    position: relative;
  }
  /* Vertical divider between the two action buttons. Drawn as a
     positioned pseudo-element on the row rather than a border on either
     button — keeps the buttons themselves visually borderless (their
     own border-radius would otherwise curve any per-button divider into
     a parenthesis) and gives us full control over the line's height
     independent of the buttons' content box. */
  .card-actions::before {
    content: '';
    position: absolute;
    top: 0.5rem;
    bottom: 0.15rem;
    left: 50%;
    width: 1px;
    margin-left: -0.5px;
    /* Same base recipe as the horizontal hairline (border 60% over
       transparent) but nudged ~30% toward --fg-faint so the vertical
       reads as a slightly darker sibling of the bar above. */
    background: color-mix(in srgb, var(--border) 70%, var(--fg-faint) 30%);
    pointer-events: none;
  }
  /* The desktop radar pill (sibling inside .rain-totals on desktop)
     is hidden on mobile — the mobile twin in card-actions takes over. */
  .rain-totals .radar-toggle-desktop { display: none; }
  /* Show the mobile twin, styled to visually match the more-details
     toggle so the pair reads as a consistent UI element. Specificity
     matches the base hide rule (0,0,2,0); source order picks this. */
  .radar-toggle-btn.radar-toggle-mobile {
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    padding: 0.1rem 0.4rem;
    background: transparent;
    border: 0;
    border-radius: var(--radius-sm);
    color: var(--fg-faint);
    font: inherit;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    cursor: pointer;
  }
  .radar-toggle-btn.radar-toggle-mobile:hover { color: var(--fg-muted); }
  .radar-toggle-btn.radar-toggle-mobile:focus-visible {
    outline: 1px solid var(--fg-faint); outline-offset: 2px;
  }
  .radar-toggle-btn.radar-toggle-mobile[aria-expanded="true"] {
    color: var(--accent);
  }
  /* Show-details on mobile — overrides the default `display: none`. */
  .more-details-toggle {
    display: flex;
    width: 100%;
    /* Tighter than desktop — every saved px on mobile pulls the next
       card up into the visible viewport. */
    margin: 0;
    padding: 0.1rem 0.4rem;
  }
  :root[data-current-details="collapsed"] #more-details { display: none; }
  /* Tighten outer/inter-card spacing on phones so the forecast card's
     top edge sits closer to the bottom of the current card and more
     of it is visible at first paint without scrolling. Wall mode (very
     unusual on a phone) keeps its own --gap/--pad. */
  :root:not([data-display="wall"]) {
    --gap: 0.35rem;
    --pad: 0.65rem;
  }
  .obs-table-wrap { margin: 0.4rem 0 0.4rem; }
}

/* Sun & Moon — flat layout, no inner backgrounds, matches obs-grid style.
   Lives inline at the bottom of the current card, separated by a hairline. */
.astro-grid {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: 1fr;
  margin-top: 0.6rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--border);
}
@media (min-width: 480px) { .astro-grid { grid-template-columns: 1fr 1fr; } }
.astro-header {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-bottom: 0.6rem;
}
.astro-icon {
  width: 1.375rem; height: 1.375rem;
  display: inline-block;
  background-color: var(--icon-color);
  -webkit-mask: var(--ic) center / contain no-repeat;
          mask: var(--ic) center / contain no-repeat;
}
.astro-icon.astro-sun {
  --ic: url("/static/icons/wx/clear-day.svg");
  background-color: #f5b748;
}
.astro-icon.astro-moon {
  --ic: url("/static/icons/wx/clear-night.svg");
}
.astro-data {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.55rem 1rem;
  margin: 0;
}
.astro-data div { display: flex; flex-direction: column; min-width: 0; }
.astro-data dt {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--fg-faint); margin: 0;
  white-space: nowrap;
}
.astro-data dt[data-icon]::before {
  content: '';
  display: inline-block;
  /* 14px reads cleanly even at 100% zoom; the bold arrow geometry inside
     the SVGs is what does the heavy lifting. Sized to sit on the cap-height
     of the 0.7rem dt text so it doesn't expand the line box. */
  width: 14px; height: 14px;
  margin-right: 4px; vertical-align: -3px;
  background-color: var(--fg-muted);
  -webkit-mask: var(--icon-url) center / contain no-repeat;
          mask: var(--icon-url) center / contain no-repeat;
}
.astro-data dt[data-icon="sunrise"]::before { --icon-url: url("/static/icons/wx/sunrise.svg"); }
.astro-data dt[data-icon="sunset"]::before  { --icon-url: url("/static/icons/wx/sunset.svg"); }

/* Inline moon-phase glyph — sits to the left of the phase name in the
   "Phase" dd. The icon is rendered as two SVG layers (a shadow-side
   disc, plus a lit-side path on top) and we fill them with explicit
   colors via the classes below so the lit portion reads as bright/
   white-ish in BOTH themes — same way the moon actually looks in the
   sky, where the illuminated face is always the bright side. Earlier
   the icon used currentColor for both, which made the dark portion
   blend into the page background and inverted the perceived phase. */
.moon-phase-icon {
  display: inline-block;
  /* ~50% bigger than the original 0.875rem so the phase shape reads
     clearly without dominating the row. */
  width: 1.3125rem;
  height: 1.3125rem;
  vertical-align: -4px;
  margin-right: 0.4rem;
  /* Theme-aware perimeter ring uses currentColor → page foreground,
     which is the inverse of the page background by design (dark fg on
     light bg; light fg on dark bg). Drives the .moon-ring stroke
     below. */
  color: var(--fg);
  /* Disc fills are absolute (don't change per theme) so the
     illuminated portion always reads as bright white and the unlit
     portion always reads as dark, regardless of which mode the user
     is in — same way the real moon looks in the sky. */
  --moon-dark: #1a1a1a;
  --moon-lit: #ffffff;
}
.moon-phase-icon .moon-dark { fill: var(--moon-dark); }
.moon-phase-icon .moon-lit  { fill: var(--moon-lit); }
.moon-phase-icon .moon-ring {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.astro-data dd {
  margin: 0;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- Unified forecast panel: meteogram + 7-day hybrid ---------- */
.wx-block + .wx-block { margin-top: 1.25rem; }
.wx-block-title {
  margin: 0 0 0.5rem 0;
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-muted);
}

/* ---- 24-hour forecast panel ----
   Two layers stacked vertically:
     1. Header cells (.hf-cell) — one per 3hr block (or 6hr on mobile),
        shows hour + condition icon + feels-like temp. Cell BG tints with
        cloud cover (option A).
     2. Sky ribbon (.hf-sky-ribbon) — thin strip below the headers, opacity
        per cell encodes cloud cover (option B). Both A and B ship; one
        gets removed once the user picks. The chart SVG below draws a
        continuous feels-like curve + per-hour precip bars across all
        columns, with hairlines at column boundaries (stronger at midnight). */
.hourly-forecast {
  position: relative;     /* anchor for the absolute .hf-day-line overlays */
  background: transparent;
  border-radius: var(--radius-sm);
  overflow: hidden;
}
/* Single full-height vertical line at midnight that overlays header → sky
   ribbon → chart in one continuous stroke. Replaces three per-region
   markers that were drifting apart by sub-pixel amounts. */
.hf-day-line {
  position: absolute;
  top: 0; bottom: 0;
  width: 1px;
  background: var(--fg-muted);
  pointer-events: none;
  z-index: 2;
}

.hf-headers {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(var(--cols, 8), 1fr);
}
.hf-cell {
  background: var(--bg-card-2);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.55rem 0.4rem 0.55rem;
  border-right: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}
.hf-cell:last-child { border-right: none; }
.hf-cell-hour {
  color: var(--fg-muted); font-size: 0.78rem;
  text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600;
}
.hf-cell-icon {
  width: 2rem; height: 2rem;
  background-color: var(--icon-color);
  -webkit-mask: var(--ic) center / contain no-repeat;
          mask: var(--ic) center / contain no-repeat;
  margin: 0.2rem 0;
}
.hf-cell-temp {
  font-family: var(--font-num); font-weight: 600; font-size: 1.05rem;
}

.hf-sky-ribbon {
  position: relative;
  height: 9px;     /* 50% taller than the original 6px */
  background: var(--bg-card-2);
  border-top: 1px solid var(--border);
}
.hf-sky-cell {
  position: absolute; top: 0; bottom: 0;
  background: var(--icon-color);
}

.hf-chart {
  position: relative;       /* anchor for the precip row label */
  width: 100%;
  background: var(--bg-card-2);
}

/* Tap-popup for the 24hr cells — surfaces the same multi-line content
   that lives in each cell's native title attr. Necessary because mobile
   browsers don't show title attrs on tap (long-press opens a system
   menu). On desktop the native hover tooltip still works; this popup
   fires on click for everyone. */
.hf-tap-popup {
  position: fixed;
  z-index: 100;
  background: var(--bg-card);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.7rem 0.85rem;
  box-shadow: var(--shadow);
  font-size: 0.85rem;
  line-height: 1.5;
  max-width: 240px;
  font-variant-numeric: tabular-nums;
  pointer-events: auto;
}
.hf-tap-popup[hidden] { display: none; }
/* Wider variant for the 7-day popup, which carries a paragraph-style
   detailed forecast in addition to the per-day facts. 240px (default) is
   tight for prose; 280px keeps it readable without dominating a phone. */
.hf-tap-popup--wide { max-width: 280px; }
.hf-tap-line { color: var(--fg); }
.hf-tap-line.hf-tap-day {
  color: var(--fg-muted);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: 0.75rem;
  margin-bottom: 0.3rem;
  padding-bottom: 0.3rem;
  border-bottom: 1px solid var(--border);
}
/* Key/value grid — small uppercase label on the left, prominent value on
   the right. ONE grid for the whole popup so labels share a width column
   (the widest label sets the column for all rows, keeping values aligned
   vertically) and values get a flexible second column that wraps when
   their text is wider than the share of the popup that's left over.
   Without this single-grid arrangement, a long value like "Slight Chance
   Light Rain" would overlap the label cell (per-row `minmax(0, 1fr)` lets
   the label cell shrink to 0 width while its text still renders). */
.hf-tap-grid {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 0.6rem;
  row-gap: 0.36rem;
  align-items: baseline;
  margin-top: 0.1rem;
}
.hf-tap-k {
  color: var(--fg-muted);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-variant-numeric: normal;
  font-weight: 500;
}
.hf-tap-v {
  color: var(--fg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
  /* Allow long values to wrap inside their column rather than overflow
     into the label or out the side of the popup. break-word handles
     edge cases like a single very long token without spaces. */
  overflow-wrap: break-word;
}
/* Prose block for the NWS detailedForecast text — set off from the bullet
   facts above by a hairline, slightly smaller and softer so it reads as a
   description rather than another key/value fact. */
.hf-tap-detail {
  color: var(--fg-muted);
  font-size: 0.8rem;
  line-height: 1.45;
  margin-top: 0.45rem;
  padding-top: 0.45rem;
  border-top: 1px solid var(--border);
}

/* AFD popup blocks — variants on .hf-tap-detail tuned for a longer prose
   section (the WFO synopsis / what's-changed) plus a strip of outbound
   links to the full product and the office homepage. The popup reuses
   .hf-tap-popup (--wide variant on desktop; --afd flips to fullscreen on
   phones). AFD-specific styling here covers the two-line header with
   close button, the segmented "section" tabs, the multi-paragraph prose
   layout, and the link strip. */
/* Header row: title + time stacked on the left, close button parked
   on the right. flex layout so the close button (mobile-only) sits in
   the corner without affecting text wrapping when it's hidden. */
.afd-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
}
.afd-header-text { min-width: 0; flex: 1 1 auto; }
/* Two-line header text: bold uppercase title ("NWS BOX DISCUSSION") on
   line 1, issuance time ("Thu, 1:59 PM EDT") on line 2 in a fainter,
   normal-case sub-line. Stacking them keeps the popup width usable on
   phones — a single combined line wrapped awkwardly in the middle of
   the timestamp. */
.hf-tap-day .afd-title { display: block; }
.hf-tap-day .afd-issued {
  display: block;
  color: var(--fg-faint);
  font-weight: 500;
  font-size: 0.7rem;
  letter-spacing: 0;
  text-transform: none;
  margin-top: 0.15rem;
}
/* Close button: hidden on desktop where outside-click + scroll dismiss
   the anchored popup; visible (and chunky enough to be a thumb target)
   only when the popup goes fullscreen on phones — see the
   .hf-tap-popup--afd block lower down. */
.afd-close-btn {
  display: none;
  background: var(--bg-card-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--fg);
  padding: 0.35rem 0.55rem;
  cursor: pointer;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  min-height: 2.25rem;
  font: inherit;
  flex: 0 0 auto;
}
.afd-close-btn:hover { filter: brightness(1.08); }
.afd-close-btn:focus { outline: none; }
.afd-close-btn:focus-visible {
  outline: 2px solid var(--fg-muted);
  outline-offset: 2px;
}
.afd-close-btn .close-icon { font-size: 1.3rem; line-height: 1; }
/* Segmented tabs — surfaced only when WHAT HAS CHANGED is present (the
   mid-day update diff). The strip overlaps the header's bottom hairline
   by 1 px so the active tab's underline visually replaces that hairline,
   producing a clean "underlined active tab" effect without doubling
   borders. */
.afd-tabs {
  display: flex;
  gap: 0.15rem;
  margin: 0.1rem 0 0.45rem 0;
  border-bottom: 1px solid var(--border);
}
.afd-tab {
  flex: 1 1 auto;
  background: transparent;
  border: 0;
  padding: 0.35rem 0.4rem;
  font: inherit;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-faint);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.afd-tab:hover { color: var(--fg-muted); }
.afd-tab--active {
  color: var(--fg);
  border-bottom-color: var(--fg-muted);
}
.afd-tab:focus { outline: none; }
.afd-tab:focus-visible {
  outline: 2px solid var(--fg-muted);
  outline-offset: 2px;
  border-radius: 2px;
}
.afd-tab-panel { display: none; }
.afd-tab-panel--active { display: block; }
.afd-prose {
  color: var(--fg);
  font-size: 0.82rem;
  line-height: 1.5;
  /* AFDs can run several paragraphs; cap the visible height and let the
     popup scroll inside its own box rather than blowing out the page. */
  max-height: 22rem;
  overflow-y: auto;
}
.afd-para { margin: 0 0 0.55rem 0; }
.afd-para:last-child { margin-bottom: 0; }
.afd-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.9rem;
  margin-top: 0.55rem;
  padding-top: 0.45rem;
  border-top: 1px solid var(--border);
  font-size: 0.78rem;
}
.afd-link {
  color: var(--link, var(--fg-muted));
  text-decoration: underline;
  text-underline-offset: 2px;
}
.afd-link:hover, .afd-link:focus { color: var(--fg); }

/* Mobile: AFD popup goes fullscreen. The desktop anchored popup is too
   cramped for several paragraphs on a phone, and there's no good place
   to dismiss-by-outside-tap when content covers most of the screen
   anyway. Below the 720px breakpoint the popup pins to the viewport,
   lets its own body scroll, and the close button in the header becomes
   the dismiss affordance. The popup's own `position:fixed` plus left/top
   inline styles set by showHTML are cleared by JS before this rule
   applies, so inset:0 wins without an !important arms race. */
@media (max-width: 720px) {
  .hf-tap-popup--afd {
    inset: 0;
    max-width: none;
    max-height: none;
    border-radius: 0;
    border: 0;
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
             env(safe-area-inset-bottom) env(safe-area-inset-left);
    overflow-y: auto;
    /* Stack above the radar / history fullscreen cards if either is also
       open — z=100 was fine for the small anchored popup; bumping to
       110 keeps the AFD above everything when it's covering the page. */
    z-index: 110;
    display: flex;
    flex-direction: column;
  }
  .hf-tap-popup--afd .afd-header {
    padding: 0.75rem 0.75rem 0.5rem;
    border-bottom: 1px solid var(--border);
    margin-bottom: 0.5rem;
    position: sticky;
    top: 0;
    background: var(--bg-card);
    z-index: 1;
  }
  /* In fullscreen the popup itself scrolls; let the prose body grow as
     needed instead of fighting an internal max-height that would create
     a nested scroll inside an already-scrolling container. */
  .hf-tap-popup--afd .afd-prose {
    max-height: none;
    overflow-y: visible;
    font-size: 0.95rem;
    padding: 0 0.75rem;
  }
  .hf-tap-popup--afd .afd-tabs {
    margin-left: 0.75rem;
    margin-right: 0.75rem;
  }
  .hf-tap-popup--afd .afd-tab {
    font-size: 0.8rem;
    padding: 0.55rem 0.5rem;
    min-height: 2.5rem;
  }
  .hf-tap-popup--afd .afd-links {
    padding: 0.6rem 0.75rem env(safe-area-inset-bottom);
    margin-top: auto;
    font-size: 0.9rem;
  }
  .hf-tap-popup--afd .afd-link {
    padding: 0.4rem 0;   /* easier tap target */
  }
  .hf-tap-popup--afd .afd-close-btn { display: inline-flex; }
}

/* Tiny glyph indicators on the left edge of each forecast row. Cloud sits
   in the sky ribbon (with a bg-card-2 chip so the contrast stays consistent
   regardless of the sky-cell opacity behind it); raindrop sits at the
   bottom of the chart on the precip baseline. pointer-events: none so they
   don't block hover/tooltips on adjacent cells. */
.hf-row-icon {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  color: var(--fg-faint);
  z-index: 2;
}
.hf-row-icon svg { display: block; }

/* All three glyphs (24hr cloud, 24hr rain, 7-day rain) share the same chip
   geometry so their left edges align cleanly across the two panels. The chip
   sits flush at left:0 — no sliver of the underlying ribbon/strip shows
   beside the icon, and the chip's bg-card-2 background guarantees the
   silhouette stays legible regardless of what's drawn behind. */
.hf-row-icon-sky,
.hf-row-icon-rain {
  left: 0;
  width: 16px;
  background: var(--bg-card-2);
  /* Square left corners flush with the panel edge so no sliver of the
     underlying strip shows through a rounded corner; right side stays
     rounded so the chip blends gracefully into the bar/cell area. */
  border-radius: 0 3px 3px 0;
}
/* Cloud chip: full ribbon height. Stretching top:0/bottom:0 means the chip
   matches whatever the ribbon height is (9px normal, 14px wall) without
   leaving a sliver of cloud-cover cell poking above or below. */
.hf-row-icon-sky {
  top: 0;
  bottom: 0;
}
.hf-row-icon-sky svg { width: 12px; height: 7.5px; }

.hf-row-icon-rain {
  bottom: 3px;
  height: 9px;
}
.hf-row-icon-rain svg { width: 7px; height: 9px; }
.hf-chart-svg {
  width: 100%;
  display: block;
  height: 40px;    /* compact: just precip bars */
}
.hf-col-rule  { stroke: var(--border); stroke-width: 1; opacity: 0.55; vector-effect: non-scaling-stroke; }
.hf-baseline  { stroke: var(--border); stroke-width: 1; opacity: 0.5; vector-effect: non-scaling-stroke; }
/* .hf-precip-bar gets its color from .mg-pr-N classes below — same 4-stop
   accent ramp used by the 7-day intra-day strip for visual consistency. */

/* Precip color ramp shared by the hourly-forecast bars and the 7-day
   intra-day strips. Uses fill + opacity instead of color-mix(), since
   color-mix is Chrome 111+/Safari 16.2+ and many embedded browsers
   (smart-TV WebKit, older Android WebView) silently drop the declaration
   and default SVG fill to black — making bars invisible on dark bg.
   Opacity floors stay above 25% because TV gamma (BT.1886 ~2.4) crushes
   low-opacity tones into the bg even when they parse correctly. */
.mg-pr-1        { fill: var(--accent); opacity: 0.4; }
.mg-pr-2        { fill: var(--accent); opacity: 0.65; }
.mg-pr-3        { fill: var(--accent); opacity: 0.9; }
.mg-pr-4        { fill: var(--accent); opacity: 1; }

/* 7-column day grid below the ribbon. CSS Grid with 7 equal columns; each
   cell stacks day name → icon → H/L → precip% bar vertically. */
.forecast-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
}
/* Wall mode has room for a 10-day horizon; setForecast slices accordingly
   and the grid widens to match. Days past NWS's reach come from Open-Meteo
   and render as the hatched aggregate fallback (see .fg-strip-bar-aggregate). */
:root[data-display="wall"] .forecast-grid {
  grid-template-columns: repeat(10, 1fr);
}
.fg-cell {
  display: grid;
  grid-template-rows: auto auto auto auto auto;
  align-items: center;
  justify-items: center;
  gap: 4px;
  padding: 0.6rem 0.4rem 0.5rem;
  background: var(--bg-card-2);
  border-radius: var(--radius-sm);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.fg-day  { color: var(--fg-muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
.fg-icon {
  width: 2.375rem; height: 2.375rem;
  background-color: var(--icon-color);
  -webkit-mask: var(--ic) center / contain no-repeat;
          mask: var(--ic) center / contain no-repeat;
}
.fg-hi   { font-family: var(--font-num); font-weight: 600; font-size: 1.1rem; }
.fg-lo   { font-family: var(--font-num); color: var(--fg-muted); font-size: 0.9rem; }

/* Intra-day precip strip: 12 bars on desktop (2hr each), 2 bars on mobile
   (am / pm). Bar height = max precip% in that block, fill color = same
   4-stop intensity ramp the meteogram uses. The strip replaces the old
   single-percentage bar — same encoding (likelihood) plus arrival time and
   intensity for free. */
.fg-strip {
  width: 100%;
  /* viewBox is 30 high (21 bar area + 9 below baseline for the noon tick).
     28px rendered keeps bars at the same visual size as the prior 22px /
     viewBox-24 layout; the extra 6px renders the room for the noon tick. */
  height: 28px;
  display: block;
}
.fg-strip-svg {
  width: 100%; height: 100%; display: block;
}
.fg-strip-base { stroke: var(--border); stroke-width: 1; opacity: 0.6; vector-effect: non-scaling-stroke; }
/* 9a/12p/3p/6p reference ticks — thin vertical lines below the baseline,
   roughly the height of the row icons (~9 viewBox units below baseline). */
.fg-strip-noon { stroke: var(--fg-faint); stroke-width: 1; opacity: 0.55; vector-effect: non-scaling-stroke; }
.fg-strip-bar  { /* color from .mg-pr-N — already defined above */ }
/* Aggregate bars: drawn when we only have a daily probability (typically
   day 7, sourced from Open-Meteo's daily-only forecast). The hatch fill
   signals "averaged, timing unknown" without implying hourly resolution
   we don't have. */
.fg-strip-bar-aggregate { fill: url(#fg-strip-hatch); opacity: 0.85; }
.fg-strip-hatch-line { stroke: var(--accent); stroke-width: 2; vector-effect: non-scaling-stroke; }

/* 7-day section: anchor for the absolute raindrop indicator that aligns
   horizontally with the 24hr panel's icons (both at left:0 of their panel).
   Vertical position is mode-specific because cell padding + strip height
   differ between desktop (8+11), mobile (6+8), and wall (8+14). */
.wx-week-block { position: relative; }
.fg-row-icon {
  position: absolute;
  left: 0;
  bottom: 17px;     /* desktop: strip 28px tall, padding-bottom 8px → center 22, icon (9px) bottom = 22 - 4.5 ≈ 17 */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 9px;
  background: var(--bg-card-2);
  border-radius: 0 3px 3px 0;
  color: var(--fg-faint);
  pointer-events: none;
  z-index: 2;
}
.fg-row-icon svg { width: 7px; height: 9px; display: block; }

/* Phones: compact the 7-column grid (still 7 wide) + tighten meteogram. */
@media (max-width: 720px) {
  .fg-cell    { padding: 0.45rem 0.2rem 0.4rem; gap: 3px; }
  .fg-icon    { width: 1.875rem; height: 1.875rem; }
  .fg-hi      { font-size: 0.95rem; }
  .fg-lo      { font-size: 0.78rem; }
  .fg-day     { font-size: 0.68rem; }
  .fg-strip   { height: 20px; }
  /* On phones the 24hr panel drops to 4 cells × 6hr (handled in JS) so
     each cell + chart bar has room to breathe. */
  .hf-cell-icon { width: 1.625rem; height: 1.625rem; }
  .hf-cell-temp { font-size: 0.95rem; }
  .hf-cell-hour { font-size: 0.7rem; }
  .hf-chart-svg { height: 36px; }
  /* Mobile cell padding-bottom is ~6.4px and strip is 16px, so center is
     ~14.4px above wx-week-block bottom — match with bottom:10px. */
  .fg-row-icon { bottom: 12px; }   /* mobile: strip 20 + padding 6.4 → center 16.4 */
}

/* User-selectable text size. Scales the root font-size so every rem-based
   value (including the clamp() expressions for the big temp and clock) grows
   proportionally. Browsers like Vanadium that don't honour system text
   scaling otherwise leave the dashboard slightly small for kitchen-counter
   reading distance — this gives the user a knob without affecting layout
   structure. The container query on .current-block remains the safety net
   for very large scales.

   The --map-btn-* + --plane-marker-scale variables ride along so the radar
   map's overlay controls (info/warnings/mode/planes buttons) and the plane
   marker icons grow alongside the text. */
:root {
  --map-btn-size: 1.7rem;
  --map-btn-gap:  0.3rem;
  --plane-marker-scale: 1;
}
:root[data-text-size="large"]  {
  font-size: 18px;
  --map-btn-size: 1.9rem;
  --map-btn-gap:  0.35rem;
  --plane-marker-scale: 1.15;
}
:root[data-text-size="xlarge"] {
  font-size: 20px;
  --map-btn-size: 2.2rem;
  --map-btn-gap:  0.4rem;
  --plane-marker-scale: 1.35;
}

/* On phone-width viewports, tighten the gap between the radar map's
   overlay buttons. Five buttons + the time-badge play/pause pill share
   the bottom edge of the map; at the default 0.3rem gap they can crowd
   the badge on narrow screens. Button size itself is preserved so tap
   targets don't shrink. */
@media (max-width: 720px) {
  :root,
  :root[data-text-size="large"],
  :root[data-text-size="xlarge"] {
    --map-btn-gap: 0.15rem;
  }
}

/* No-script warning */
.noscript-warn {
  margin: var(--gap); padding: 1rem; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
}

/* ===== Settings drawer ===================================================
   Triggered by the gear icon in the topbar. A small absolutely-positioned
   panel; on phones it widens to fill the viewport (minus a margin). Persists
   user choices for plane card visibility + radar auto-expand to localStorage.
*/
.settings-btn .settings-icon {
  display: inline-block;
  width: 18px; height: 18px;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M19.14,12.94a7.49,7.49,0,0,0,.05-.94,7.49,7.49,0,0,0-.05-.94l2.11-1.65a.5.5,0,0,0,.12-.64l-2-3.46a.5.5,0,0,0-.61-.22l-2.49,1a7.32,7.32,0,0,0-1.63-.94L14.4,2.42a.5.5,0,0,0-.5-.42H9.9a.5.5,0,0,0-.5.42L9.07,5.14A7.32,7.32,0,0,0,7.44,6.08l-2.49-1a.5.5,0,0,0-.61.22l-2,3.46a.5.5,0,0,0,.12.64L4.57,11.06a7.49,7.49,0,0,0,0,1.88L2.46,14.59a.5.5,0,0,0-.12.64l2,3.46a.5.5,0,0,0,.61.22l2.49-1a7.32,7.32,0,0,0,1.63.94l.33,2.72a.5.5,0,0,0,.5.42h3.9a.5.5,0,0,0,.5-.42l.33-2.72a7.32,7.32,0,0,0,1.63-.94l2.49,1a.5.5,0,0,0,.61-.22l2-3.46a.5.5,0,0,0-.12-.64ZM12,15.5A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M19.14,12.94a7.49,7.49,0,0,0,.05-.94,7.49,7.49,0,0,0-.05-.94l2.11-1.65a.5.5,0,0,0,.12-.64l-2-3.46a.5.5,0,0,0-.61-.22l-2.49,1a7.32,7.32,0,0,0-1.63-.94L14.4,2.42a.5.5,0,0,0-.5-.42H9.9a.5.5,0,0,0-.5.42L9.07,5.14A7.32,7.32,0,0,0,7.44,6.08l-2.49-1a.5.5,0,0,0-.61.22l-2,3.46a.5.5,0,0,0,.12.64L4.57,11.06a7.49,7.49,0,0,0,0,1.88L2.46,14.59a.5.5,0,0,0-.12.64l2,3.46a.5.5,0,0,0,.61.22l2.49-1a7.32,7.32,0,0,0,1.63.94l.33,2.72a.5.5,0,0,0,.5.42h3.9a.5.5,0,0,0,.5-.42l.33-2.72a7.32,7.32,0,0,0,1.63-.94l2.49,1a.5.5,0,0,0,.61-.22l2-3.46a.5.5,0,0,0-.12-.64ZM12,15.5A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z'/></svg>") center / contain no-repeat;
}
.settings-drawer {
  position: fixed;
  top: 3.4rem;
  right: 0.6rem;
  width: min(22rem, calc(100vw - 1.2rem));
  z-index: 50;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}
.settings-drawer[hidden] { display: none; }
.settings-h {
  margin: 0;
  font-size: 1rem;
  color: var(--fg);
}
.settings-group {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.75rem 0.65rem;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.settings-group legend {
  padding: 0 0.35rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--fg-muted);
}
.settings-group label {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-size: 0.92rem;
  color: var(--fg);
  cursor: pointer;
}
.settings-group input { cursor: pointer; }
/* The "Collapsed" aircraft mode only makes sense on phones; on desktop the
   card sits in its own grid cell, so collapsing just leaves a hole. Hide the
   option on wider viewports — JS also self-corrects a stale 'collapsed'
   choice when it can't apply. */
@media (min-width: 721px) {
  .settings-group label.mobile-only { display: none; }
}
.settings-select-row {
  justify-content: space-between;
}
.settings-select-row select {
  padding: 0.25rem 0.4rem;
  background: var(--bg-card-2);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
  cursor: pointer;
}

/* Frame-time badge layered on the radar map during animation playback.
   Clickable: tap to expand a scrub-slider panel anchored just above it. */
.radar-time-badge {
  position: absolute;
  bottom: 0.5rem;
  left: 0.5rem;
  z-index: 500;
  padding: 0.25rem 0.55rem;
  background: color-mix(in srgb, var(--bg-card) 88%, transparent);
  color: var(--fg);
  font-family: var(--font-num);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  opacity: 0.7;
  transition: opacity 0.25s ease;
}
/* Once the first WMS tile-load has settled (or the safety timeout has
   fired) JS adds `.loaded`, snapping the badge to full opacity and
   prefixing a ▶ play-affordance glyph. The transition from translucent
   "Loading…" → opaque "▶ NOW · 11:35 AM" is the signal that the radar
   layer has arrived even when the map shows no precipitation. */
.radar-time-badge.loaded {
  opacity: 1;
}
/* Play / pause glyph rendered as a CSS mask over `currentColor` so it
   always picks up the theme's foreground colour and never falls into the
   browser's emoji-glyph path. Unicode codepoints (U+25B6, U+23F8) have
   colour-emoji presentations on iOS / Android that the FE0E variation
   selector + font-variant-emoji didn't reliably suppress, so we draw
   the shapes ourselves. Same `mask` pattern used elsewhere in this file
   for the gear / display-mode icons. */
.radar-time-badge.loaded::before {
  content: '';
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-right: 0.35em;
  vertical-align: -0.04em;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polygon points='6,4 20,12 6,20'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polygon points='6,4 20,12 6,20'/></svg>") center / contain no-repeat;
}
/* While the loop is animating, swap the ▶ play triangle for a ⏸ pause
   pair-of-bars. The badge is now the single play/pause control — no
   separate scrub panel. */
.radar-time-badge.loaded.playing::before {
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect x='6' y='4' width='4' height='16'/><rect x='14' y='4' width='4' height='16'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><rect x='6' y='4' width='4' height='16'/><rect x='14' y='4' width='4' height='16'/></svg>") center / contain no-repeat;
}
.radar-map { position: relative; }
.settings-footer { display: flex; justify-content: space-between; gap: 0.5rem; }
.settings-reset {
  padding: 0.4rem 0.9rem;
  font-size: 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--fg-muted);
  cursor: pointer;
}
.settings-reset:hover { color: var(--fg); border-color: var(--fg-muted); }
.settings-close {
  padding: 0.4rem 0.9rem;
  font-size: 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-card-2);
  color: var(--fg);
  cursor: pointer;
}
.settings-close:hover { filter: brightness(1.08); }
:root[data-display="wall"] .settings-btn { display: none; }

/* ===== Aircraft card header + collapse chevron ============================
   Chevron + collapse behavior are mobile-only. On desktop the grid cell stays
   allocated regardless, so collapsing the body just leaves an empty card. */
.aircraft-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.aircraft-card-head h2 { margin: 0; }
@media (min-width: 721px) {
  .aircraft-collapse-btn { display: none; }
}
/* Hide the collapse chevron when the card is in "bottom" mode: it's
   already out of the way at the bottom of the page, and clicking the
   chevron would set mode to "collapsed" — which loses the bottom
   positioning and reverts the card to its original slot below the
   current conditions. */
:root[data-aircraft="bottom"] .aircraft-collapse-btn { display: none; }
.aircraft-collapse-btn {
  background: transparent;
  border: none;
  color: var(--fg-muted);
  cursor: pointer;
  padding: 0.25rem;
  border-radius: var(--radius-sm);
}
.aircraft-collapse-btn:hover { color: var(--fg); }
.aircraft-collapse-btn .chev {
  display: inline-block;
  width: 0; height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 7px solid currentColor;
  transition: transform 0.15s ease;
}
@media (prefers-reduced-motion: reduce) {
  .aircraft-collapse-btn .chev { transition: none; }
}

/* ===== Radar toggle button (in current card, under wind compass) ========== */
.radar-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.35rem 0.7rem;
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--fg);
  background: var(--bg-card-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  justify-self: end;   /* anchor to the right edge of its grid column */
}
.radar-toggle-btn:hover { filter: brightness(1.08); }
.radar-toggle-btn[aria-expanded="true"] {
  background: color-mix(in srgb, var(--accent) 18%, var(--bg-card-2));
  border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
}
.radar-icon {
  display: inline-block; width: 14px; height: 14px;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 2 L12 22 M2 12 L22 12' stroke='black' stroke-width='1.5' fill='none'/><circle cx='12' cy='12' r='3' fill='black'/><circle cx='12' cy='12' r='7' stroke='black' stroke-width='1.2' fill='none'/><circle cx='12' cy='12' r='10' stroke='black' stroke-width='1' fill='none'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 2 L12 22 M2 12 L22 12' stroke='black' stroke-width='1.5' fill='none'/><circle cx='12' cy='12' r='3' fill='black'/><circle cx='12' cy='12' r='7' stroke='black' stroke-width='1.2' fill='none'/><circle cx='12' cy='12' r='10' stroke='black' stroke-width='1' fill='none'/></svg>") center / contain no-repeat;
}
@media (max-width: 720px) {
  .radar-toggle-btn { padding: 0.3rem 0.55rem; font-size: 0.75rem; }
}

/* ===== Radar card =========================================================
   Hidden by default. When visible, sits full-width between the current/aircraft
   row and the forecast card. On mobile (≤720px) the body becomes a fixed
   overlay covering the screen, giving touch users the screen real-estate that
   a 250px inline map can't.
*/
.card.radar {
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.radar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.radar-head h2 { margin: 0; font-size: 1rem; }
.radar-actions { display: flex; gap: 0.35rem; }
.radar-fullscreen-btn,
.radar-close-btn {
  background: var(--bg-card-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--fg);
  padding: 0.25rem 0.5rem;
  cursor: pointer;
  font-size: 0.9rem;
  display: inline-flex; align-items: center; justify-content: center;
}
.radar-fullscreen-btn:hover,
.radar-close-btn:hover { filter: brightness(1.08); }
.close-icon { font-size: 1.1rem; line-height: 1; }
.fullscreen-icon {
  display: inline-block;
  width: 14px; height: 14px;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5' stroke='black' stroke-width='2' fill='none' stroke-linecap='round'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5' stroke='black' stroke-width='2' fill='none' stroke-linecap='round'/></svg>") center / contain no-repeat;
}
.radar-map {
  width: 100%;
  /* Default height matches the current/aircraft row's height once JS syncs.
     Until that runs (e.g. very first paint, or radar opened before the top
     row settles), 380px is a reasonable inline default. */
  height: 380px;
  border-radius: var(--radius-sm);
  background: var(--bg-card-2);
  overflow: hidden;
}
/* Wall mode: larger map height to use the available vertical room. */
:root[data-display="wall"] .radar-map { height: 50vh; min-height: 380px; }

/* Fullscreen overlay mode (mobile default; opt-in via button on larger screens) */
.card.radar.radar-fullscreen {
  position: fixed;
  inset: 0;
  z-index: 60;
  margin: 0;
  border-radius: 0;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
  background: var(--bg);
}
.card.radar.radar-fullscreen .radar-map {
  flex: 1;
  height: auto;
  min-height: 0;
  border-radius: 0;
}
.card.radar.radar-fullscreen .radar-head { padding: 0.75rem; }

@media (max-width: 720px) {
  /* On phones the inline radar is too cramped to be useful: always promote
     to fullscreen when the user opens it. */
  .card.radar:not([hidden]) {
    position: fixed;
    inset: 0;
    z-index: 60;
    margin: 0;
    border-radius: 0;
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
             env(safe-area-inset-bottom) env(safe-area-inset-left);
    background: var(--bg);
  }
  .card.radar:not([hidden]) .radar-map {
    flex: 1;
    height: auto;
    min-height: 0;
    border-radius: 0;
  }
  .card.radar:not([hidden]) .radar-fullscreen-btn { display: none; }
  .card.radar:not([hidden]) .radar-head { padding-inline: 0.75rem; }
  /* Roomier close target on phones — the desktop size is fine with a mouse
     but cramped under a thumb. ~25% bigger in each axis. */
  .card.radar:not([hidden]) .radar-close-btn {
    padding: 0.45rem 0.75rem;
    min-width: 2.5rem;
    min-height: 2.5rem;
  }
  .card.radar:not([hidden]) .radar-close-btn .close-icon { font-size: 1.4rem; }
}

/* Small overlay controls anchored to the bottom-right of .radar-map:
   the warnings-toggle button (⚠) on the left, the info button (i) on the
   right. Both float above Leaflet's tile/overlay panes but stay clear of
   the radar time-stamp badge in the bottom-left. */
.radar-info-btn,
.radar-warnings-btn,
.radar-mode-btn,
.radar-planes-btn,
.radar-layer-btn {
  position: absolute;
  bottom: 0.5rem;
  z-index: 500;
  width: var(--map-btn-size);
  height: var(--map-btn-size);
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg-card) 88%, transparent);
  color: var(--fg);
  border: 1px solid var(--border);
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  line-height: 1;
}
/* Button positions stack from the right edge, spaced by one button-width
   + one gap each. Driven by --map-btn-size / --map-btn-gap so they stay
   correct at any text-size setting. */
.radar-info-btn     { right: 0.5rem; font: italic 600 0.95rem/1 serif; }
.radar-warnings-btn { right: calc(0.5rem + 1 * (var(--map-btn-size) + var(--map-btn-gap))); font-size: 0.95rem; }
.radar-planes-btn   { right: calc(0.5rem + 2 * (var(--map-btn-size) + var(--map-btn-gap))); font-size: 0.95rem; }
.radar-mode-btn     { right: calc(0.5rem + 3 * (var(--map-btn-size) + var(--map-btn-gap))); font-weight: 700; font-size: 0.78rem; letter-spacing: 0.02em; }
.radar-layer-btn    { right: calc(0.5rem + 4 * (var(--map-btn-size) + var(--map-btn-gap))); }
.radar-layer-btn[aria-pressed="true"] {
  /* Layer visible (the default): accent-tinted background mirrors the
     planes-toggle's active state so "layer on" reads at a glance. */
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-card));
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  color: var(--accent);
}
.radar-layer-btn[aria-pressed="false"] { opacity: 0.55; }
.radar-planes-btn[aria-pressed="true"] {
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-card));
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  color: var(--accent);
}
.radar-planes-btn[aria-pressed="false"] { opacity: 0.55; }
.radar-mode-btn[aria-pressed="true"] {
  /* Velocity mode = highlighted so it's visually distinct from reflectivity. */
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-card));
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  color: var(--accent);
}
.radar-warnings-btn[aria-pressed="true"] {
  /* Active state: red tint signals "warnings are being drawn". */
  background: color-mix(in srgb, var(--bad) 22%, var(--bg-card));
  border-color: color-mix(in srgb, var(--bad) 50%, var(--border));
  color: var(--bad);
}
.radar-warnings-btn[aria-pressed="false"] {
  /* Off state: muted, with a strike-through-ish opacity hint. */
  opacity: 0.55;
}
.radar-info-btn:hover,
.radar-warnings-btn:hover,
.radar-mode-btn:hover,
.radar-planes-btn:hover,
.radar-layer-btn:hover { filter: brightness(1.1); }

/* "You are here" marker. The dot picks up a subtle dark drop-shadow so
   it reads as an elevated UI element rather than another flat overlay
   shape, matching the soft elevation the dashboard cards use. The ring
   gets no shadow — it's intentionally faint and atmospheric, hinting at
   "approximate area" rather than competing with the dot for attention. */
.radar-home-dot {
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.55));
}

/* Plane markers. The close plane (one that matches the close-plane card)
   shows in accent; backdrop planes are dimmed so the eye lands on the one
   that matters. Inline SVG inside a divIcon — pointer-events stay enabled
   so the popup still opens on click. */
.plane-marker {
  color: var(--fg-muted);
  opacity: 0.55;
  display: block;
}
.plane-marker svg { width: 100%; height: 100%; display: block; }
.plane-marker-close {
  color: var(--accent);
  opacity: 1;
  filter: drop-shadow(0 0 2px color-mix(in srgb, var(--accent) 50%, transparent));
}
.radar-info-popup {
  position: absolute;
  right: 0.5rem;
  bottom: 2.5rem;                /* sit above the button */
  z-index: 501;
  max-width: min(20rem, calc(100% - 1rem));
  padding: 0.6rem 0.75rem;
  background: var(--bg-card);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
  font-size: 0.78rem;
  line-height: 1.4;
}
.radar-info-popup[hidden] { display: none; }
.radar-info-popup p { margin: 0.15rem 0; }
.radar-info-popup a { color: var(--fg); text-decoration: underline; }

/* Aircraft photo attribution overlay — clicking the photo toggles a small
   panel pinned to the bottom of the figure with the photographer credit +
   Planespotters link. The position:relative is now on .aircraft-photo
   itself (above), so the popup anchors there. */
.aircraft-photo-popup {
  position: absolute;
  left: 0.5rem;
  right: 0.5rem;
  bottom: 0.5rem;
  padding: 0.45rem 0.65rem;
  background: color-mix(in srgb, var(--bg-card) 92%, transparent);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.78rem;
  line-height: 1.35;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}
.aircraft-photo-popup[hidden] { display: none; }
.aircraft-photo-popup a { color: var(--fg); text-decoration: underline; }
/* Extra-detail list inside the popup. Two-column rows: label / value.
   The label column is narrow and right-aligned for a clean key:value
   read. Stays compact — the popup overlays the photo, so vertical
   density matters. */
.aircraft-photo-details {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 0.6rem;
  row-gap: 0.15rem;
  margin: 0 0 0.4rem;
  font-size: 0.78rem;
}
.aircraft-photo-details > div { display: contents; }
.aircraft-photo-details dt { color: var(--fg-muted); text-align: right; }
.aircraft-photo-details dd { margin: 0; color: var(--fg); }
.aircraft-photo-credit { font-size: 0.78rem; color: var(--fg-muted); }
.aircraft-photo-credit a { color: inherit; }

/* "Generic example" pill — shown when the photo is a Wikipedia
   type-fallback rather than the actual airframe. Centered horizontally
   along the bottom of the figure, just above the image's lower edge.
   The .aircraft-photo selector matches the figure (which already has
   position: relative); the is-generic modifier is added by
   setAircraft when photo.generic is truthy. */
.aircraft-photo.is-generic::after {
  content: "Generic example";
  position: absolute;
  bottom: 2px;
  left: 50%;
  transform: translateX(-50%);
  padding: 2px 8px;
  background: color-mix(in srgb, var(--bg-card) 80%, transparent);
  color: var(--fg-muted);
  border: 1px solid var(--border);
  border-radius: 3px;
  font-size: 0.7rem;
  line-height: 1.2;
  letter-spacing: 0.02em;
  pointer-events: none;
  white-space: nowrap;
  /* Ensure the badge sits above the image and any popup positioned
     within the same figure. */
  z-index: 2;
}

/* Leaflet container theming. The default attribution control is disabled
   at the map level — we render our own info-button + popup instead. */
.leaflet-container { font-family: inherit; background: var(--bg-card-2); }
.leaflet-bar a {
  background: var(--bg-card) !important;
  color: var(--fg) !important;
  border-bottom-color: var(--border) !important;
}
.leaflet-bar a:hover { background: var(--bg-card-2) !important; }

/* ===== History card =====================================================
   Triggered by the "more history →" link in the obs-table label row.
   Hidden by default. On desktop renders inline as a card in the grid; on
   phones (or by user request via the fullscreen button) renders as a
   full-viewport overlay. Mirrors the radar card pattern. */

/* The trigger lives in the obs-table's row-labels first <td>. Match the
   surrounding label styling so it reads as part of the table chrome
   rather than a separate UI element. */
.history-trigger {
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-faint);
  cursor: pointer;
  white-space: nowrap;
}
.history-trigger:hover { color: var(--fg-muted); }
.history-trigger:focus-visible {
  outline: 1px solid var(--fg-faint);
  outline-offset: 2px;
  border-radius: 2px;
}
/* Two-span label so the trigger can shrink from "history →" to "hist →"
   on phone widths, freeing up the column for the obs-table data cells
   to its right. Desktop + wall both stay on the full word. */
.hist-label-short { display: none; }
@media (max-width: 540px) {
  .history-trigger { font-size: 0.6rem; }
  .hist-label-full  { display: none; }
  .hist-label-short { display: inline; }
}

.card.history {
  padding: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.history-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.history-head h2 { margin: 0; font-size: 1rem; }
.history-actions { display: flex; gap: 0.35rem; }
.history-fullscreen-btn,
.history-close-btn {
  background: var(--bg-card-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--fg);
  padding: 0.25rem 0.5rem;
  cursor: pointer;
  font-size: 0.9rem;
  display: inline-flex; align-items: center; justify-content: center;
}
.history-fullscreen-btn:hover,
.history-close-btn:hover { filter: brightness(1.08); }

.history-body {
  display: grid;
  gap: 1rem;
}
@media (min-width: 720px) {
  .history-body { grid-template-columns: 1fr 1fr; }
}
@media (min-width: 1100px) {
  .history-body { grid-template-columns: repeat(4, 1fr); }
}
.history-loading {
  margin: 0;
  color: var(--fg-muted);
  font-size: 0.9rem;
}

.hist-section {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.75rem;
  background: var(--bg-card-2);
}
.hist-section h3 {
  margin: 0 0 0.5rem;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* Simple key/value row — used for streaks, counts, and entries without a
   date. Two-column grid with the value right-aligned. */
.hist-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.5rem;
  align-items: baseline;
  padding: 0.4rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.hist-row:last-child { border-bottom: 0; }
.hist-k {
  font-size: 0.78rem;
  color: var(--fg-muted);
  min-width: 0;
  overflow-wrap: break-word; /* labels wrap rather than push the table wide */
}
.hist-v {
  text-align: right;
  min-width: 0;
}
.hist-num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--fg);
}
.hist-num-soft { color: var(--fg-muted); font-weight: 500; }
.hist-unit {
  font-family: var(--font-base, inherit);
  font-size: 0.78rem;
  color: var(--fg-muted);
  font-weight: 400;
}
.hist-when {
  display: block;     /* breaks below the value when used inside .hist-v */
  font-family: var(--font-base, inherit);
  font-size: 0.72rem;
  color: var(--fg-faint);
  font-weight: 400;
  margin-top: 0.1rem;
}
.hist-sub {
  font-family: var(--font-base, inherit);
  font-size: 0.7rem;
  color: var(--fg-faint);
  font-weight: 400;
  margin-top: 0.05rem;
}

/* Window block — labeled time period containing one or more stat tiles.
   Used for rain windows (single tile), wind windows (single tile), and
   temperature windows (twin hi/lo tiles). Each window is its own visual
   block within the section, separated by hairlines. */
.hist-window {
  padding: 0.55rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.hist-window:last-child { border-bottom: 0; }
.hist-window-label {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--fg-faint);
  margin-bottom: 0.3rem;
}
.hist-window-yoy {
  margin-top: 0.3rem;
  font-size: 0.7rem;
  color: var(--fg-faint);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Stat tile — value on top, date caption below. Arrow optional (used
   for high/low coding in the temp pair). */
.hist-stat {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 0.4rem;
  align-items: baseline;
  min-width: 0;
}
.hist-stat-arrow {
  grid-row: 1 / 2;
  grid-column: 1 / 2;
  font-size: 0.85rem;
  line-height: 1;
  color: var(--fg-faint);
}
.hist-stat-high .hist-stat-arrow { color: #d96149; }
.hist-stat-low  .hist-stat-arrow { color: #4a8acc; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    .hist-stat-high .hist-stat-arrow { color: #f08469; }
    .hist-stat-low  .hist-stat-arrow { color: #6daee0; }
  }
}
:root[data-theme="dark"] {
  .hist-stat-high .hist-stat-arrow { color: #f08469; }
  .hist-stat-low  .hist-stat-arrow { color: #6daee0; }
}
.hist-stat-num {
  grid-row: 1 / 2;
  grid-column: 2 / 3;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--fg);
  line-height: 1.1;
}
.hist-stat-when {
  grid-row: 2 / 3;
  grid-column: 2 / 3;
  font-family: var(--font-base, inherit);
  font-size: 0.72rem;
  color: var(--fg-faint);
  font-weight: 400;
  margin-top: 0.15rem;
}
/* Plain (single-tile) stats inside a window — wind, rain — left-align
   so the tile reads as a row rather than competing with the right-edge
   of the card. */
.hist-stat-plain {
  grid-template-columns: auto;
}
.hist-stat-plain .hist-stat-num,
.hist-stat-plain .hist-stat-when { grid-column: 1 / -1; }

/* Twin hi/lo tile pair — equal-width columns. */
.hist-tile-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.6rem;
}

.hist-foot {
  margin: 0;
  font-size: 0.75rem;
  color: var(--fg-faint);
  text-align: center;
}

/* Fullscreen overlay (mobile default; opt-in elsewhere via the button) */
.card.history.history-fullscreen {
  position: fixed;
  inset: 0;
  z-index: 60;
  margin: 0;
  border-radius: 0;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
  background: var(--bg);
  overflow-y: auto;
}
.card.history.history-fullscreen .history-head { padding: 0.75rem; }
.card.history.history-fullscreen .history-body { padding: 0 0.75rem 1rem; }

@media (max-width: 720px) {
  /* On phones the inline card is awkward — promote to fullscreen. */
  .card.history:not([hidden]) {
    position: fixed;
    inset: 0;
    z-index: 60;
    margin: 0;
    border-radius: 0;
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
             env(safe-area-inset-bottom) env(safe-area-inset-left);
    background: var(--bg);
    overflow-y: auto;
  }
  .card.history:not([hidden]) .history-head { padding: 0.75rem; }
  .card.history:not([hidden]) .history-body { padding: 0 0.75rem 1rem; }
  .card.history:not([hidden]) .history-fullscreen-btn { display: none; }
  .card.history:not([hidden]) .history-close-btn {
    padding: 0.45rem 0.75rem;
    min-width: 2.5rem;
    min-height: 2.5rem;
  }
  .card.history:not([hidden]) .history-close-btn .close-icon { font-size: 1.4rem; }
}

