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

:root {
  --bg: #f8fafc;
  --surface: #ffffff;
  --border: #e2e8f0;
  --text: #0f172a;
  --text-muted: #64748b;
  --accent: #0f766e;
  --accent-dim: #0d9488;
  --now-dot: #0ea5e9;
  --rain: #0284c7;
  --warn: #eab308;
  /* Moon marker/fill color. Same value as --text-muted in light mode
     (a paler silver has ~1.2:1 contrast against the white tile — invisible),
     but dark mode gets a genuinely lighter slate than --text-muted's own
     dark value, since a dark surface has plenty of contrast budget to spare
     for a proper "silvery" look there. See [data-theme="dark"] override. */
  --moon-glow: #64748b;
  /* .moon-marker's lit/dark split. First attempt paired --moon-glow with a
     fixed near-black for "dark" in both themes — on real screen testing (not
     just the ΔE math, which said it was fine) that read as murky, two grays
     both reading as "dark" with nothing bright to anchor the eye — the same
     low-chroma "both sides look gray" problem AWN's own reference icon
     avoids by pairing white with a plain mid-gray instead of black. So each
     theme now gets its own genuinely-contrasting pair, rather than reusing
     --moon-glow as a fixed anchor on the same side in both themes: light
     mode's "dark" side is exactly --moon-glow's *light-mode* value (so it
     still matches the moon-fill/glow-ring color the marker already sits
     against on the bar), paired with white for "lit". Dark mode flips which
     extreme reuses --moon-glow — its dark-mode value is already pale enough
     to read as the bright side there — and pairs it with a true near-black
     for "dark", which the ΔE math (~77) said was fine and, unlike the light
     pairing, held up on screen. Validated with the dataviz skill's
     validate_palette.js — see [[project_dashboard_sun_moon]]. */
  --moon-lit: #ffffff;
  --moon-dark: #64748b;
  --radius: 4px;
  --nav-h: 52px;
  --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.07), 0 1px 2px -1px rgb(0 0 0 / 0.06);
  --chart-grid: #e2e8f0;
  --chart-tick: #94a3b8;
  --chart-tooltip-bg: #0f172a;
  --chart-tooltip-text: #f1f5f9;
  --chart-temp: #dc2626;
  --chart-dewpoint: #16a34a;
  --chart-wind: #1e40af;
  --chart-humidity: #0f766e;
  --chart-uv: #6d28d9;
  --chart-pressure: #334155;
  --chart-solar: #eab308;
  --chart-rain: #0ea5e9;
  /* Meteogram's cloud-cover strip — a warm gray distinct from --chart-pressure's
     cool slate (both would otherwise collide when Stage 2 makes pressure visible
     on the same page). No existing token was free for this without an actual
     color clash between two different metrics. */
  --chart-cloud: #78716c;
  /* History page's 90-day/1-year wind rose — sequential ramp for the 5
     non-calm Beaufort speed bins. Light->dark = low->high speed,
     --windrose-4 deliberately equal to --chart-wind so the ramp matches the
     wind speed chart's own line color at the "Moderate Breeze" step, with
     --windrose-5 one step further/darker for "Fresh+" (rarely populated at
     this station, but a real 5th bin, not a repeat of --windrose-4).
     Lightness-monotonicity checked via the dataviz skill's palette
     validator (the categorical checks intentionally FAIL on a sequential
     ramp like this one — expected, not a real failure, see the skill docs). */
  --windrose-1: #bfdbfe;
  --windrose-2: #93c5fd;
  --windrose-3: #3b82f6;
  --windrose-4: #1e40af;
  --windrose-5: #172554;
  --windrose-calm: #cbd5e1;
  /* The Sun tile's own color — shared by the position bar/marker (.sun-fill/.sun-marker,
     below) and the day/night pie's day wedge, so both Sun-tile elements read as one
     identity. Briefly set equal to --status-amber (2026-07-08, on the theory that "the sun
     obviously isn't a severity indicator, nobody confuses the two"), reverted the same day
     on reflection — the sun sits in the same Almanac grid as the UV pyramid's own
     Caution-colored band, and the whole point of the severity-color redesign was one hue
     meaning exactly one thing app-wide. This value is deliberately NOT --chart-solar (which
     now only drives the History Solar chart, independent of the Sun tile) and NOT
     --status-amber — its own token, its own color. */
  --sun-day: #f59e0b;
  /* Shared status-classification colors — UV level, Heat Index risk, outdoor temp bands,
     AQI, and the trend arrows (pressure/temp/day-length) all draw from this one set, so
     e.g. "green" always means the same shade everywhere. Two *independently* tuned
     ramps (light here, dark below) rather than one shared value per token — light and
     dark only need to be coherent within themselves, not identical to each other.
     Colorblind-safety is deliberately not a design constraint for this app (small,
     all-sighted user base); these are always paired with a text label, never color
     alone, which is the actual mitigation for the vivid-but-lower-contrast hues below.
     Chosen 2026-07-07 after comparing this ramp against two independently-generated
     alternatives (see project_color_three_way_comparison memory) — true yellow/orange
     over muddier amber/burnt-orange alternatives that tested worse on cohesion. */
  --status-green: #16a34a;
  --status-amber: #eab308;
  --status-orange: #f97316;
  --status-red: #dc2626;
  --status-cyan: #06b6d4;
  --status-blue: #2563eb;
  /* Lighter companion to --status-blue — used where a trend needs two blue intensities
     (e.g. temp falling vs. falling fast) rather than a single flat blue. Untouched by
     the redesign above. */
  --status-blue-light: #60a5fa;
  /* Freezing only now (see --status-rose below for the "beyond red" role this used to
     share, which collided cold-extreme with severity-extreme under one hue). */
  --status-purple: #7c3aed;
  /* "Beyond red" severity cap — UV Extreme, Heat Index Extreme Danger, Dew Point
     Oppressive, AQI Very Unhealthy. Signals worse-than-red through hue, not darkness. */
  --status-rose: #e11d48;
  /* AQI-only, "Hazardous" — one tier past --status-rose. EPA's official 6-tier scale
     names this tier "Maroon", distinct from "Red" (Unhealthy), but literal dark maroon
     fails contrast against the dark-mode surface (needs shade-tuning that fights dark-
     mode legibility). Magenta instead, signalling "worse than rose" through hue. */
  --status-pink: #c026d3;
  --font-sans: 'Overpass', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'Overpass Mono', monospace;
  --value-weight: 600;
  --value-letter-spacing: -0.02em;
}

[data-theme="dark"] {
  --bg: #18181b;
  --surface: #27272a;
  --border: #3f3f46;
  --text: #f4f4f5;
  --text-muted: #a1a1aa;
  --accent: #fbbf24;
  --accent-dim: #f59e0b;
  --now-dot: #fbbf24;
  --rain: #38bdf8;
  --warn: #fde047;
  --moon-glow: #cbd5e1;
  --moon-lit: #cbd5e1;
  --moon-dark: #0f172a;
  --shadow: none;
  --chart-grid: #3f3f46;
  --chart-tick: #a1a1aa;
  --chart-tooltip-bg: #18181b;
  --chart-tooltip-text: #f4f4f5;
  --chart-temp: #ef4444;
  --chart-dewpoint: #22c55e;
  --chart-wind: #7dd3fc;
  --chart-humidity: #14b8a6;
  --chart-uv: #a855f7;
  --chart-pressure: #94a3b8;
  --chart-solar: #fde68a;
  --chart-rain: #38bdf8;
  --chart-cloud: #a8a29e;
  /* Flips light->dark for the dark theme, per the sequential-ramp
     convention (see :root's comment above) — --windrose-4 still matches
     --chart-wind's own dark-mode value, --windrose-5 one step lighter/more
     saturated for "Fresh+", popping brighter against the dark surface. */
  --windrose-1: #0c4a6e;
  --windrose-2: #0369a1;
  --windrose-3: #0ea5e9;
  --windrose-4: #7dd3fc;
  --windrose-5: #e0f2fe;
  --windrose-calm: #52525b;
  --sun-day: #fde68a;
  /* Independently-tuned dark-mode half of the ramp defined in :root — see the comment
     there. --status-cyan and --status-blue-light are intentionally not overridden here,
     they stay fixed across both themes. */
  --status-green: #4ade80;
  --status-amber: #fde047;
  --status-orange: #fb923c;
  --status-red: #f87171;
  --status-blue: #60a5fa;
  --status-purple: #a78bfa;
  --status-rose: #fb7185;
  --status-pink: #e879f9;
}

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.4;
}

/* ── Nav ────────────────────────────────────────────────── */
nav {
  position: sticky;
  top: 0;
  z-index: 10;
  height: var(--nav-h);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow);
  display: flex;
  align-items: center;
  padding: 0 16px;
  gap: 16px;
}

nav .site-name {
  font-weight: 600;
  font-size: 1rem;
  color: var(--text);
  flex: 1;
}

nav .nav-buttons {
  display: flex;
  gap: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 3px;
}

nav a {
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 500;
  padding: 6px 16px;
  border-radius: 3px;
  transition: color 0.15s, background 0.15s;
  white-space: nowrap;
}

nav a:hover { color: var(--text); background: var(--border); }
nav a.active { color: var(--bg); background: var(--accent); font-weight: 600; }

/* Phone-width only (matches the almanac grid's own phone/tablet line below) — site-name
   ("Mt. View Dr. Weather") doesn't fit next to the button pill at this width and wraps to
   multiple lines, which overflows nav's fixed height and mashes everything together.
   Reflowed via `order` (not DOM changes): site-name + theme-toggle share line 1, the
   button pill is forced to its own full-width line 2 via flex-basis 100%. Looks fine on
   iPad (≥768px) already, so this shouldn't fire there. */
@media (max-width: 680px) {
  nav {
    flex-wrap: wrap;
    height: auto;
    padding: 10px 16px;
  }
  nav .theme-toggle { order: 2; }
  nav .nav-buttons {
    order: 3;
    flex: 1 1 100%;
  }
  nav .nav-buttons a {
    flex: 1;
    text-align: center;
  }
}

.theme-toggle {
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  font-size: 1.1rem;
  color: var(--text-muted);
  padding: 4px 8px;
  line-height: 1;
  display: flex;
  align-items: center;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
  flex-shrink: 0;
}
.theme-toggle:hover { color: var(--text); background: var(--border); }

/* ── Main layout ────────────────────────────────────────── */
main {
  padding: 16px;
  max-width: 960px;
  margin: 0 auto;
}

/* Meteogram gets its own, wider cap — the one page centered on a continuous
   horizontal timeline (Dashboard/History/Almanac are card/summary layouts
   that don't benefit from extra width the same way). 1400px is wide enough
   that 72h's ~1170px of scrollable content (24 columns + axis gutters, see
   js/meteogram.js) fits without scrolling on a normal maximized desktop
   window, while still capping things on an ultrawide monitor rather than
   stretching a single line chart edge to edge. Deliberately a second fixed
   cap, not something computed from the currently-active range/data — tying
   page width to a moving target (which range is selected, whether pressure
   has arrived yet) would add a second layout-shift dynamic on top of the
   one already documented in js/meteogram.js's pressure-fetch pop-in.
   js/meteogram.js's width-adaptive scroll-mode check already measures
   whatever width is actually available at runtime, so it needs no changes
   here — it'll stop needing scroll mode on wide-enough windows and fall
   back to it on narrower ones, automatically. */
#meteogramMain {
  max-width: 1400px;
}

h2 {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin: 40px 0 10px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

/* Each chunk of the Current Conditions date/day-of-year is one unbreakable
   unit (see updateConditionsDateHeading() in dashboard.js) — the regular
   space between the two <span>s is the only valid wrap point, so narrow
   viewports break after the year, not mid-date or mid-"Day X of Y". */
#conditionsDate span {
  white-space: nowrap;
}

h2:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}

/* Forecast heading gets a heavier, more deliberate section break than the
   default h2 — more vertical room, a thicker accent-colored rule instead of
   the neutral hairline, and a bumped-up size/color so it reads as a real
   section boundary rather than a repeat of "Current Conditions" above the
   tile grid. Shipped as "Seam Treatment A" while the tile grid was still
   masonry, to bridge the style clash with the forecast section's own
   regular grid. The tile grid is a real CSS Grid now too (see the Metric
   grid section above), so that clash is gone — this divider is doing less
   work than it used to and may be worth softening or removing; not done
   here since it's a visual call, not a bug. */
#forecastHeading {
  margin-top: 64px;
  padding-top: 28px;
  border-top: 2px solid var(--accent-dim);
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  color: var(--text);
}


/* ── Metric grid ─────────────────────────────────────────
   The dashboard's former two-panel layout (a Conditions "hero" sidebar
   beside this grid) is gone — Today is just another tile in the same grid
   now (see project_conditions_card memory for why). No more dashboard-layout
   wrapper; .metric-grid sits directly under <main>. */
.metric-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}

/* .metric-col was a real flex column under the old masonry layout (DOM
   grouping = column membership, see git history around 2026-07-08 if that
   mechanism is ever needed again); now every tile is a direct grid child at
   every breakpoint, positioned purely by `order` below, so this is
   unconditional at all widths. */
.metric-col { display: contents; }

/* Reading order shared by phone (2 cols) and tablet (3 cols) — same eight
   values, just wrapped at a different width, so tablet gets Today/Temp/Wind,
   Rain/Baro/UV, Sun/Moon/(Indoor) for free without a second order list.
   Desktop (≥960px, below) gets its own order matching the column-pairing
   grouping (Today+Rain, Temp+Wind, Baro+UV, Sun+Moon) established
   separately for that tier. */
@media (max-width: 959px) {
  .tile-today { order: 1; }
  .tile-temp  { order: 2; }
  .tile-wind  { order: 3; }
  .tile-rain  { order: 4; }
  .tile-baro  { order: 5; }
  .tile-uv    { order: 6; }
  .tile-sun   { order: 7; }
  .tile-moon  { order: 8; }
  /* Unordered items default to order:0, which would otherwise jump this
     tile (hidden by default, in DOM after Moon) ahead of everything above
     the instant it's toggled visible. */
  #indoorTempTile { order: 9; }
}

/* Tablet tier — iPad portrait and similar widths. 3 columns × 3 rows (8
   tiles + the optional Indoor Temp 9th); the last cell sits empty unless
   Indoor Temp is on. */
@media (min-width: 680px) and (max-width: 959px) {
  .metric-grid { grid-template-columns: repeat(3, 1fr); }
}

/* Desktop — wide screens. 4 columns × 2 rows, with the same tile pairing
   the old masonry columns used (Today+Rain, Temperature+Wind, Baro+UV,
   Sun+Moon), just read across a row instead of down a column now that
   every tile is a real grid cell. A row's height comes from its own
   tallest tile (plain CSS Grid `auto` rows) — not forced equal across both
   rows; revisit with a more global (all-8-tiles-uniform) option later if
   that's ever called for. */
@media (min-width: 960px) {
  .metric-grid { grid-template-columns: repeat(4, 1fr); }

  .tile-today { order: 1; }
  .tile-temp  { order: 2; }
  .tile-baro  { order: 3; }
  .tile-sun   { order: 4; }
  .tile-rain  { order: 5; }
  .tile-wind  { order: 6; }
  .tile-uv    { order: 7; }
  .tile-moon  { order: 8; }
  #indoorTempTile { order: 9; }
}

/* ── Section badge inside temp cards ────────────────────── */
.section-badge {
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent);
  margin-bottom: 2px;
}

/* ── "Today" tile ────────────────────────────────────────
   No longer a bespoke hero card — just another .tile using the shared
   .uv-top/.icon-slot/.secondaries pattern (see project_conditions_card
   memory for the history here). What isn't off-the-shelf: the icon (a
   Lucide SVG via wmoIconSvg(), same as every other weather-condition icon
   in the app — see .wx-ico below) pairs with a description caption directly
   beneath it — .cond-icon-col replaces .icon-slot's usual direct position
   in .uv-top so the two stack together as one unit, rather than the caption
   spanning the full tile width below both columns; and Hi pairs with a
   small stacked "Low" caption inline, same hero treatment this card used
   before it became a regular tile. */
.cond-icon-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.cond-icon {
  font-size: 3rem;
  line-height: 1;
}

/* Weather-condition icons (wmoIconSvg() in js/forecast.js) — Lucide, ISC
   licensed, every glyph stroke="currentColor" fill="none" at the source.
   .wx-ico itself carries no color and no fixed size: it inherits whatever
   font-size the surrounding element already sets (.cond-icon, .forecast-card
   .icon, .hourly-icon, .icon-col .emoji — one shared rule covers all four
   contexts, confirmed unnecessary to calibrate per-icon since Lucide draws
   every icon at consistent visual weight on its own 24x24 grid, unlike a
   hand-varied icon set). Color is opt-in via .wx-lu-*, deliberately mapped
   to tokens that already exist and already mean something app-wide, not
   colors invented for this purpose. Full comparison against emoji and
   Meteocons (fill/line/monochrome), including why a single-color icon needs
   its diagnostically important part colored rather than its most visually
   prominent one (e.g. a rain-shower icon colored as rain, not as the sun
   peeking through) — project_meteocons_icons memory. */
.wx-ico { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.wx-ico svg { width: 100%; height: 100%; display: block; }
.wx-lu-sun { color: var(--sun-day); }
.wx-lu-moon { color: var(--moon-glow); }
.wx-lu-cloud { color: var(--chart-cloud); }
.wx-lu-rain { color: var(--chart-rain); }
/* --status-purple, not a dedicated snow token — real gap found on the
   Almanac page (a snow icon and a rain-% stat read as near-identical blues
   at a glance, live screenshot comparison, not assumed). --status-purple
   already means exactly one thing app-wide, "Freezing," and nothing else,
   so this both fixes the distinctness problem and reinforces existing
   meaning rather than introducing a new color to learn. */
.wx-lu-snow { color: var(--status-purple); }
.wx-lu-storm { color: var(--warn); }

.cond-desc {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
  text-align: center;
}

/* .cond-desc ("Clear sky") used to visibly collide with the Low value
   (.cond-lo) at narrow iPhone widths — previously patched with a hand-tuned
   `transform: translateX(15px)` at <=410px, calibrated against a 2-digit
   Low ("61°"). That patched the wrong side: the real cause is
   .cond-hero-row (the Hi value + Low stack) living inside
   .uv-label-value, a flex item with `min-width: 0` so it can shrink when
   .uv-top is tight for space. Its content doesn't shrink, so once a
   3-digit Hi ("102°") needs more room than the compressed box has, the Low
   stack overflows *outside* .uv-label-value's right edge uncontained and
   lands directly under the icon/description column — confirmed via
   getBoundingClientRect(): .cond-lo's box measured past .uv-label-value's
   own right edge. translateX on .cond-desc never addressed this; it just
   happened to visually mask one specific measured case (2-digit Low,
   <=410px) and broke again at 3 digits. Real fix is flex-wrap below: it
   lets Low drop to its own line *inside* the row's box instead of
   spilling out sideways when there isn't room, which holds for any
   Hi/Low digit count and stays a single line whenever there's space
   (confirmed via Playwright screenshot matrix: 75°/61° unaffected, 102°/70°
   no longer overlaps at 390/375/320px). */
.cond-hero-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px;
}

.cond-lo-stack {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.cond-lo-label {
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
}

.cond-lo {
  font-size: 1rem;
  font-weight: 500;
  font-family: var(--font-mono);
  color: var(--text-muted);
}

/* "Much cooler than yesterday" is prose, not a short value like every other
   .secondary-value ("5.14 mph", "58%") — no label-left/value-right row (that
   was tried: even stacked, squeezing it into a value column meant it wasn't
   guaranteed to fit one line at the longest phrasing). Instead, a plain
   caption with the tile's full width, same treatment as .cond-desc above
   the divider (sans font, no heading — the sentence is self-explanatory,
   same reasoning that caption never needed a "Description" label either). */
.cond-compare-caption {
  font-family: var(--font-sans);
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--text-muted);
}

/* .cond-source's dashboard usage is gone (the "Today" tile's attribution now
   lives in a plain .secondary row instead), but almanac.html still uses this
   class for its own source tag (#almSource) — base rule kept for that,
   scoped positioning override in .almanac-card .cond-source below. */
.cond-source {
  font-size: 0.68rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

/* Lives *inside* <main> on every page (moved there 2026-07-10 — see the
   git log around that date), specifically so it shares main's own
   containing block/padding instead of computing its own independent
   centering as a sibling of main. That's what makes footer alignment
   structural rather than a per-page coincidence: main's own 960px
   max-width + 16px padding already produces the correct left inset for
   plain pages, so this rule contributes zero *horizontal* padding of its
   own — adding any would double up on top of main's. max-width/margin here
   are inert on every page except where an override below narrows them
   further (960 can never bind once nested inside a max-960 main, since
   main's own padding always leaves less than 960px available) — kept
   rather than deleted so the narrowing overrides below read as "start from
   this, then override," not a mystery number appearing from nowhere. */
.app-footer {
  max-width: 960px;
  margin: 0 auto;
  text-align: left;
  padding: 16px 0 24px;
  font-size: 0.68rem;
  font-weight: 400;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  opacity: 0.6;
}
/* Almanac's own .almanac-card is a narrower 640px box independently
   centered *within* main's 960px column (not flush with main's own edge).
   This override's narrower max-width actually binds (640 < main's
   ~928px-960px available content width) — no horizontal padding needed on
   top of that, deliberately: the user wants the footer text flush with the
   card's own *outer border*, not indented to match the card's internal
   16px content padding (tried that first, rejected 2026-07-10 — sat
   noticeably right of the card's visible edge once actually seen
   side-by-side in a full screenshot). */
.app-footer-almanac {
  max-width: 640px;
}
/* Meteogram's own #meteogramMain cap is wider than every other page's
   shared 960px main — the footer needs to match, or its centered box would
   sit narrower than (and misaligned with) the content above it. */
.app-footer-wide {
  max-width: 1400px;
}

/* Generic on/off switch — used by the history page's Indoor charts toggle
   (a full-width row at the top of that page; see #indoorToggleRow below).
   The dashboard's own Indoor Temp toggle moved out of this row-with-divider
   shape entirely once "Today" stopped being a special card — see
   .indoor-toggle-inline below for its new home in the header status bar. */
.toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/* Standalone page-level instance (history page) needs its own bottom margin. */
#indoorToggleRow {
  margin-bottom: 16px;
}

/* Dashboard's Indoor Temp toggle — lives in .status-bar now, inline with the
   status dot/text/refresh button, pushed to the far right of that row. No
   divider/row styling needed here (unlike .toggle-row above); this is a
   compact label+switch pair, not a full-width settings row. */
.indoor-toggle-inline {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

.toggle-label {
  font-size: 0.72rem;
  color: var(--text-muted);
}

.toggle-switch {
  position: relative;
  width: 34px;
  height: 20px;
  border-radius: 10px;
  background: var(--border);
  border: none;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}

.toggle-switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
  transition: left 0.15s;
}

.toggle-switch.on {
  background: var(--accent);
}

.toggle-switch.on::after {
  left: 16px;
}

.cond-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 0;
}

/* Shared with almanac.html's cell labels ("Weather", "Avg Temp", "Wind (mph)", etc.) */
.cond-row-label {
  font-size: 0.75rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.uv-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--text-muted);
}

.wind-compass {
  display: block;
  margin: 10px auto 2px;
}

.uv-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* Year to Date cell only. Its sun-badge icon (58px, kept large deliberately —
   see .sun-badge-wrap-nudge below) is taller than a single-line value, so
   plain center-alignment left the value sitting well below where the label
   right above it would suggest. Icon position is handled separately via its
   own transform (also below), so this only moves the value text — the icon
   fills the row's own height regardless of align-items. */
.uv-top-align-start {
  align-items: flex-start;
}

.uv-label-value {
  min-width: 0;
}

/* Shared fixed-width slot every .uv-top icon sits in (UV pyramid, pressure gauge,
   rain drop, dew wave, thermometer) — centers whatever's inside it via flexbox,
   which is exact by construction, rather than each icon getting its own hand-tuned
   margin-right guess at "centered in the leftover whitespace" (which needed
   re-tuning after every visual check: pressure gauge, rain drop, dew wave all went
   through this). Width sized to the UV pyramid, the widest icon at 73px, since
   it's the one already proven to fit in both the dashboard tile (~115-240px inner
   content depending on breakpoint) and the almanac's narrower, more stable
   ~168px cell without complaint. One shared width instead of five per-icon,
   per-context values. */
.icon-slot {
  width: 75px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* Sun tile (dashboard) / Sun cell (almanac) only — added alongside .icon-slot, not
   a replacement for it, so every other icon (UV pyramid, rain drop, dew wave,
   thermometer, pressure gauge, moon glyph) keeps the shared centered treatment
   untouched. Day length ("14h32m", fmtDayLength() in js/moon.js) is deliberately
   never allowed to wrap or shrink — unlike every other tile's shorter value — so at
   phone width the centered sun-pie sat close enough to overlap/hide the superscript
   "m" unit specifically. Starts from the exact same centered position every other
   icon-slot uses (no justify-content/margin override), then nudges right by a pure
   visual transform — deliberately not a flex-alignment change, so "same spot as all
   the others, then shift" is literal, not just an equivalent-looking end state.
   Landed on a flat 18px after iterating through 2 en (15.84px), 3 en (23.76px), and
   4 en (31.68px) — 1 en being 0.5 × 0.55em × 1.8rem (half the day-length value's
   superscript unit size) ≈ 7.92px. 2 en still left a few px of overlap on mainstream
   widths; 4 en closed the overlap everywhere but visibly pushed the icon past the
   cell's own right edge into the inter-cell gap (user caught this from the actual
   screenshot); 3 en satisfied both constraints but read as having moved further
   right than necessary once compared side by side against the default/4-en
   screenshots. 18px is a plain round number, not tied to the en unit anymore —
   fully clears the text on the user's actual iPhone 11 (414px CSS width — note this
   is *wider* than the 12/13/14's 390px, despite both being "6.1-inch" phones; 2x vs
   3x pixel density is why) and the 430px class. Known, accepted gaps: ~1.7px of
   overlap remains at exactly 390px (the 12/13/14 group — not the user's device),
   ~7.4px at 375px (iPhone SE2/SE3/8/X); the 320px 1st-gen iPhone SE is hopeless
   regardless of icon position or shift amount (the cell itself is narrower there
   than the unbroken text alone needs). See project_mobile_overflow_gotchas memory
   for the full comparison table across every shift amount tried.

   The overlap this fixes only happens in the narrow phone layout (2-column
   metric-grid) — at 680px+ .metric-grid switches to a wider grid tier (3 or
   4 columns, see the tablet/desktop rules above) and every icon-slot has
   room to stay centered like the rest. Originally shipped as an unscoped rule that pushed
   the icon at every width, including desktop, misaligning it against every
   other tile's icon; scoped to max-width:679px 2026-07-07 to match .metric-grid's
   own breakpoint and confine the nudge to the layout mode it was meant for. */
@media (max-width: 679px) {
  .icon-slot-sun {
    transform: translateX(18px);
  }
}

.uv-pyramid {
  display: block;
  flex-shrink: 0;
}

/* Wraps just the almanac UV pyramid so it can carry a tap tooltip (severity label,
   e.g. "Moderate") — ::after never renders on <svg> directly, same reason every
   other icon-wrap (.rain-drop-wrap, .thermo-gauge-wrap, etc.) exists. Dashboard's UV
   tile already shows this label as visible text right below the pyramid, so it
   doesn't get this wrapper/tooltip — would just be redundant there. */
.uv-pyramid-wrap {
  display: block;
  flex-shrink: 0;
}

.uv-pyramid-wrap.tooltip-target {
  border-bottom: none;
}

.rain-drop-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}

/* Icon, not text — opt out of .tooltip-target's dotted border-bottom (meant for text
   spans like "Hi at 5:55 PM"), same override .thermo-gauge-wrap.tooltip-target uses.
   Lives on the wrapper div, not the <svg> itself — CSS generated content (::after,
   what .tooltip-target actually uses to paint the bubble) never renders on SVG
   elements in any browser, so the trigger has to be a real HTML element. cursor:help
   and border-bottom are plain CSS properties and *do* apply fine to <svg>, which is
   why the desktop cursor still changed even though no bubble ever appeared. */
.rain-drop-wrap.tooltip-target {
  border-bottom: none;
}

.rain-drop {
  display: block;
}

/* Under the drop, not beside it — trades the earlier width problem for some added
   height in this row; kept compact (small font, tight gap) to minimize it. */
.rain-drop-pct {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--rain);
  white-space: nowrap;
}

.rain-drop-pct:empty {
  display: none;
}

.sun-badge-wrap {
  display: flex;
  flex-shrink: 0;
}

/* Same SVG-can't-take-::after reasoning as .rain-drop-wrap.tooltip-target above. */
.sun-badge-wrap.tooltip-target {
  border-bottom: none;
}

/* Pulls the (deliberately oversized, 58px) badge up to better match the
   Sun/Moon cells' icon height — same leftover-nudge pattern as
   .almanac-card .pressure-gauge/.moon-glyph. Vertical shift applies at every
   width (the size mismatch isn't width-dependent); the extra rightward shift
   below is phone-only, same scoping as .icon-slot-sun, since there's no
   equivalent crowding to correct for at desktop's 3-column layout.
   margin-bottom matches the translateY 1:1 — transform only shifts paint
   position, it doesn't shrink the layout box, so without this the icon-slot
   still reserved its full un-shifted height and left a 15px hole between the
   icon and .secondaries below that the Sun/Moon cells don't have (confirmed
   via measurement: 25px gap here vs. Sun's 12px, the exact 15px difference). */
.sun-badge-wrap-nudge {
  transform: translateY(-15px);
  margin-bottom: -15px;
}

@media (max-width: 679px) {
  .sun-badge-wrap-nudge {
    transform: translateY(-15px) translateX(6px);
  }
}

.sun-badge {
  display: block;
}

.compass-label {
  font-size: 11px;
  font-weight: 700;
  fill: var(--text);
  font-family: var(--font-sans);
}

.compass-heading {
  font-size: 13px;
  font-weight: 700;
  fill: var(--text-muted);
  font-family: var(--font-mono);
}

/* Faded and parked at a neutral heading (not a real reading) below CALM_WIND_MPH — see
   renderCompassRose()'s `calm` option in js/widgets.js. */
.compass-needle.calm {
  opacity: 0.3;
}

.uv-level-value {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Sun/moon position bar: fill accumulates behind a marker riding its edge,
   same "fill = progress" convention as the pressure gauge/UV pyramid. Shared
   structure for both — only the color (.sun-fill/.sun-marker vs
   .moon-fill/.moon-marker below) differs. At night/moon-down the fill goes
   flat/muted (there's no meaningful position in an inactive window) and
   .astro-night-caption takes over with a countdown instead — see
   buildAstroBar() in widgets.js. */
.astro-bar-wrap {
  position: relative;
  height: 34px;
  margin-top: 4px;
}

/* Track's vertical center is 34px/2 = 17px — .astro-marker's top below is
   set to that same 17px so both are centered on the same line; don't
   change one without the other. */
.astro-track {
  position: absolute;
  top: 14px;
  left: 0;
  right: 0;
  height: 6px;
  border-radius: 3px;
  background: var(--border);
  overflow: hidden;
}

.astro-fill {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  border-radius: 3px;
  width: 0%;
}

.astro-fill.night {
  background: var(--text-muted);
  opacity: 0.35;
}

/* Purely positional — .sun-marker/.moon-marker below supply the actual shape
   (a plain dot for the sun; a crescent SVG for the moon, so they aren't
   forced into the same circle-with-border look). */
.astro-marker {
  position: absolute;
  top: 17px;
  left: 0%;
  width: 20px;
  height: 20px;
  transform: translate(-50%, -50%);
}

.astro-marker.hidden {
  display: none;
}

.sun-fill {
  background: var(--sun-day);
}

.sun-marker {
  border-radius: 50%;
  background: var(--sun-day);
  border: 2px solid var(--surface);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--sun-day) 25%, transparent);
}

/* var(--moon-glow), not var(--text-muted) directly — dark mode wants a
   distinctly paler/silvery tone than regular muted text gets, see the
   variable's definition at the top of this file for the contrast reasoning. */
.moon-fill {
  background: var(--moon-glow);
}

/* Solid disc, same size/border/glow treatment as .sun-marker (not upsized —
   that compensation was specific to the old thin crescent silhouette, see
   git history), split lit/dark like a last-quarter moon so it still reads as
   distinctly "the moon" next to the sun's uniform disc. Fixed presentation —
   always this same half-and-half split regardless of the real current phase
   (an identity marker, not a phase readout; same convention as the
   thermometer glyph's fixed fill level) — the real phase still shows
   correctly elsewhere in this tile (headline %, phase name, emoji).
   Left-lit/right-dark matches Last Quarter in Northern Hemisphere convention
   (also how the 🌗 emoji renders), though which side doesn't functionally
   matter — the glow ring already separates the marker from the track either way. */
.moon-marker {
  border-radius: 50%;
  background: linear-gradient(90deg, var(--moon-lit) 50%, var(--moon-dark) 50%);
  border: 2px solid var(--surface);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--moon-glow) 25%, transparent);
}

/* No reserved min-height: most of the time this is empty, and letting it
   collapse to 0 keeps the tile tight (the far more common state) at the
   cost of a small layout shift when the caption appears. Italic dropped —
   it was the only italic text anywhere in the app. Negative margin-top pulls
   it up into .astro-bar-wrap's own dead space below the track (the wrapper
   is 34px tall to fit the marker+glow, but the track itself sits at
   top:14px/height:6px, so ~14px of empty wrapper space already exists below
   it before this caption's own box even starts) — without this it reads as
   oddly far from the bar it's describing. */
.astro-night-caption {
  text-align: center;
  font-size: 0.8rem;
  color: var(--text-muted);
  margin-top: -10px;
}

/* margin-top clearance (not on .astro-night-caption's own negative margin,
   which governs its text-to-bar spacing specifically) — needed since the
   marker was resized to a full 20px circle+glow (see .moon-marker), which
   now reaches the full height of .astro-bar-wrap with no dead space left
   below it; without this the glow overlapped Sunrise/Sunset/Moonrise/Moonset. */
.astro-ends {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-top: 6px;
}

.astro-end-right {
  align-items: flex-end;
}

/* A real label/value row that follows .astro-ends — not the .secondaries
   pattern (that draws a divider, which doesn't fit here since the bar
   above already separates headline from these facts). */
.astro-extra-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  margin-top: 6px;
}

/* Was a plain emoji text glyph (font-size 2.2rem); now an SVG rendered by
   renderMoonGlyph() (js/widgets.js). Border/glow recipe copied from
   .moon-marker below, not the sun pie's stroke-free look — see that
   function's comment for why (keeps --moon-dark's near-black value from
   disappearing in dark mode without a second bright color to anchor against). */
.moon-glyph {
  display: block;
  flex-shrink: 0;
  border-radius: 50%;
  border: 2px solid var(--surface);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--moon-glow) 25%, transparent);
}

/* Wraps the moon-glyph SVG (both dashboard and almanac) so it can carry a
   tap tooltip — ::after never renders on <svg> directly, same reason
   .sun-pie-wrap exists. Dashboard's tooltip is lunar age (day of cycle,
   days to next major phase); almanac's is % of the day visible, split
   day/night — same wrapper mechanism, different content per page. */
.moon-glyph-wrap {
  display: block;
  flex-shrink: 0;
}

.moon-glyph-wrap.tooltip-target {
  border-bottom: none;
}

.moon-phase-label {
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--text-muted);
}

.astro-time-group {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 8px;
  row-gap: 3px;
}

.astro-time-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  box-shadow: var(--shadow);
  /* Grid items default to min-width:auto, which refuses to shrink below their content's
     natural width — at the phone-width 2-column layout below, that forced the whole page
     to auto-widen past the true device width instead of tiles actually sharing it. Same
     bug/fix as .alm-section on the almanac page. */
  min-width: 0;
}

/* min-height reserves 2 lines' worth regardless of whether a given label
   actually wraps. The grid (see the Metric grid section above) stretches
   every tile in a row to the same *total* height, but that only adds blank
   space at the bottom of shorter tiles — it doesn't keep a label-to-value
   position aligned between tiles whose label wraps a different number of
   lines. Reserving the height universally, even on labels that never wrap,
   is what actually guarantees that internal alignment. */
.tile .label {
  font-size: 0.78rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  line-height: 1.3;
  min-height: 2.6em;
}

/* "Baro (hPa)" — phone-width .metric-grid uses a bare `repeat(2, 1fr)`
   (each track is really minmax(auto, 1fr)), so a column's minimum width is
   set by the single widest *unbreakable* word anywhere in that column, not
   by any one tile's own needs. Column 2 (Outside Temperature, Rain, UV
   Index, Moon) gets a wide floor for free because "Temperature" is one
   unbroken word; Column 1 (Today, Wind, Baro, Sun) never had anything
   forcing a floor wider than "Today", so "Baro (hPa)" — which has a space
   in it and can wrap — was losing the tie-break and wrapping. Forcing it
   nowrap makes it the new floor for Column 1 instead, the same mechanism
   that already works for "Temperature" — and it needs *less* width than
   "Temperature" already proves this grid can grant a column without
   overflow, so this should widen Column 1 rather than break anything. */
.label-nowrap {
  white-space: nowrap;
}

/* Opts a unit symbol out of .tile .label's uppercase transform — "hPa"
   (hecto-Pascal) is a real mixed-case abbreviation, unlike "mph"/"in" which
   read fine uppercased; forcing it to "HPA" loses that meaning. */
.label-unit-case {
  text-transform: none;
}

.tile .value {
  font-size: 1.8rem;
  font-weight: var(--value-weight);
  color: var(--text);
  line-height: 1.1;
  font-family: var(--font-mono);
  letter-spacing: var(--value-letter-spacing);
}

.value-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
}

.temp-top {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 28px;
}

.temp-label-value {
  min-width: 0;
}

/* Value text ("91.2°") overlaps the thermometer icon at phone width — a
   different mechanism than the Today tile's .cond-desc fix, and NOT fixable
   by adjusting `gap` (tried first, confirmed ineffective by direct
   measurement): .temp-label-value is the flex-shrinkable item here, so
   reducing gap just lets its own box absorb the reclaimed space and grow —
   the overflowing text's start position (pinned to the row's fixed left
   edge) and the icon-slot's position (already flush against the tile's
   right padding, ~1px slack at every width) both stay unchanged regardless
   of gap. .icon-slot has `flex-shrink: 0`, so its `width` is respected
   exactly — shrinking it directly moves its own left edge (and therefore
   the icon) rightward by the same amount, which is what actually clears
   the overlap. Not applied to the shared base .icon-slot (used by every
   other tile's icon) — a dedicated class, same pattern as .icon-slot-sun.
   Measured directly: overlap is real at every width, including 414px
   (iPhone 11/XR, 8.8px) — worse at 390px (12/13/14, 20.8px), worse still at
   375px/320px (up to 55.8px at 320px, only partially closed by this — same
   scope as the Today tile fix, SE-class widths aren't the target here
   either). Shrinking the slot from 75px to 45px reclaims 30px, clearing
   both 414px and 390px with margin. Scoped to the same .metric-grid phone
   breakpoint as .icon-slot-sun's fix (679px), not a narrow band — this tile
   only ever has one icon, so a smaller centered slot has no real downside
   in the 2-column phone layout, unlike shrinking the shared base rule every
   other tile also uses. */
@media (max-width: 679px) {
  .icon-slot-temp {
    width: 45px;
  }
}

.thermo-gauge {
  display: block;
  flex-shrink: 0;
}

/* Wrapper is the actual flex item of .temp-top now (svg tooltip moved here — see
   .rain-drop-wrap.tooltip-target's comment for why: ::after never renders on <svg>). */
.thermo-gauge-wrap {
  display: block;
  flex-shrink: 0;
}

.thermo-gauge-wrap.tooltip-target {
  border-bottom: none;
}

/* Trend arrow moved off the value row and onto the icon itself (was "68.7°F ↓↓" —
   the arrow was the second-biggest space consumer after the value, which is what
   forced phone width into a separate stacked/centered layout below the value; see
   git history for that old rule if this needs reverting). Plain colored text, no pill
   background — deliberately flatter than the rain drop's badge, matching this icon's
   own flat-graphic style rather than introducing a boxed-badge metaphor here too.
   Sits at top-right, which lines up with the tube's always-unfilled top portion
   (THERMO_FIXED_FILL_FRAC in js/widgets.js never fills above 75% from the bottom), so
   it's never sitting on top of the color fill — no contrast problem to solve with a
   pill in the first place. */
.thermo-gauge-wrap .temp-trend {
  position: absolute;
  top: -2px;
  right: -8px;
  font-size: 0.95rem;
}

.temp-trend {
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1.1;
}

/* Smaller than .temp-trend and on its own line (see the Sun tile's markup) —
   full-size next to Day Length made the tile noticeably wider than its
   neighbors for a fact that's secondary to the headline. */
.day-length-trend {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1.2;
}

/* Superscript unit letters in fmtDayLength()/fmtDurationAbs()'s output
   (js/moon.js) — "14ʰ39ᵐ" instead of "14h 39m". Plain ASCII h/m/s sized down
   via CSS rather than real Unicode superscript glyphs, which risk a silent
   font-fallback mismatch in Overpass Mono. Saves real width now that the Sun
   cell's day/night pie shares the row with this value (see .icon-slot), and
   matches the standard astronomical convention for hour/minute/second
   durations (e.g. right-ascension notation), so it isn't an invented shorthand. */
.dl-unit {
  font-size: 0.55em;
  vertical-align: super;
  letter-spacing: 0;
}

/* .day-length-trend is already the smaller of the two contexts .dl-unit
   appears in (0.85rem vs. the main value's larger size) — shrinking its
   unit letters *again* on top of that (0.55em of an already-small base)
   made the delta's h/m/s essentially unreadable. Cancel the superscript
   there only; the main day-length value above it keeps it. */
.day-length-trend .dl-unit {
  font-size: 1em;
  vertical-align: baseline;
}

/* Same cancellation as .day-length-trend above, same reason: the countdown
   caption ("Sunrise in 3h45m") reuses fmtDayLength() but at small status-bar
   text size (0.8rem, see .astro-night-caption) — superscripting is only
   meant to save space on the large headline day-length value. */
.astro-night-caption .dl-unit {
  font-size: 1em;
  vertical-align: baseline;
}

/* Clock-accurate day/night pie: 0/24 at bottom, noon at top, clockwise — see
   renderSunPie() in js/widgets.js. Sized to fit the Sun cell's unusually wide
   value alongside it; smaller than the shared .icon-slot's 75px width, which
   is fine since .icon-slot just centers whatever's inside it. */
.sun-pie {
  display: block;
  flex-shrink: 0;
}

/* Wraps the sun-pie SVG (both dashboard and almanac) so it can carry a tap tooltip
   (% of the day that's daylight) — ::after never renders on <svg> directly, same
   reason every other icon-wrap (.rain-drop-wrap, .thermo-gauge-wrap, etc.) exists. */
.sun-pie-wrap {
  display: block;
  flex-shrink: 0;
}

.sun-pie-wrap.tooltip-target {
  border-bottom: none;
}


.tile .secondaries {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.tile .secondary {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
}

.tile .secondary-label {
  font-size: 0.75rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  white-space: nowrap;
}

.tile .secondary-value {
  font-size: 0.9rem;
  font-weight: 400;
  color: var(--text);
  font-family: var(--font-mono);
  white-space: nowrap;
}

.secondary-value-group {
  display: flex;
  align-items: center;
  gap: 6px;
}

.heat-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
  display: none;
}

.heat-dot.visible {
  display: inline-block;
}

.heat-dot.tooltip-target {
  border-bottom: none;
}

#pressureTrend {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3em;
}

.pressure-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--text-muted);
}

.pressure-dot.lit {
  background: var(--status-cyan);
}

/* ── Forecast cards ─────────────────────────────────────── */
.forecast-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
}

@media (max-width: 479px) { .forecast-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 960px) { .forecast-grid { grid-template-columns: repeat(7, 1fr); } }

.forecast-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 12px;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 6px;
  box-shadow: var(--shadow);
}

.forecast-card .day {
  font-size: 0.85rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.forecast-card .icon { font-size: 2rem; line-height: 1; }

.forecast-card .hi-lo {
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--text);
}

.forecast-card .hi-lo span { color: var(--text-muted); font-weight: 400; }

.forecast-card .precip {
  font-size: 0.9rem;
  color: var(--rain);
}

.forecast-card .wind {
  font-size: 0.9rem;
  color: var(--text-muted);
}

/* ── Status bar ─────────────────────────────────────────── */
.status-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-bottom: 12px;
}

.status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-muted);
}

.status-dot.live { background: var(--status-green); }

.status-dot.error { background: var(--status-red); }

.status-dot.stale { background: var(--warn); }

/* ── Offline banner ─────────────────────────────────────── */
.offline-banner {
  display: none;
  background: var(--warn);
  color: #0f172a;
  text-align: center;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 8px;
}

.offline-banner.visible { display: block; }

/* ── Loading / skeleton ─────────────────────────────────── */
.skeleton {
  background: linear-gradient(90deg, var(--surface) 25%, var(--border) 50%, var(--surface) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
  border-radius: 4px;
  color: transparent !important;
  user-select: none;
}

@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* ── History charts ─────────────────────────────────────── */
.chart-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 16px;
  box-shadow: var(--shadow);
}

.chart-card h3 {
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 12px;
}

/* Solar Radiation card's header row (90d/1y only) — title + a compact Peak
   UV Index stat badge, since UV doesn't get its own chart at long ranges
   (solar/UV correlate at r²=0.96-0.99 even at raw resolution at this
   station, so a second near-identical line would just double up the same
   curve). The h3's own margin-bottom is neutralized here and moved to the
   row itself, so title and badge sit on one baseline instead of the badge
   trailing below it. */
.chart-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.chart-card-head h3 { margin-bottom: 0; }
/* Generic card-header stat badge (label + colored value) — currently only
   the Rain chart's cumulative Total badge (90d/1y). Originally built for the
   Solar Radiation card's Peak UV Index badge too; that badge was replaced by
   a strip/gradient directly on the card (2026-07-10, see js/history.js
   buildUvStrip()/buildUvGradient()), so only the color-neutral base plus
   Rain's own modifier are still live — a future metric badge should add its
   own modifier here rather than reach for .rain's color. */
.card-stat-badge {
  display: flex;
  align-items: baseline;
  gap: 6px;
  white-space: nowrap;
}
.card-stat-label {
  font-size: 0.75rem;
  color: var(--text-muted);
}
.card-stat-value {
  font-size: 0.85rem;
  font-weight: 600;
}
.card-stat-value.rain { color: var(--chart-rain); }

/* UV Index strip/gradient — history.js's buildUvStrip()/buildUvGradient(),
   replacing both the old standalone chartUV line chart (24h/48h/7d/30d) and
   the Peak UV Index badge (90d/1y) with one consistent treatment on the
   Solar Radiation card, at three densities depending on range. Column
   structure and the auto-contrast-label technique mirror
   js/meteogram.js's .cloud-strip (js/widgets.js's shared hexLuminance/
   TEXT_CROSSOVER_LUMINANCE) — cell color source differs (uvLevel()'s fixed
   EPA tiers, not an intensity alpha-blend), so it's its own rule set rather
   than reusing .cloud-strip's classes on a differently-meaning metric. */
.uv-strip-label {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--chart-tick);
  margin: 0 0 4px;
}
.uv-strip { display: flex; gap: 2px; margin: 0 0 6px; }
.uv-col { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.uv-col .seg {
  width: 100%;
  height: 26px;
  border-radius: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 1;
}
.uv-col .t {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  color: var(--chart-tick);
}

/* Gradient bar (90d/1y) — a continuous linear-gradient, one real color stop
   per day. The same per-day cell treatment used at 7d/30d was tried at this
   range first and ruled out: 90-365 individual cells are illegible at any
   real card width (confirmed against a 169-column mockup at 7d, worse here),
   so the gradient trades an always-visible number for a hover-only readout
   in exchange for scaling to a year with no density ceiling. */
.uv-gradient-wrap { position: relative; margin: 0 0 6px; }
.uv-gradient-inner { height: 22px; border-radius: var(--radius); overflow: hidden; }
.uv-gradient-bar { width: 100%; height: 100%; cursor: crosshair; }
.uv-crosshair {
  position: absolute;
  top: -4px;
  bottom: -4px;
  width: 1px;
  background: var(--text);
  opacity: 0.6;
  pointer-events: none;
  display: none;
}
.uv-tooltip {
  position: absolute;
  bottom: calc(100% + 8px);
  transform: translateX(-50%);
  background: var(--text);
  color: var(--bg);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  padding: 5px 8px;
  border-radius: 3px;
  white-space: nowrap;
  pointer-events: none;
  display: none;
  line-height: 1.4;
  z-index: 1;
}
.uv-tooltip .d { opacity: 0.7; }
.uv-tooltip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 4px solid transparent;
  border-top-color: var(--text);
}

/* Chart.js's responsive/maintainAspectRatio:false sizing measures this
   wrapper (a stable, explicitly-sized box), not the canvas's own forced CSS
   size directly — Chart.js needs a parent with a real height of its own to
   size correctly; a canvas that's merely forced to a size via CSS while
   sharing an auto-height parent with other content (the h3 title, and now
   the 90d/1y band's legend div) gets its internal drawing buffer sized off
   that parent's content-driven height instead, which doesn't match the
   canvas's own forced CSS box — Chart.js then stretches its buffer non-
   uniformly to fill it, visibly distorting (squashing) all canvas-drawn
   text. Confirmed via direct measurement 2026-07-08: every chart already
   had a small version of this (canvas asked to be 200px tall, Chart.js's
   real buffer was 229px), worsened to 258px once the legend div added a
   second sibling changing the card's total content height. This wrapper is
   the standard Chart.js fix — see project_longterm_charts memory. */
.chart-canvas-wrap { position: relative; width: 100%; height: 200px; }
.chart-canvas-wrap canvas { width: 100% !important; height: 100% !important; }
/* Wind rose SVG (90d/1y Wind Direction card) shares this same fixed-height
   wrap rather than introducing a new unconstrained one — see the big
   comment above about the canvas/CSS-box buffer-mismatch bug this pattern
   exists to avoid. No `display` set here deliberately: only .hidden (below)
   controls display, so toggling it always wins regardless of selector
   specificity — only one of svg/canvas is ever unhidden at a time, so plain
   document flow sizes it to the wrap's full 100%/200px box either way. */
.chart-canvas-wrap svg { width: 100%; height: 100%; }

/* Wind rose (90d/1y Wind Direction card only, toggled by
   setDailySummaryModeVisibility() in js/history.js) — a rose is circular, so
   stretching it across the same full-width/200px-tall box every other chart
   uses squashes it into an illegible sliver (confirmed via screenshot: a tiny
   circle lost in a wide letterboxed strip). Narrower-than-half-width instead,
   with the legend beside the plot (not stacked below it) to use that
   horizontal space rather than adding vertical height. `aspect-ratio: 1`
   (not a fixed height) keeps the wrap exactly square regardless of the
   `1fr` grid column's actual resolved width, so the SVG's own
   preserveAspectRatio="xMidYMid meet" never has to letterbox — that
   letterboxing was part of what read as "too much whitespace above the
   plot" alongside the h3's own margin, tightened below. The nested
   selectors intentionally out-specify the shared .chart-canvas-wrap rule
   regardless of source order (a bare same-specificity override bit this
   project before — see project_mobile_overflow_gotchas memory). */
.chart-card--wind-rose {
  max-width: 680px;
  margin-left: auto;
  margin-right: auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 20px;
  align-items: center;
}
.chart-card--wind-rose h3 { grid-column: 1 / -1; margin-bottom: 4px; }
.chart-card--wind-rose .chart-canvas-wrap { grid-column: 1; height: auto; aspect-ratio: 1; }
.chart-card--wind-rose #windRoseLegend {
  grid-column: 2;
  flex-direction: column;
  flex-wrap: nowrap;
  gap: 10px;
  padding-top: 0;
  white-space: nowrap;
}
@media (max-width: 480px) {
  .chart-card--wind-rose { max-width: 100%; grid-template-columns: 1fr; }
  .chart-card--wind-rose .chart-canvas-wrap { grid-column: 1; }
  .chart-card--wind-rose #windRoseLegend {
    grid-column: 1;
    flex-direction: row;
    flex-wrap: wrap;
    white-space: normal;
    padding-top: 10px;
  }
}

/* Custom legend for the 90-day/1-year Temperature & Dew Point chart — Chart.js's
   own legend can't represent the high/low band as one swatch distinct from its
   two underlying line datasets, so this replaces it entirely in that mode
   (chartTemp's built-in legend is display:false whenever this is shown). */
.chart-legend-custom {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  padding: 10px 2px 0;
  /* Matches Chart.js's own legend exactly, not the page's Overpass body font
     — Chart.js draws its canvas legend/tick/tooltip text in its own built-in
     default font (never overridden anywhere in this app, see history.js),
     and --chart-tick (not --text-muted) is the color token every other
     chart's ticks/legend already use. */
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-size: 13px;
  color: var(--chart-tick);
}
.chart-legend-custom .legend-item { display: flex; align-items: center; gap: 6px; }
.chart-legend-custom .legend-swatch { width: 14px; height: 3px; border-radius: 2px; }
.chart-legend-custom .legend-swatch-band { height: 10px; background: color-mix(in srgb, var(--chart-temp) 20%, transparent); }
/* Wind rose legend swatches — filled boxes (matching the rose's own wedge
   fills), not the thin line swatch the other custom legends use for line
   series. */
.chart-legend-custom .legend-swatch-box { width: 14px; height: 14px; border-radius: 3px; }
/* Dashed-line swatch (meteogram.js's Gust legend entry) — no border-radius,
   since the repeating-gradient dash pattern is set inline per-instance
   (color varies by theme) and rounded corners would clip the first/last dash. */
.chart-legend-custom .legend-swatch-dashed { border-radius: 0; }



/* ── Forecast card click affordance ─────────────────────── */
.forecast-card {
  cursor: pointer;
  transition: box-shadow 0.15s, transform 0.15s;
}
.forecast-card:hover {
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
  transform: translateY(-1px);
}

/* ── Bottom sheet ───────────────────────────────────────── */
.sheet-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgb(0 0 0 / 0.45);
  z-index: 100;
  animation: fadeIn 0.2s ease;
}
.sheet-backdrop.open { display: block; }

@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

.bottom-sheet {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  max-width: 680px;
  margin: 0 auto;
  background: var(--surface);
  border-radius: 12px 12px 0 0;
  z-index: 101;
  transform: translateY(100%);
  transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
  max-height: 80vh;
  overflow-y: auto;
  padding-bottom: env(safe-area-inset-bottom);
  -webkit-overflow-scrolling: touch;
}
.bottom-sheet.open { transform: translateY(0); }

.sheet-handle {
  width: 36px;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  margin: 12px auto 0;
  cursor: pointer;
  flex-shrink: 0;
}

.sheet-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
}

.sheet-close {
  display: none;
  margin-left: auto;
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 1.1rem;
  line-height: 1;
  padding: 4px 8px;
  cursor: pointer;
  flex-shrink: 0;
}
.sheet-close:hover { color: var(--text); }

.sheet-icon { font-size: 3rem; line-height: 1; }

.sheet-day {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--text);
}

.sheet-cond {
  font-size: 1.05rem;
  color: var(--text-muted);
  margin-top: 2px;
}

.sheet-blurb {
  padding: 14px 20px;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}

/* Own paragraph, not appended to .sheet-blurb — reads as a distinct aside
   ("vs. yesterday"), not a continuation of the sky/wind narrative above it.
   Empty/hidden (not just empty text) when no comparison is available, so it
   doesn't leave a stray padded gap between the blurb and the hourly strip. */
.sheet-compare {
  padding: 12px 20px;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.sheet-compare:empty {
  display: none;
}

.sheet-hourly {
  display: flex;
  overflow-x: auto;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  gap: 4px;
}
.sheet-hourly::-webkit-scrollbar { display: none; }

.hourly-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  min-width: 52px;
  flex-shrink: 0;
}

.hourly-time {
  font-size: 0.85rem;
  color: var(--text-muted);
  font-weight: 500;
  position: relative;
}

.hourly-col.now .hourly-time {
  color: var(--text);
  font-weight: 700;
}

.hourly-col.now .hourly-time::before {
  content: '';
  position: absolute;
  top: -8px;
  left: 50%;
  transform: translateX(-50%);
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--now-dot);
}

.hourly-icon { font-size: 1.4rem; line-height: 1; }

.hourly-temp {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
  font-family: var(--font-mono);
}

.hourly-pop {
  font-size: 0.85rem;
  color: var(--rain);
  font-weight: 500;
  min-height: 1em;
}

.sheet-stat-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto 1rem auto auto;
  column-gap: 16px;
  row-gap: 4px;
  padding: 20px;
}

/* Flatten each stat so its label and value join the grid directly */
.sheet-stat { display: contents; }

.sheet-stat:nth-child(1) .sheet-stat-label { grid-column: 1; grid-row: 1; }
.sheet-stat:nth-child(2) .sheet-stat-label { grid-column: 2; grid-row: 1; }
.sheet-stat:nth-child(1) .sheet-stat-value { grid-column: 1; grid-row: 2; }
.sheet-stat:nth-child(2) .sheet-stat-value { grid-column: 2; grid-row: 2; }
.sheet-stat:nth-child(3) .sheet-stat-label { grid-column: 1; grid-row: 4; }
.sheet-stat:nth-child(4) .sheet-stat-label { grid-column: 2; grid-row: 4; }
.sheet-stat:nth-child(3) .sheet-stat-value { grid-column: 1; grid-row: 5; }
.sheet-stat:nth-child(4) .sheet-stat-value { grid-column: 2; grid-row: 5; }

.sheet-stat-label {
  font-size: 0.85rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
}

/* Sized down from an original 1.15rem — that read heavier than the hourly
   strip's own mono temperatures (.hourly-temp, 1.05rem) just above it, and
   was the one size in the sheet that made Sunrise/Sunset's "AM"/"PM" wrap
   to 2 lines. Applied uniformly to every stat pair, not just that one,
   so the grid doesn't end up with one oddly-smaller value next to the rest. */
.sheet-stat-value {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text);
  font-family: var(--font-mono);
}

.sheet-aqi {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 0 20px 20px;
  border-top: 1px solid var(--border);
  padding-top: 16px;
  margin-top: -8px;
}

.sheet-aqi-label {
  font-size: 0.85rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
}

.sheet-aqi-value {
  font-size: 0.95rem;
  font-weight: 600;
  font-family: var(--font-mono);
}

/* ── Sheet: modal mode on wide screens ──────────────────── */
@media (min-width: 768px) {
  .sheet-handle { display: none; }
  .sheet-close  { display: block; }

  .bottom-sheet {
    top: 50%;
    left: 50%;
    right: auto;
    bottom: auto;
    width: min(580px, 90vw);
    max-width: none;
    margin: 0;
    max-height: 85vh;
    border-radius: 12px;
    transform: translate(-50%, -50%) scale(0.95);
    opacity: 0;
    pointer-events: none;
    transition: transform 0.2s ease, opacity 0.2s ease;
  }

  .bottom-sheet.open {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
    pointer-events: auto;
  }
}

/* ── Range picker ───────────────────────────────────────── */
.range-picker {
  position: relative;
  display: flex;
  gap: 12px;
  margin-bottom: 16px;
}

/* Two rows on phone widths: filters on top, navigation (calendar/back/
   forward/refresh) below — a single row was overflowing past the screen
   edge (refresh button clipped off-screen). Sizing itself (segmented pill,
   uniform nav-button squares) is NOT responsive — same at every width — only
   this row-vs-column wrapping is iOS/phone-specific. */
@media (max-width: 680px) {
  .range-picker {
    flex-direction: column;
    gap: 8px;
  }
}

/* Segmented pill — reuses the exact look of the top-level nav's
   Dashboard/Graphs/Almanac picker (nav .nav-buttons / nav a.active) rather
   than a one-off style here, since both are the same kind of control:
   mutually-exclusive view states, only one active at a time. Equal-width
   segments (flex:1) rather than content-sized, matching that "uniform size"
   look directly. */
.range-picker-filters {
  display: flex;
  gap: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 3px;
}
.range-picker-filters button {
  flex: 1;
  text-align: center;
  background: none;
  border: none;
  border-radius: 3px;
  color: var(--text-muted);
  font-size: 0.9rem;
  font-weight: 500;
  padding: 10px 16px;
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
  white-space: nowrap;
}
.range-picker-filters button:hover { color: var(--text); background: var(--border); }
.range-picker-filters button.active { color: var(--bg); background: var(--accent); font-weight: 600; }
.range-picker-filters button.prefetching { opacity: 0.5; cursor: wait; }
.range-picker-filters button.prefetching::after { content: ' …'; }

/* PROTOTYPE (see conversation, not yet confirmed): longer word labels
   ("3 Months", "2 Days") replace the old abbreviations, split into two
   meaningful groups — Day/2 Days/Week/Month (raw/near-raw data) vs
   3 Months/Year (the daily-summary/trend pipeline) — rather than an
   arbitrary 3+3 split. Side by side at tablet/desktop widths (room for all
   6 comfortably); stacked at phone widths. */
.range-picker-filter-groups { display: flex; gap: 8px; flex: 1; min-width: 0; }
@media (min-width: 681px) {
  .range-picker-filters-long { flex: 0 0 auto; }
  .range-picker-filters-long button { flex: 0 0 auto; padding-left: 14px; padding-right: 14px; }
}
@media (max-width: 680px) {
  .range-picker-filter-groups { flex-direction: column; }
}

/* Independent actions (open date picker / step back-forward / reload), not
   mutually-exclusive states like the filters above — deliberately kept as
   separate buttons rather than merged into one segmented pill, just made
   uniform size (44x44, iOS's own minimum recommended tap target) and grouped
   with a consistent small gap so they read as one distinct cluster. */
.range-picker-nav {
  display: flex;
  gap: 8px;
}

.range-refresh,
.calendar-btn {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-muted);
  cursor: pointer;
  transition: all 0.15s;
}
.range-refresh:hover, .calendar-btn:hover { color: var(--text); border-color: var(--accent-dim); }
.calendar-btn.active { color: var(--accent); border-color: var(--accent); }
.range-refresh:disabled { opacity: 0.4; cursor: wait; }
.calendar-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.range-refresh.spinning svg { animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Meteogram's arbitrary-date picker "back to the default rolling window"
   reset (2026-07-10) — appears only once a start date is picked (see
   updateLatestBtnVisibility() in js/meteogram.js). Text pill rather than a
   third icon square: unlike History's Back/Forward/Refresh, this one has no
   obvious glyph and reads better labeled. */
.hist-latest-btn {
  height: 44px;
  padding: 0 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: 4px;
  color: var(--accent);
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s;
}
.hist-latest-btn:hover { background: var(--accent); color: var(--surface); }

.calendar-popover {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 20;
  /* Was 240px/2px gaps — day cells worked out to ~29x27px, well under the
     ~44px minimum touch target most mobile guidelines recommend. Widened
     the popover and gaps together so cells land close to that instead. */
  width: 320px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  padding: 12px;
}
.calendar-popover.hidden { display: none; }

.calendar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  color: var(--text);
  font-size: 0.9rem;
  font-weight: 600;
}
.calendar-nav {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 1.3rem;
  line-height: 1;
  padding: 8px 12px;
  cursor: pointer;
  border-radius: 4px;
}
.calendar-nav:hover { color: var(--text); background: var(--border); }

.calendar-nav-group { display: flex; }

.calendar-weekdays {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  text-align: center;
  color: var(--text-muted);
  font-size: 0.72rem;
  margin-bottom: 6px;
}

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px;
}

.calendar-day {
  min-width: 0;
  min-height: 40px;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  /* Real border at rest now, not just on hover — reads as a distinct tile
     per day rather than a bare number floating in the grid. */
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  font-size: 0.95rem;
  padding: 8px 0;
  cursor: pointer;
  transition: all 0.15s;
}
.calendar-day:hover:not(:disabled) { border-color: var(--accent-dim); }
.calendar-day.today { font-weight: 700; color: var(--accent); }
/* border-color explicit here too, not just fill — the rest-state border is a
   real var(--border) gray now (not transparent like before), which would
   otherwise show as a mismatched muted ring around the accent fill. */
.calendar-day.selected { background: var(--accent); border-color: var(--accent); color: var(--surface); font-weight: 600; }
.calendar-day.unavailable { color: var(--border); cursor: not-allowed; }

/* Multi-day highlight (Meteogram's arbitrary-date picker, 2026-07-10) — a
   contiguous run of highlighted days, not History's single .selected day.
   Mocked as an interactive Artifact and approved before this was built (see
   project_meteogram memory): every highlighted day gets the same solid-fill
   treatment .selected already uses, but rounding only applies at each run's
   true ends (seg-start/seg-end) — a day between two other highlighted days
   stays square on the sides touching its neighbors, so the run reads as one
   continuous bar rather than a string of separate rounded chips. */
.calendar-day.in-range {
  position: relative;
  background: var(--accent);
  border-color: var(--accent);
  color: var(--surface);
  font-weight: 600;
}
.calendar-day.in-range.seg-start { border-top-left-radius: 10px; border-bottom-left-radius: 10px; }
.calendar-day.in-range.seg-end { border-top-right-radius: 10px; border-bottom-right-radius: 10px; }
.calendar-day.in-range:not(.seg-start) { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.calendar-day.in-range:not(.seg-end) { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.calendar-day:hover:not(:disabled).in-range { border-color: var(--accent); }
/* Bridges .calendar-grid's own 4px gap between two consecutive in-range
   cells in the same row, so the fill reads as unbroken rather than a visible
   seam every day. Sits behind the day-number text (z-index 1 vs 2). */
.calendar-day.in-range.bridge-right::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  right: -4px;
  width: 8px;
  background: var(--accent);
  z-index: 1;
}
.calendar-day span.daynum { position: relative; z-index: 2; }

.calendar-mode-toggle-wrap {
  margin-bottom: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
.calendar-mode-caption {
  font-size: 0.68rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 6px;
}
.calendar-mode-toggle {
  display: flex;
  gap: 4px;
}
/* Second row for 90d/1y start-date picking (2026-07-10) — its own row
   rather than 5 buttons crammed into one (the popover is a fixed 320px),
   mirroring the range-picker's own short/long grouping just outside the
   popover for the same reason: a visibly different pipeline, not just
   "more of the same buttons." */
.calendar-mode-toggle-long {
  margin-top: 4px;
}
.calendar-mode-toggle button {
  flex: 1;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-muted);
  font-size: 0.82rem;
  padding: 9px 0;
  cursor: pointer;
  transition: all 0.15s;
}
.calendar-mode-toggle button:hover { color: var(--text); border-color: var(--accent-dim); }
.calendar-mode-toggle button.active { color: var(--accent); border-color: var(--accent); font-weight: 600; }
.calendar-mode-hint {
  font-size: 0.7rem;
  color: var(--text-muted);
  margin-top: 6px;
}

/* Meteogram's date-picker popover has no day/week/month mode-toggle (span
   length comes from the range pill outside the popover, not a choice made
   inside it) — this sits where .calendar-mode-toggle-wrap does on History's
   popover, same bottom-border/spacing, just a single hint line. */
.calendar-hint {
  font-size: 0.76rem;
  color: var(--text-muted);
  line-height: 1.4;
  margin: 0 0 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
.calendar-hint strong { color: var(--text); }

.history-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--accent);
  color: var(--bg);
  border-radius: 6px;
  padding: 10px 14px;
  margin-bottom: 16px;
  font-size: 0.9rem;
  font-weight: 600;
}
.history-banner button {
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--bg);
  border-radius: 4px;
  color: var(--bg);
  font-size: 0.75rem;
  font-weight: 600;
  padding: 4px 10px;
  cursor: pointer;
  white-space: nowrap;
}
.history-banner button:hover { background: rgba(0, 0, 0, 0.12); }

.hidden { display: none; }

/* ── Tooltips ───────────────────────────────────────────── */
.tooltip-target {
  position: relative;
  cursor: help;
  /* Was `1px dotted var(--text-muted)` — same gray/weight as .alm-section's
     solid cell borders, too hard to tell apart at a glance/small screen.
     Accent color differentiates it from a structural border; 1.5px width
     is a middle ground (2px read as heavier than this app's otherwise
     1px-hairline vocabulary, 1px dots nearly read as a solid line). */
  border-bottom: 1.5px dotted var(--accent-dim);
}

.tooltip-target::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  /* --tooltip-shift (default 0) is set by positionTooltip() in js/widgets.js right
     before the bubble shows, nudging it back on-screen when the trigger sits close
     enough to a screen edge that plain centering would push it off — see that
     function's comment for why this replaced a per-element CSS override. */
  transform: translateX(calc(-50% + var(--tooltip-shift, 0px)));
  background: var(--chart-tooltip-bg);
  color: var(--chart-tooltip-text);
  font-size: 0.9rem;
  font-family: var(--font-sans);
  /* Not nowrap: some tooltips are long ("Hi at 5:55 PM · Lo at 4:05 AM") and, being
     centered via transform, demand room symmetrically on both sides of a tiny trigger
     element — on a narrow phone tile that's wider than the tile itself, and even though
     invisible (opacity:0) until tapped, it still counts toward the trigger's layout width,
     which forced the whole page to render zoomed-out on mobile. Capped + wrapping instead;
     it's a rarely-shown bubble, wrapping to 2 lines there costs nothing visually important.
     width: max-content is required alongside max-width — with width left at its default
     auto, an absolutely-positioned box with only `left` set shrinks-to-fit against its
     (tiny, ~10-44px) trigger element rather than the max-width cap, collapsing multi-word
     tooltips into a tall one-word-per-line column. max-content sizes from the content's
     own natural width instead, still clamped by max-width, so the overflow fix above still
     holds. */
  max-width: 180px;
  width: max-content;
  /* pre-line, not normal: identical wrapping behavior for every existing tooltip
     (none contain a literal newline), but honors an explicit \n where one is
     added deliberately (e.g. the Year cell's all-time comparison line below)
     instead of leaving that break to wherever the text happens to wrap. */
  white-space: pre-line;
  text-align: center;
  padding: 4px 8px;
  border-radius: 4px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 20;
}

.tooltip-target:hover::after,
.tooltip-target.tooltip-visible::after {
  opacity: 1;
}

/* ── Almanac page ───────────────────────────────────────── */
.alm-toolbar {
  max-width: 640px;
  margin: 0 auto;
}

.date-picker {
  position: relative;
  display: inline-block;
  margin-bottom: 16px;
}

.almanac-empty {
  color: var(--text-muted);
  font-size: 0.9rem;
  padding: 20px 0;
}

.almanac-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  box-shadow: var(--shadow);
  max-width: 640px;
  margin: 0 auto;
}

.almanac-card.hidden { display: none; }

.alm-header {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.alm-date-full {
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--text);
  font-family: var(--font-mono);
  letter-spacing: var(--value-letter-spacing);
}

.alm-date-sub {
  display: flex;
  gap: 6px;
  font-size: 0.8rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

/* Single flat 3x3 grid (2026-07-06) — superseded the old 2x2-left +
   flex-column-right split. That split existed specifically because
   Astronomy was drastically taller than its row-mates (moon phase + 3
   secondary rows vs. neighbors' 1-3), and CSS Grid's default row-stretch
   would've forced the shorter cells to match it with a lot of wasted empty
   space. Once Astronomy split into Sun/Moon and every cell gained real
   content of its own (Solar Noon, moon age, Monthly/Yearly rain totals),
   all cells became close enough in height that a uniform grid's stretch
   behavior no longer creates a visible mismatch — see project_almanac_page
   memory for the full history. 2026-07-07: a 9th cell ("Year to Date") filled
   the grid's last remaining blank slot, so it's now a full 3x3 with no empty
   cells left. */
.alm-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-top: 14px;
}

@media (max-width: 680px) {
  .alm-grid { grid-template-columns: repeat(2, 1fr); }
}

/* No further collapse to 1 column below 480px (removed 2026-07-05) — a single
   full-width column made each .secondary row's label/value pair (space-between
   across the whole card width) stretch apart with an ugly gap in the middle.
   Staying at 2 columns keeps cells close to the width they're already tuned for
   at the 680px breakpoint above. This used to pair with a `justify-content:
   flex-start` override on `.almanac-card .secondary` below 480px — removed
   2026-07-07, see that rule's own former location for why. */

.alm-section {
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* Grid items default to min-width:auto, which refuses to shrink below their content's
     natural width — at phone-width 2-column (see the 480px override above), that made
     the whole page overflow horizontally instead of the grid tracks actually sharing the
     available width. Standard fix, safe at every other breakpoint too. */
  min-width: 0;
  /* Same border/radius/shadow recipe as the dashboard's .tile, but no background swap —
     .almanac-card is already var(--surface), so giving each cell that same color keeps
     the "one card, one day" feel while the border still demarcates each stat's area. */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
  box-shadow: var(--shadow);
}

.alm-weather-heads {
  display: flex;
  justify-content: space-between;
  gap: 8px;
}

.alm-weather-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 2px;
  margin-bottom: 4px;
}

/* Was 2rem — noticeably smaller than the other cells' graphics (60px
   compass, 73x64px UV pyramid, 2.2rem moon glyph). Bumped to be in the
   same visual weight class as those, not the dashboard's full 4rem (that
   size was tuned for the wider Conditions sidebar, a different context). */
.alm-weather-row .cond-icon { font-size: 3rem; }

.alm-temp-avg-group {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Classifies the day's Avg Temp using the same tempLevel() bands as the dashboard's
   thermometer glyph — a plain dot rather than the thermometer shape itself, since a
   thermometer reads as "live instrument reading," which a day's average isn't. */
.temp-level-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
  display: none;
}

.temp-level-dot.visible {
  display: inline-block;
}

.temp-level-dot.tooltip-target {
  border-bottom: none;
}

.dew-wave-wrap {
  display: block;
  flex-shrink: 0;
}

.dew-wave-wrap.tooltip-target {
  border-bottom: none;
}

.almanac-card .cond-desc {
  text-align: left;
  font-size: 0.85rem;
}

.almanac-card .cond-source {
  margin-top: 16px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.almanac-card .value {
  font-size: 1.8rem;
  font-weight: var(--value-weight);
  color: var(--text);
  line-height: 1.1;
  font-family: var(--font-mono);
  letter-spacing: var(--value-letter-spacing);
}

/* Vertical pressure gauge: fills bottom-to-top as level rises (Low → Normal →
   High) — column-reverse so the first dot in DOM order (the one
   renderPressureDots lights first) ends up at the bottom, matching the
   "rising column" metaphor. Shared by the almanac's Pressure cell and the
   dashboard's Pressure tile; position offsets below are per-context (see
   each), since the two cells are different widths. */
.pressure-gauge {
  display: flex;
  flex-direction: column-reverse;
  gap: 2px;
  flex-shrink: 0;
}

/* Same override as every other icon carrying a tooltip (.heat-dot,
   .uv-pyramid-wrap, etc.) — the dotted-underline convention is for text,
   not a graphic. */
.pressure-gauge.tooltip-target {
  border-bottom: none;
}

/* Segmented bar, not circular dots — reads as one gauge filling up (like a
   signal-strength/battery indicator) rather than 3 separate status lights,
   which is what plain dots-with-gaps looked like. */
.pressure-gauge .pressure-dot {
  width: 16px;
  height: 9px;
  border-radius: 3px;
}

/* Horizontal centering is handled by the shared .icon-slot wrapper now — this is
   just a leftover vertical nudge specific to how the gauge's own dots sit relative
   to the almanac's value text baseline, unrelated to the outer slot. */
.almanac-card .pressure-gauge {
  transform: translateY(-4px);
}

/* Same leftover-nudge pattern as .pressure-gauge above — sat visibly low in
   its slot at the smaller 36px size this page uses (see project_moon_phase_glyph
   memory), unrelated to the icon-slot's own centering. */
.almanac-card .moon-glyph {
  transform: translateY(-4px);
}

.almanac-card .secondaries {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
}

.almanac-card .secondary {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
}

/* A `justify-content: flex-start` override used to apply here below 480px — it dates
   from when the grid collapsed to 1 full-width column at that breakpoint (removed
   2026-07-05), and was kept afterward on a "reads better regardless" judgment call.
   Removed 2026-07-07: with a fixed gap instead of space-between, each row's value
   lands at (that row's own label width + 10px) — fine when every label in a cell is
   the same length, but "Max"/"Min" aren't quite identical, so their values landed at
   visibly different distances from the cell's edge (confirmed: a consistent ~9px
   discrepancy at every phone width). That's the real bug the user was seeing as
   "portrait looks misaligned, landscape doesn't" — landscape's wider viewport never
   dropped below 480px, so it never hit this override and stayed reliably
   space-between the whole time. Reverting to space-between at every width, matching
   landscape, fixes it — verified this doesn't reintroduce the original "ugly gap"
   problem, since the grid staying 2-column (not 1) already solved that separately. */

/* Max Solar (UV cell) / Solar Noon (Sun cell) only. Their values ("832 W/m²",
   "12:59 PM") are long enough that the browser wraps them mid-value at every phone
   width (confirmed via Playwright: still wrapping up to ~428-450px, not one specific
   device) — and staying right-aligned mid-wrap reads oddly, the second line
   indented against nothing. Stack into two lines instead — label above, value
   below, both left-justified — only below 480px; above that the value fits on one
   line next to its label like every other secondary row, unaffected. Selector is
   `.almanac-card .secondary-stack-narrow`, not bare `.secondary-stack-narrow` —
   the base `.almanac-card .secondary` rule above (2 classes) otherwise beats a
   1-class selector on specificity regardless of source order, so `gap: 2px` here
   was silently losing to that rule's `gap: 8px` (caught via direct pixel
   measurement: label-to-value gap rendered at 8px, not 2px, after this shipped). */
@media (max-width: 480px) {
  .almanac-card .secondary-stack-narrow {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 2px;
  }
}

.almanac-card .secondary-label {
  font-size: 0.75rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  white-space: nowrap;
}

.almanac-card .secondary-value {
  font-size: 0.9rem;
  font-weight: 400;
  color: var(--text);
}

.compass-mini {
  width: 60px;
  height: 60px;
  /* The "N" label is large enough to poke slightly above the 0-100 viewBox —
     let it render there instead of clipping at the SVG's own edge. */
  overflow: visible;
}

/* Compensate for the smaller physical scale so the lone "N" stays legible. */
.compass-mini .compass-label {
  font-size: 19px;
}

/* Dashboard-only: sized up a touch from the base 60px since the icon-slot row
   is governed by the label/value stack's own ~64px height anyway (unlike
   almanac's absolutely-positioned version, which can't borrow this same
   headroom for free) — .wind-compass's leftover margin (10px auto 2px, a
   remnant of its old standalone-below-secondaries layout) is also zeroed
   here since icon-slot's own flex centering replaces it. Net effect: bigger
   icon, shorter tile than before this rule existed, not taller. */
.icon-slot .compass-mini {
  width: 66px;
  height: 66px;
  margin: 0;
}

/* Wraps just the Wind section's label + value (not its secondaries) so the
   compass can be positioned absolutely against them without the compass's
   own (taller) box forcing extra height into the row — see conversation:
   flex-centering the compass against the value left ~20px of dead
   whitespace above "1 mph" that Temperature/Pressure didn't have. */
.alm-wind-head {
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: relative;
  min-height: 60px; /* reserve room for the absolutely-positioned compass so it can't overlap .secondaries below */
}

/* Positioning lives on the wrapper div now, not .compass-mini itself — added so the
   compass can carry a tap tooltip (numeric heading), and ::after/tooltip bubbles
   never render on <svg> elements directly (see .rain-drop-wrap.tooltip-target's
   comment for the full explanation). .compass-mini's own width/height/overflow
   rules above are unaffected, still sized on the SVG. */
.alm-wind-head .compass-mini-wrap {
  position: absolute;
  top: -3px; /* aligns the ring's center with the value text's center (measured) */
  right: 0;
}

.compass-mini-wrap.tooltip-target {
  border-bottom: none;
}

/* ── Meteogram (meteogram.html) ──────────────────────────────────────────── */

/* Forecast/Historical mode pill + Stage 3's range pill, side by side
   (2026-07-10) — mocked as an interactive Artifact and approved before this
   was built. Both pills keep their own fit-content width (below); this row
   just places them next to each other with a gap. Stacks to a column below
   680px — the exact same breakpoint/behavior the real .range-picker
   (History page) already uses, not a new one, so a narrow phone gets two
   full-width-feeling stacked pills instead of two illegibly-squeezed ones
   sharing a row. */
.mode-range-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}
@media (max-width: 680px) {
  .mode-range-row { flex-direction: column; }
}

/* Stage 3 range toggle — reuses .range-picker-filters' pill look (History
   page) directly, but standalone here (not nested in .range-picker-filter-
   groups like History's two-group layout), so it needs its own width/spacing
   rather than stretching to the full card width like a bare flex block would.
   #meteoModeToggle shares this rule (both are plain fit-content pills); their
   shared margin-bottom moved to .mode-range-row above so it isn't doubled
   between the two nested pills. */
#meteoRangeToggle, #meteoModeToggle {
  width: fit-content;
}

/* Anchors #meteoCalPopover (position:absolute) under the calendar button
   specifically, not the whole .mode-range-row — same relationship
   .range-picker's own position:relative has to #calendarPopover on History. */
#meteoCalNav {
  position: relative;
}

/* Dense hourly weather-icon strip (js/meteogram.js buildIconRow) — one column
   per sampled hour, same flex-shrink pattern as .wind-row below so the two
   rows' columns line up under the same x-positions at any viewport width. */
.icon-row-dense {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  padding: 2px 2px 0;
}
.icon-col {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.icon-col .emoji {
  font-size: 1.6rem;
  line-height: 1;
}
.icon-col .t {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--chart-tick);
}
/* Historical mode only, while the archive-icon fetch is still in flight
   (js/meteogram.js's isHistArchivePending()) — a dim placeholder glyph so
   the row holds its real height from the first paint instead of popping in
   (and shoving the chart below it down) once the fetch resolves. */
.icon-pending {
  color: var(--border);
}

/* Cloud-cover strip — compact grey-scale band (Stage 1 addition), one
   column per sampled hour, same cadence/alignment as the icon row above it.
   Deliberately not a full line/area chart: professional meteogram cloud
   panels (NWS/ECMWF convention) are conventionally rendered as grey-scale
   bands, not curves — this is a compact single-value adaptation (total
   cloud % only, no low/mid/high layer breakdown). */
.cloud-strip-label {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--chart-tick);
  margin: 8px 0 4px;
}
.cloud-strip {
  display: flex;
  gap: 2px;
  margin: 0 0 2px;
}
.cloud-col {
  flex: 1 0 0;
  min-width: 0;
}
.cloud-col .seg {
  width: 100%;
  height: 26px;
  border-radius: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Same reserved-space treatment as .icon-pending above, same reason — no
   text inside, so a flat muted fill (not a color-mix cloud-shade) is enough
   to read as "loading," distinct from any real cloud percentage. */
.seg.cloud-pending {
  background: var(--border);
}
/* No pill — a fixed text color was measured to fail WCAG's large-text
   minimum in dark mode at high cloud% (2.29:1 vs. the 3:1 floor), so
   js/meteogram.js computes each segment's actual blended color and sets
   `color` inline per segment (near-black or near-white), the standard
   auto-contrast-label technique. This rule only supplies what's genuinely
   shared across every segment. Shading itself must be done via
   background-color (color-mix) on .seg, never the `opacity` CSS property —
   opacity would fade this text along with it, undoing the per-segment fix. */
.cloud-col .pct {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 1;
}

/* Historical mode's icon/cloud provenance disclosure (js/meteogram.js
   updateIconDisclosure) — hidden by default (Forecast mode, or Historical
   before the archive fetch resolves/if it fails), shown via .visible.
   Deliberately plain small muted text, not a warning-colored callout — this
   is a routine data-source fact, not an error state. */
.icon-disclosure {
  display: none;
  font-size: 0.72rem;
  color: var(--text-muted);
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--border);
}
/* No prior inline-text link exists anywhere in this app to match (every
   other <a> is a nav button) — --accent-dim chosen deliberately over a
   default browser blue, same token already used for this app's tooltip-
   underline convention, so this reads as "this app's own quiet interactive
   affordance," not an unstyled default. */
.icon-disclosure a {
  color: var(--accent-dim);
  text-decoration: underline;
}
.icon-disclosure a:hover {
  color: var(--accent);
}
.icon-disclosure.visible {
  display: block;
}

/* Same quiet-inline-link treatment as .icon-disclosure a above, generalized
   for the app-wide Open-Meteo CC BY 4.0 attribution links added 2026-07-10
   (Dashboard/Meteogram forecast-source status lines, Almanac's historical
   sky-icon source line) — inherits whatever font-size/color context it's
   placed in, only overrides color + underline. */
.attribution-link {
  color: var(--accent-dim);
  text-decoration: underline;
}
.attribution-link:hover {
  color: var(--accent);
}

/* Wind arrow row (js/meteogram.js buildWindArrowRow) — one column per sampled
   hour, horizontal scroll as a last resort at very narrow widths rather than
   letting arrows overlap/become illegible. */
.wind-row {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 64px;
  padding: 4px 2px 0;
  overflow-x: auto;
}
.wind-col {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 3px;
}
.wind-col svg { display: block; }
.wind-col .spd {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--text-muted);
  white-space: nowrap;
}
.wind-col .t {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--chart-tick);
}

/* Below iPad width, 16 columns of "HH:00" text overlaps into illegibility
   (confirmed via a real 375px screenshot) — the chart directly below each row
   already has its own auto-thinning time axis, so the per-column label here
   is redundant at that width, not the only time reference. Icons/arrows
   themselves stay untouched — this is the "sparser but not broken" line from
   the primary iPad target's phone/secondary check, not a phone-optimized
   redesign. */
@media (max-width: 480px) {
  .icon-col .t,
  .wind-col .t {
    display: none;
  }
}

/* Stage 3 range toggle: 24h/48h/72h all share the base .icon-col/.cloud-col/
   .wind-col rules above by default (fluid flex columns, exactly like before
   this feature existed) — whether any given range needs to switch out of
   that is fully width-adaptive (see js/meteogram.js's
   iconColumnWidthAvailable() check), not tied to a specific range. When it
   does, js/meteogram.js's updateScrollLayout() adds .meteo-scroll-active and
   an explicit pixel width on .meteo-scroll-inner; these overrides switch the
   same columns to a fixed width so more of them can exist off-screen instead
   of being squeezed into the same space. */
.meteo-scroll.meteo-scroll-active {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Without this, iOS has to disambiguate horizontal-panel-scroll vs.
     vertical-page-scroll on every gesture start, which can read as a brief
     hesitation/stutter right as a drag begins. pan-x tells it upfront this
     element only scrolls horizontally. */
  touch-action: pan-x;
}
.meteo-scroll.meteo-scroll-active::-webkit-scrollbar { display: none; }
.meteo-scroll-active .icon-col,
.meteo-scroll-active .cloud-col,
.meteo-scroll-active .wind-col {
  flex: none;
  /* Set per-render by updateScrollLayout() (js/meteogram.js) — plotWidth
     divided by the real column count (including the +1 endpoint column),
     not a flat SAMPLE_INTERVAL_HOURS*SCROLL_HOUR_PX constant, so the row
     always exactly fills plotWidth with no overflow into the axis gutter.
     45px fallback only matters before the first render sets the var. */
  width: var(--scroll-col-width, 45px);
}
/* A real bug, caught live: .icon-row-dense/.cloud-strip/.wind-row's own
   `gap` (2px, 2px, 4px respectively) is harmless in fluid mode — flex:1
   columns there just divide up "whatever's left after gaps," always
   exactly filling the row regardless of gap size. Fixed-width columns
   don't have that self-correcting property: gaps add ON TOP of the fixed
   per-column total instead of being absorbed, so a row's gap-inflated
   content no longer matches meteo-scroll-inner's own JS-computed width
   (which has no gap term at all), overflowing further past the chart's
   right edge the more columns there are — worst, and most visible, at 72h.
   Zeroing gap here removes gap as a variable entirely rather than teaching
   the JS width formula three different per-row gap values to track
   (fragile against a future CSS tweak) — columns are wide enough (45px) to
   not need a separate `gap` for breathing room; their own centered content
   not filling the full column width does that job instead (see the 45px
   width rule and its own comment above). Fluid mode (24h/48h) keeps its
   gap, untouched. */
.meteo-scroll-active .icon-row-dense,
.meteo-scroll-active .cloud-strip,
.meteo-scroll-active .wind-row {
  gap: 0;
}
/* .wind-row's own overflow-x:auto (above) is a narrow-viewport fallback for
   the *unscaled* 24h/48h case — inert here since .meteo-scroll-inner's
   explicit width already matches its fixed-width children exactly, so it
   never has anything of its own to scroll; the outer .meteo-scroll is what
   actually scrolls in this mode. */
