/* Notum Storm Guard monitor — dark ops theme (single deliberate dark mode).
   Palette per dataviz reference: surfaces #0d0d0d/#1a1a19, ink #fff/#c3c2b7/#898781,
   series dark steps: blue #3987e5, aqua #199e70, yellow #c98500, violet #9085e9,
   red #e66767; status: good #0ca30c, warning #fab219, serious #ec835a, critical #d03b3b. */

:root {
  color-scheme: dark;
  --page: #0d0d0d;
  --surface: #1a1a19;
  --surface-2: #232322;
  --ink: #ffffff;
  --ink-2: #c3c2b7;
  --muted: #898781;
  --grid: #2c2c2a;
  --baseline: #383835;
  --ring: rgba(255, 255, 255, 0.10);
  --s-blue: #3987e5;
  --s-aqua: #199e70;
  --s-yellow: #c98500;
  --s-violet: #9085e9;
  --s-red: #e66767;
  --good: #0ca30c;
  --warning: #fab219;
  --serious: #ec835a;
  --critical: #d03b3b;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--page); color: var(--ink); }
body { font-size: 14px; line-height: 1.45; }

/* Fluid rather than a single 640 px step: the old jump from 17 px to 13 px
   left every width in between served by whichever side of the breakpoint it
   fell on. clamp covers the range continuously and the letter-spacing rides
   along, because 2.5px of tracking on a 13px word is what made the small
   size look cramped. */
h1 { font-size: clamp(13px, 1.15vw + 9.5px, 17px);
     letter-spacing: clamp(1.2px, .4vw + .1px, 2.5px);
     margin: 0; font-weight: 700; }
h2 { font-size: 13px; letter-spacing: 1.2px; margin: 0; font-weight: 600; text-transform: uppercase; color: var(--ink-2); }
.hint { color: var(--muted); font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 12px; }
.sub { color: var(--muted); font-size: 12px; }
.center { text-align: center; }

/* ---------- header ---------- */
#topbar {
  display: flex; align-items: center; gap: clamp(10px, 2.5vw, 20px);
  padding: 10px clamp(10px, 2.5vw, 18px);
  border-bottom: 1px solid var(--grid);
  position: sticky; top: 0; background: rgba(13, 13, 13, 0.92);
  backdrop-filter: blur(6px); z-index: 1200;
}
.brand { display: flex; align-items: center; gap: 12px; flex: 1; }
.bolt { font-size: 26px; filter: drop-shadow(0 0 6px rgba(201, 133, 0, .6)); }
.badge {
  display: inline-block; padding: 1px 8px; border-radius: 4px;
  background: var(--surface-2); border: 1px solid var(--ring);
  font-size: 11px; letter-spacing: 1px; color: var(--ink-2);
}
.badge.live { color: var(--good); border-color: var(--good); }
.badge.synthetic { color: var(--warning); border-color: var(--warning); }

.svc { display: flex; flex-direction: column; align-items: center; gap: 3px; font-size: 10px; color: var(--muted); letter-spacing: 1px; }
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--muted); }
.dot.ok { background: var(--good); box-shadow: 0 0 6px rgba(12, 163, 12, .7); }
.dot.warn { background: var(--warning); }
.dot.bad { background: var(--critical); box-shadow: 0 0 6px rgba(208, 59, 59, .7); }
.topbar-api {
  color: var(--muted); text-decoration: none; font-size: 12px;
  letter-spacing: 1.2px; font-weight: 600; padding: 4px 11px;
  border: 1px solid var(--ring); border-radius: 6px;
  transition: color .15s, border-color .15s;
}
.topbar-api:hover { color: var(--s-blue); border-color: var(--s-blue); }
.footer-api { color: var(--s-blue); text-decoration: none; margin-left: 6px; }
.footer-api:hover { text-decoration: underline; }
.clock { text-align: right; }
#clock-utc { font-size: 19px; font-variant-numeric: tabular-nums; letter-spacing: 1px; }

/* ---------- layout ---------- */
/* two auto-packing columns: cards stack flush, so a short neighbour can
   never open a hole (the old grid-template-areas pinned rows and left gaps
   whenever card heights diverged) */
/* main is now a VERTICAL stack of tiers (answer, then map + evidence, then
   engine room). .cols is the horizontal pair that main used to be. */
main {
  display: flex; flex-direction: column; gap: 14px;
  padding: 14px 18px 4px; max-width: 1700px; margin: 0 auto;
}
.cols { display: flex; gap: 14px; align-items: flex-start; }
.col { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
#col-a { flex: 7; }
#col-b { flex: 5; }
#map-card { display: flex; flex-direction: column; }
.cols.engine {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: start;
}
@media (max-width: 1080px) {
  .cols { flex-direction: column; }
  .col { width: 100%; }
  .cols.engine { grid-template-columns: 1fr; }
}

/* ---------- sources & ensemble ---------- */
.sources-row { display: flex; gap: 8px; flex-wrap: wrap; margin: 4px 0 10px; }
.src-chip {
  display: flex; align-items: center; gap: 6px; padding: 3px 9px;
  background: var(--surface-2); border: 1px solid var(--ring); border-radius: 12px;
  font-size: 11px; color: var(--ink-2); font-variant-numeric: tabular-nums;
}
.src-chip .dot { width: 7px; height: 7px; }
.degraded-banner {
  background: rgba(236, 131, 90, 0.12); border: 1px solid var(--serious);
  color: var(--serious); border-radius: 6px; padding: 6px 10px;
  font-size: 12px; margin-bottom: 8px;
}
/* ---- ensemble member rows (2026-08-01 streamline) ---- */
#ensemble-members { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 8px; }
.member { display: flex; align-items: center; gap: 10px; font-size: 12px;
  padding: 6px 10px; background: var(--surface-2); border: 1px solid var(--ring);
  border-radius: 7px; cursor: help; }
.member .m-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--baseline); flex: none; }
.member.on .m-dot { background: var(--good); box-shadow: 0 0 6px rgba(12,163,12,.7); }
.member.warn .m-dot { background: var(--warning); }
.member .m-name { font-weight: 700; letter-spacing: .8px; width: 38px; flex: none; }
.member .m-bar { flex: 1; height: 7px; background: var(--surface); border-radius: 4px;
  overflow: hidden; border: 1px solid var(--grid); }
.member .m-bar span { display: block; height: 100%; background: var(--s-blue);
  border-radius: 4px; transition: width .3s; }
.member.on .m-bar span { background: linear-gradient(90deg, var(--s-blue), var(--s-aqua)); }
.member .m-pct { width: 38px; text-align: right; font-variant-numeric: tabular-nums; flex: none; }
.member .m-state { width: 110px; text-align: right; color: var(--muted); font-size: 11px; flex: none; }
.member:not(.on) .m-name { color: var(--muted); font-weight: 500; }

.card {
  background: var(--surface); border: 1px solid var(--ring); border-radius: 10px;
  padding: 12px 14px; min-width: 0;
}
.card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }

/* ---------- map ----------
   The map container is a SQUARE, always: height derives from width via
   aspect-ratio (flex: none so neighbouring column growth can never stretch
   it - late-loading tables used to expand the flexed map and shift the
   layout). Leaflet is re-measured by the existing ResizeObserver. */
#map {
  flex: none; width: 100%; aspect-ratio: 1 / 1; min-height: 0;
  border-radius: 8px; background: #0a0d12; outline: 1px solid var(--grid);
}
#map-card { align-self: start; }
.leaflet-container { background: #0a0d12; font: inherit; }
.bin-select { display: flex; gap: 4px; flex-wrap: wrap; }

/* layer toggle pills — second row under the lead-time selector */
.layer-toggles { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 0 10px; }
.layer-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 12px; border-radius: 999px; font-size: 12px; cursor: pointer;
  background: var(--surface-2); color: var(--ink-2);
  border: 1px solid var(--ring);
  transition: border-color .15s, background .15s, color .15s, box-shadow .15s;
}
.layer-btn::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--baseline); transition: background .15s, box-shadow .15s;
}
.layer-btn:hover { border-color: var(--s-blue); color: var(--ink); }
.layer-btn.on {
  background: rgba(57, 135, 229, 0.16); border-color: var(--s-blue); color: var(--ink);
}
.layer-btn.on::before {
  background: var(--s-blue); box-shadow: 0 0 6px rgba(57, 135, 229, 0.9);
}
.vg-block { opacity: 0.85; font-size: 11px; margin-top: 2px; }
/* leads that have not passed the clean skill gate: visually provisional */
/* ---- Storm predictor (7-day outlook tier) + map product tabs ---- */
.map-tabs { display: flex; border: 1px solid var(--ring); border-radius: 9px;
  overflow: hidden; }
.map-tab { padding: 7px 14px; background: var(--surface-2); color: var(--muted);
  border: none; cursor: pointer; font-family: inherit; font-size: 12.5px;
  font-weight: 600; transition: background .15s, color .15s; }
.map-tab + .map-tab { border-left: 1px solid var(--ring); }
#map-tab-now.on { background: #1c2733; color: #7fc4ff; }
#map-tab-sp.on { background: #2a2416; color: #ffd76b; }
.map-tab:hover:not(.on) { color: var(--txt, #dfe5ea); }
.sp-tag { font-size: 9.5px; text-transform: uppercase; letter-spacing: .07em;
  opacity: .75; margin-left: 6px; }
#nowcast-controls .bin-select { margin-bottom: 6px; }
/* lead slider (nowcast windows as one control; shared by map + city modal) */
/* double-class specificity: .bin-select and .cm-bins are flex rows for the
   old button layout - the slider's children must stack regardless of order */
.lead-wrap.lead-wrap { position: relative; margin: 4px 0 6px; display: block; }
/* Sliders are the primary control on this page and the main thing a thumb
   has to hit. touch-action: none stops the browser claiming a horizontal
   drag as a page scroll, which on mobile made the slider feel like it
   ignored you; the vertical padding gives a ~40px grab band without making
   the coloured track itself any taller. */
.lead-slider { -webkit-appearance: none; appearance: none; width: 100%;
  height: 14px; border-radius: 7px; outline: none; cursor: pointer;
  border: 1px solid rgba(255,255,255,0.12); background: #232322;
  touch-action: none; padding: 0; margin: 0;
  background-clip: padding-box; }
.lead-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
  width: 22px; height: 22px; border-radius: 50%; background: #fff;
  border: 2px solid var(--s-blue); box-shadow: 0 1px 5px rgba(0,0,0,0.6);
  cursor: grab; }
.lead-slider:active::-webkit-slider-thumb { cursor: grabbing;
  box-shadow: 0 0 0 6px rgba(57,135,229,0.22), 0 1px 5px rgba(0,0,0,0.6); }
.lead-slider::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%;
  background: #fff; border: 2px solid var(--s-blue); cursor: grab; }
.lead-slider:focus-visible { outline: 2px solid var(--s-blue);
  outline-offset: 3px; }
/* the invisible grab band: a taller hit area around every slider without
   changing how any of them look */
.lead-wrap, .sun-slider-wrap, .sp-panel { position: relative; }
.lead-wrap .lead-slider, .sun-slider-wrap #sun-slider, .sp-panel #sp-slider {
  position: relative; z-index: 1;
}
.lead-wrap::before, .sun-slider-wrap::before {
  content: ""; position: absolute; left: 0; right: 0;
  top: -13px; height: 40px; z-index: 0;
}
.lead-ticks { position: relative; height: 13px; font-size: 9.5px;
  color: var(--muted); margin-top: 2px; }
.lead-ticks span { position: absolute; transform: translateX(-50%); }
.lead-ticks span:first-child { transform: none; }
.lead-ticks span:last-child { transform: translateX(-100%); }
.lead-readout { font-size: 11.5px; color: var(--ink-2); margin-top: 1px;
  font-variant-numeric: tabular-nums; }
.lead-readout b { color: var(--ink); }
.lead-outlook { color: #ffd76b; border-bottom: 1px dashed rgba(255,215,107,0.6);
  cursor: help; }
.lead-spread { color: var(--muted); cursor: help; }
.cm-spread { color: var(--muted); font-size: 10.5px; cursor: help; }
.mini-table td.dim { color: var(--muted); opacity: .75; cursor: help; }
/* skill figures on too few verified events: visibly provisional */
.stat .v .thin { color: var(--muted); opacity: .8; cursor: help;
  border-bottom: 1px dotted var(--baseline); }
.sp-panel { margin: 2px 0 10px; }
.sp-panel input[type=range] { -webkit-appearance: none; appearance: none;
  width: 100%; height: 14px; border-radius: 7px; outline: none;
  border: 1px solid rgba(255,255,255,0.12); background: #232833;
  cursor: pointer; }
.sp-panel input[type=range]::-webkit-slider-thumb { -webkit-appearance: none;
  appearance: none; width: 18px; height: 18px; border-radius: 50%;
  background: #fff; border: 2px solid #ffb347;
  box-shadow: 0 1px 5px rgba(0,0,0,0.6); }
.sp-panel input[type=range]::-moz-range-thumb { width: 16px; height: 16px;
  border-radius: 50%; background: #fff; border: 2px solid #ffb347; }
.sp-days { display: grid; grid-template-columns: repeat(7, 1fr);
  font-size: 10px; color: var(--txt-dim, #9aa3ad); text-align: center;
  margin-top: 3px; }
.sp-days .sel { color: #ffd9a0; font-weight: 600; }
.sp-note { font-size: 10.5px; color: var(--txt-mut, #6d7680);
  margin-top: 4px; line-height: 1.5; }

/* gate verdict chips: the ratchet at a glance */
.gate-strip { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 6px 0 8px; font-size: 11.5px; }
.gate-champ { color: var(--ink-2); }
.gate-champ.none { color: var(--muted); }
.gate-chips { display: inline-flex; gap: 4px; flex-wrap: wrap; }
.gate-chip { padding: 2px 8px; border-radius: 10px; font-size: 10.5px;
  font-variant-numeric: tabular-nums; cursor: help; border: 1px solid; }
.gate-chip.win { color: var(--good); border-color: var(--good);
  background: rgba(12, 163, 12, 0.10); }
.gate-chip.lose { color: var(--serious); border-color: rgba(236, 131, 90, 0.5);
  background: rgba(236, 131, 90, 0.08); }
.train-gate .lab { color: var(--txt-mut, #6d7680); text-transform: uppercase;
  font-size: 9.5px; letter-spacing: .06em; margin-right: 4px; }
.map-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; gap: 12px; flex-wrap: wrap; }
.legend { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--muted); }
.legend-bar {
  width: 160px; height: 8px; border-radius: 4px;
  background: linear-gradient(90deg, #30123b, #4662d8, #35abf8, #1be5b5, #74fe5d, #c9ef34, #fbb938, #f56918, #c92903, #7a0403);
  outline: 1px solid var(--ring);
}
.map-meta { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.map-meta.stale { color: var(--warn, #e2a33d); font-weight: 600; }
/* reserve the scrollbar gutter: panel show/hide (city selection) must not
   change page width and nudge the map */
html { scrollbar-gutter: stable; }

/* ---------- city forecast (search) ---------- */
.city-search { display: flex; gap: 6px; margin-bottom: 10px; }
.city-search input {
  flex: 1; background: var(--surface-2); border: 1px solid var(--ring);
  border-radius: 6px; color: var(--ink); padding: 6px 10px; font: inherit; font-size: 13px;
}
.city-search input:focus { outline: 1px solid var(--s-blue); }
.city-search button {
  background: var(--surface-2); border: 1px solid var(--ring); border-radius: 6px;
  color: var(--ink-2); padding: 6px 10px; cursor: pointer; font-size: 12px;
}
.city-search button:hover { border-color: var(--s-blue); color: var(--ink); }
.crosshair-icon {
  color: #fff; font-size: 22px; font-weight: 700; line-height: 22px;
  text-shadow: 0 0 6px var(--s-blue), 0 0 3px #000; background: none; border: none;
}

/* ---------- charts / tables ---------- */
/* fixed-height, overflow-clipped boxes: Chart.js sizes the canvas from the
   box, and minmax(0,·) columns let boxes SHRINK — a plain 1fr child's
   min-width:auto would let a wide canvas blow the grid out of the card */
.chart-box { position: relative; height: 190px; overflow: hidden; }
.chart-box.short { height: 140px; }
.charts-2col { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
               gap: 10px; align-items: start; }
.charts-2col > * { min-width: 0; }
.foot-note { font-size: 11px; color: var(--muted); margin-top: 4px; }

.mini-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 4px; }
.mini-table th { color: var(--muted); font-weight: 500; text-align: right; padding: 3px 6px; border-bottom: 1px solid var(--grid); }
.mini-table td { text-align: right; padding: 3px 6px; border-bottom: 1px solid var(--grid); font-variant-numeric: tabular-nums; color: var(--ink-2); }
.mini-table th:first-child, .mini-table td:first-child { text-align: left; color: var(--muted); }
.mini-table td.best { color: var(--good); font-weight: 600; }
.mini-table td.worst { color: var(--serious); }
.mini-table tr.sel td { background: rgba(57, 135, 229, 0.08); }

.stat-row { display: flex; gap: 18px; flex-wrap: wrap; margin: 6px 0 10px; }
.stat { min-width: 70px; }
.stat .v { font-size: 17px; font-weight: 650; font-variant-numeric: tabular-nums; }
.stat .k { font-size: 10px; color: var(--muted); letter-spacing: .8px; text-transform: uppercase; }

/* ---------- training ---------- */
.train-state { font-size: 15px; font-weight: 600; margin: 2px 0 6px; }
.train-state .state-training { color: var(--s-blue); }
.train-state .state-done { color: var(--good); }
.train-state .state-error { color: var(--critical); }
.train-state .state-waiting_for_data { color: var(--warning); }
.progress { height: 6px; border-radius: 3px; background: var(--surface-2); overflow: hidden; margin-bottom: 8px; }
.progress-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--s-blue), var(--s-violet)); transition: width .6s; }

.quality-notes { font-size: 12px; color: var(--ink-2); }
.quality-notes .kv { margin-bottom: 6px; }
.quality-notes b { color: var(--ink); }

footer { padding: 10px 18px 22px; max-width: 1700px; margin: 0 auto; color: var(--ink-2); font-size: 12px; }
footer .sub { margin-top: 4px; }

a { color: var(--s-blue); }

/* ---------- mobile optimizations ---------- */
button, .layer-btn, .src-chip { touch-action: manipulation; }

.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin; }
.table-scroll .mini-table { min-width: 520px; }
.table-scroll #ensemble-table { min-width: 720px; }
.table-scroll #quality-table { min-width: 640px; }

@media (max-width: 640px) {
  body { font-size: 13px; }
  #topbar { padding: 7px 10px; gap: 10px; }
  /* h1 sizing is fluid now (clamp above) and needs no step here */
  .bolt { font-size: 20px; }
  .brand .sub { font-size: 10px; }
  #clock-utc { font-size: 15px; }
  main { padding: 10px 8px 4px; gap: 10px; }
  .card { padding: 10px; border-radius: 8px; }
  .card-head { flex-direction: column; align-items: flex-start; gap: 3px; margin-bottom: 6px; }
  .map-tabs { width: 100%; }
  .map-tab { flex: 1; text-align: center; white-space: nowrap; }
  .map-tab .sp-tag { display: none; }   /* "7 day" tag wraps awkwardly on narrow screens */

  /* map: stays square (aspect-ratio); controls become swipeable rows */
  .bin-select, .layer-toggles {
    flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch;
    scrollbar-width: none; max-width: 100%; padding-bottom: 2px;
  }
  .bin-select::-webkit-scrollbar, .layer-toggles::-webkit-scrollbar { display: none; }
  .layer-btn { padding: 6px 12px; flex: 0 0 auto; }
  .map-foot { flex-direction: column; align-items: flex-start; gap: 6px; }

  /* iOS zooms into inputs with font-size < 16px — prevent focus-zoom */
  .city-search input { font-size: 16px; padding: 8px 12px; }


  .charts-2col { grid-template-columns: 1fr; }
  .chart-box { height: 170px; }
  .chart-box.short { height: 130px; }

  .stat-row { gap: 10px 16px; }
  .stat .v { font-size: 15px; }
  .stat .k { font-size: 9px; }
  .sources-row { gap: 6px; }
  .src-chip { font-size: 10px; padding: 3px 8px; }

  .train-state { font-size: 13px; }
  .mini-table { font-size: 11px; }
  .mini-table th, .mini-table td { padding: 3px 5px; }
  footer { padding: 8px 10px 18px; font-size: 11px; }
}

@media (max-width: 380px) {
  #clock-utc { font-size: 13px; }
}

/* ---------- zoom-level-of-detail city labels ---------- */
.city-dot-label.big { font-size: 13px; font-weight: 700; color: #fff; }

/* ---------- map watermark (all maps) ---------- */
#map { position: relative; }
.map-watermark {
  position: absolute; right: 9px; bottom: 22px; z-index: 500;
  pointer-events: none; text-align: right;
  font-size: 10px; line-height: 1.18; letter-spacing: .4px; font-weight: 700;
  color: rgba(255, 255, 255, .74);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .95), 0 0 4px rgba(0, 0, 0, .7);
}
.map-watermark small { display: block; font-weight: 400; opacity: .82; letter-spacing: .2px; }

/* ---------- detailed-map trigger ---------- */
.detail-btn { white-space: nowrap; }
.detail-btn[disabled] { opacity: .45; cursor: default; }

/* ---------- city detail modal (100 m risk map) ---------- */
.modal-overlay {
  position: fixed; inset: 0; z-index: 2000;
  background: rgba(0, 0, 0, .66); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal-overlay[hidden] { display: none; }
body.modal-open { overflow: hidden; }
.modal-card {
  background: var(--surface); border: 1px solid var(--grid); border-radius: 10px;
  width: min(1040px, 96vw); max-height: 92vh; display: flex; flex-direction: column;
  overflow: hidden; box-shadow: 0 24px 70px rgba(0, 0, 0, .6);
}
.modal-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 13px 16px; border-bottom: 1px solid var(--grid);
}
.modal-titles { min-width: 0; }
.modal-titles h2 { font-size: 15px; text-transform: none; letter-spacing: .3px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.modal-sub { color: var(--muted); font-size: 12px; margin-top: 3px; }
.modal-actions { display: flex; align-items: center; gap: 8px; flex: none; }
.modal-btn {
  background: var(--surface-2); border: 1px solid var(--grid); color: var(--ink-2);
  border-radius: 6px; height: 30px; padding: 0 10px; cursor: pointer; font-size: 12px;
  white-space: nowrap;
}
.modal-btn:hover { border-color: var(--s-blue); color: var(--ink); }
.modal-btn.ok { border-color: var(--good); color: var(--good); }
.modal-close {
  background: transparent; border: 1px solid var(--grid); color: var(--ink-2);
  border-radius: 6px; width: 30px; height: 30px; cursor: pointer; font-size: 14px; flex: none;
}
.modal-close:hover { border-color: var(--s-red); color: var(--ink); }
.cm-statusbar {
  padding: 8px 16px; font-size: 12px; color: var(--ink-2);
  border-bottom: 1px solid var(--grid); background: var(--surface-2);
}
.cm-statusbar:empty { display: none; }
.cm-statusbar.alert { color: var(--warning); font-weight: 600; }
.cm-onset {
  padding: 8px 16px; font-size: 13px; font-weight: 600; color: #ffd27a;
  background: rgba(255, 180, 60, .07);
  border-bottom: 1px solid var(--grid);
}
.cm-onset small { font-weight: 400; color: var(--ink-2); }

/* estimated-onset banner in the city forecast card (main screen) */
.vg-onset[hidden], .cm-onset[hidden] { display: none; }
.vg-onset {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin: 10px 0 4px; padding: 9px 13px;
  background: linear-gradient(90deg, rgba(255, 180, 60, .16), rgba(255, 180, 60, .03));
  border: 1px solid rgba(255, 180, 60, .32); border-left: 3px solid var(--warning);
  border-radius: 8px;
}
.vg-onset .lab {
  font-size: 10px; font-weight: 700; letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--warning);
}
.vg-onset .t {
  font-size: 21px; font-weight: 750; color: #ffd27a;
  font-variant-numeric: tabular-nums; line-height: 1;
}
.vg-onset .w { font-size: 12px; color: var(--ink-2); }
/* observed-actuals (OAP) readout: distinct from the amber forecast banner
   because this is measurement, not prediction */
/* quiet state: pastel green, no storm expected at this point within 4 h */
.vg-onset.quiet {
  background: linear-gradient(90deg, rgba(140, 214, 168, .15), rgba(140, 214, 168, .03));
  border-color: rgba(140, 214, 168, .32); border-left-color: #8fd6a8;
}
.vg-onset.quiet .lab { color: #8fd6a8; }
.vg-onset.quiet .msg { font-size: 13px; font-weight: 600; color: #bde8cb; }
.modal-body {
  display: grid; grid-template-columns: minmax(300px, 480px) minmax(210px, 1fr);
  gap: 14px; padding: 14px 16px; overflow: auto;
}
.cm-mapwrap { position: relative; min-width: 0; align-self: start; }
#cm-canvas {
  width: 100%; display: block; border-radius: 8px; background: #12140f;
  border: 1px solid var(--grid);
}
.cm-side { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.cm-bins { display: flex; flex-wrap: wrap; gap: 4px; }
.cm-legend { display: flex; flex-direction: column; gap: 3px; }
.cm-legend-bar {
  height: 10px; border-radius: 5px;
  background: linear-gradient(to right, #30123b, #4662d8, #35abf8, #1be5b5,
    #74fe5d, #c9ef34, #fbb938, #f56918, #c92903, #7a0403);
}
.cm-legend-lab { display: flex; justify-content: space-between; font-size: 10px; color: var(--muted); }
.cm-info { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; }
.cm-fact {
  display: flex; justify-content: space-between; gap: 8px; font-size: 11px;
  border-bottom: 1px solid var(--grid); padding-bottom: 3px;
}
.cm-fact span { color: var(--muted); }
.cm-fact b { color: var(--ink-2); font-variant-numeric: tabular-nums; }
.cm-note { font-size: 11px; color: var(--muted); line-height: 1.42; }
.cm-note b { color: var(--ink-2); font-weight: 600; }
@media (max-width: 720px) {
  .modal-body { grid-template-columns: 1fr; }
  .modal-overlay { padding: 8px; }
}

/* solar view (city modal) */
#cm-sun-btn.on { background: #6b5416; border-color: #c9a227; color: #ffe9a8; }

.sun-pct { display: inline-block; padding: 1px 7px;
  border-radius: 9px; background: #6b5416; color: #ffe9a8; font-size: 11.5px;
  font-weight: 600; vertical-align: 1px; }
.sun-pct-max { display: inline-block; padding: 1px 7px;
  border-radius: 9px; border: 1px solid #8a6b1f; color: #c9a75a;
  background: transparent; font-size: 11.5px; font-weight: 500;
  vertical-align: 1px; }

@media (max-width: 640px) {
  .sun-pct, .sun-pct-max { font-size: 10.5px; padding: 1px 6px; }
}

.sun-panel { display: inline-block; padding: 1px 7px; border-radius: 9px;
  background: #1d3a24; border: 1px solid #2f6b3d; color: #9fe0ad;
  font-size: 11.5px; font-weight: 600; vertical-align: 1px; }
@media (max-width: 640px) { .sun-panel { font-size: 10.5px; padding: 1px 6px; } }

/* ---- Sun tab (city modal) ---- */
.cm-tabs { display: flex; gap: 0; margin-bottom: 10px; border: 1px solid var(--ring);
  border-radius: 9px; overflow: hidden; }
.cm-tab { flex: 1; padding: 8px 10px; background: var(--surface-2); color: var(--muted);
  border: none; cursor: pointer; font-size: 13px; font-weight: 600;
  font-family: inherit; transition: background .15s, color .15s; }
.cm-tab + .cm-tab { border-left: 1px solid var(--ring); }
.cm-tab.on { background: #2a2416; color: #ffd76b; }
#cm-tab-storm.on { background: #1c2733; color: #7fc4ff; }

.sun-hero { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 10px; }
.sun-hero-main { display: flex; align-items: baseline; gap: 7px; }
#sun-hero-val { font-size: 34px; font-weight: 700; color: #ffd76b;
  font-variant-numeric: tabular-nums; line-height: 1; }
.sun-hero-unit { color: var(--muted); font-size: 13px; }
.sun-hero-sub { display: flex; flex-wrap: wrap; gap: 5px 7px; }

.sun-chart { height: 130px; }
.sun-chart-note { color: var(--muted); font-size: 10px; margin: 3px 0 10px; }

.sun-facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  gap: 8px; margin-bottom: 10px; }
.sun-fact { background: var(--surface-2); border: 1px solid var(--ring);
  border-radius: 8px; padding: 7px 9px; }
.sf-k { color: var(--muted); font-size: 10px; text-transform: uppercase;
  letter-spacing: .5px; }
.sf-v { font-size: 15px; font-weight: 650; color: var(--ink); margin: 2px 0 1px;
  font-variant-numeric: tabular-nums; }
.sf-s { color: var(--muted); font-size: 10px; }

.sun-note { color: var(--muted); font-size: 11px; line-height: 1.45; }

@media (max-width: 640px) {
  #sun-hero-val { font-size: 28px; }
  .sun-facts { grid-template-columns: repeat(2, 1fr); }
  .sun-chart { height: 110px; }
}

/* ================= design pass 2026-07-26: depth, tables, affordances ===== */

/* cards get depth against a darker page; header gains an identity line */
body { background: #0d0e10; }
.card { box-shadow: 0 1px 0 rgba(255,255,255,.03) inset, 0 6px 18px rgba(0,0,0,.35);
  border-color: #2c2f34; }
#topbar { border-bottom: 1px solid transparent;
  border-image: linear-gradient(90deg, #e8b33d 0%, #3f8cff 45%, transparent 90%) 1;
  background: #0d0e10; }

/* quiet probabilities recede, live ones lead */

/* tables: numerals align and tabulate; gentle zebra; header separation */
.mini-table td:not(:first-child), .mini-table th:not(:first-child) {
  text-align: right; font-variant-numeric: tabular-nums; }
.mini-table thead th { border-bottom: 1px solid #33373d; color: #9aa3ad; }
.mini-table tbody tr:nth-child(even) { background: rgba(255,255,255,.018); }

/* scroll affordance: fade the clipped edge so tables read as scrollable */
.table-scroll { position: relative;
  -webkit-mask-image: linear-gradient(90deg, #000 92%, transparent 100%);
          mask-image: linear-gradient(90deg, #000 92%, transparent 100%); }
.table-scroll:hover { -webkit-mask-image: none; mask-image: none; }

/* interactive affordances + accessibility */
.layer-btn:hover:not(.on), .cm-tab:hover:not(.on) {
  border-color: #4d5b6b; color: var(--ink); }
button:focus-visible, input:focus-visible {
  outline: 2px solid #3f8cff66; outline-offset: 1px; }
.modal-btn { min-height: 32px; }

/* readability floors */
@media (max-width: 640px) {
  .foot-note, .sf-s, .sun-chart-note { font-size: 10px; }
  .sf-v { font-size: 14px; }
}

/* ---- sun hour slider: the 48 h of light at a glance ---- */
.sun-slider-wrap { position: relative; margin: 2px 0 22px; }
#sun-slider, #sp-slider { touch-action: none; }
#sun-slider { -webkit-appearance: none; appearance: none; width: 100%;
  height: 16px; border-radius: 9px; border: 1px solid #3a3f47;
  box-shadow: inset 0 1px 3px rgba(0,0,0,.5); cursor: pointer;
  background: #1a2238; outline-offset: 3px; }
#sun-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
  width: 22px; height: 22px; border-radius: 50%; background: #f4f6f8;
  border: 3px solid #0d0e10; box-shadow: 0 0 0 1.5px #ffd76b, 0 2px 6px rgba(0,0,0,.55);
  cursor: grab; }
#sun-slider:active::-webkit-slider-thumb { cursor: grabbing; transform: scale(1.08); }
#sun-slider::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%;
  background: #f4f6f8; border: 3px solid #0d0e10;
  box-shadow: 0 0 0 1.5px #ffd76b, 0 2px 6px rgba(0,0,0,.55); cursor: grab; }
#sun-slider::-moz-range-track { background: transparent; }

.sun-ticks { position: relative; height: 14px; margin-top: 4px; }
.sun-tick { position: absolute; transform: translateX(-50%); top: 0;
  color: var(--muted); font-size: 9.5px; letter-spacing: .3px; white-space: nowrap; }
.sun-tick.midnight { color: #c9a75a; font-weight: 700; text-transform: uppercase; }

.sun-slider-bubble { position: absolute; bottom: calc(100% + 6px);
  transform: translateX(-50%); background: #23262b; border: 1px solid #4d5b6b;
  color: var(--ink); font-size: 11.5px; font-weight: 600; padding: 3px 9px;
  border-radius: 7px; white-space: nowrap; pointer-events: none; z-index: 3;
  box-shadow: 0 3px 10px rgba(0,0,0,.5); font-variant-numeric: tabular-nums; }

@media (max-width: 640px) {
  #sun-slider { height: 18px; }
  #sun-slider::-webkit-slider-thumb { width: 26px; height: 26px; }
  .sun-tick { font-size: 9px; }
}

/* ---- sun panel: fixed geometry so slider steps never move the layout ---- */
#sun-hero-val { min-width: 3.4ch; display: inline-block; text-align: right; }
.sun-hero-sub { min-height: 21px; }          /* chips row keeps its height */
.cm-statusbar { min-height: 34px; }          /* status text may wrap or not */
.sun-facts { grid-auto-rows: 58px; }         /* tiles never resize on value */
.sf-v, .sf-s { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sun-chart { height: 130px; }                /* chart box is fixed, not fluid */
#sun-panel { contain: layout; }              /* isolate reflow to the panel */

/* every varying number gets a FIXED slot, so values changing with the slider
   cannot reflow the chip row (8% vs 76% vs 100% used to shift everything) */
#sun-hero-val { min-width: 4.6ch; }          /* fits up to 1100 */
.sun-hero-sub b { display: inline-block; text-align: right; font-weight: inherit;
  font-variant-numeric: tabular-nums; }
.sun-pct b, .sun-pct-max b { min-width: 2.7ch; }
.sun-panel b { min-width: 3.2ch; }
.sun-hero-sub { flex-wrap: nowrap; overflow: hidden; }
.sf-v { font-variant-numeric: tabular-nums; }

/* status text length varies by an em here and there; pin the bar so the map
   below it never shifts by a pixel between slider steps */
.cm-statusbar { height: 40px; min-height: 40px; display: flex;
  align-items: center; overflow: hidden; }

/* ================= presentation overhaul 2026-08-02 ======================
   The page used to open with 24 identical "0.0%" tiles and a 24-row table of
   dashes: roughly 1000 px of vertical space spent to say "nothing is
   happening", with the one sentence a visitor actually wanted buried in the
   middle of it. The data has not been reduced anywhere. It has been given a
   hierarchy: the answer, then the map, then the evidence, then the engine.  */

/* ---------- tier 1: the answer ---------- */
.hero { padding: 14px 16px 15px; }
.hero-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 14px; flex-wrap: wrap; margin-bottom: 13px;
}
.hero-place h2 {
  font-size: 21px; letter-spacing: .4px; text-transform: none;
  color: var(--ink); font-weight: 700; line-height: 1.15;
}
.hero-place .sub { margin-top: 2px; }
.hero .city-search { margin-bottom: 0; flex: 0 1 380px; min-width: 230px; }

.hero-grid {
  display: grid; grid-template-columns: minmax(210px, 280px) minmax(0, 1fr);
  gap: 18px; align-items: start;
}

/* observed conditions: the biggest number on the page, because "how warm is
   it" is the question asked most often */
.now-panel { min-width: 0; }
.now-line { display: flex; align-items: flex-start; gap: 12px; }
.now-temp {
  font-size: 47px; font-weight: 700; line-height: .95; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -1px;
}
.now-temp .deg { font-size: 26px; font-weight: 500; color: var(--muted);
  vertical-align: super; line-height: 1; margin-left: 1px; }
.now-side { padding-top: 3px; min-width: 0; }
.now-cond {
  font-size: 15px; font-weight: 600; color: var(--ink-2);
  text-transform: capitalize; line-height: 1.25;
}
.now-feels { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.now-facts {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 5px 12px; margin-top: 12px;
}
.now-fact { min-width: 0; }
.now-fact .k {
  font-size: 9.5px; letter-spacing: .7px; text-transform: uppercase;
  color: var(--muted);
}
.now-fact .v {
  font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.now-src { font-size: 10.5px; color: var(--muted); margin-top: 10px; }
.now-empty { font-size: 12.5px; color: var(--muted); padding: 6px 0; }

/* ---------- the 2 hour risk strip ----------
   Replaces 24 tiles that each held one number. Same 24 values, plus the
   SHAPE of the risk over time, in a twelfth of the height. The exact figure
   for any bin is one hover or tap away, and every bin is still listed in
   full in the track-record table below. */
.panel-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 10px; margin-bottom: 7px; flex-wrap: wrap;
}
.panel-title {
  font-size: 11.5px; letter-spacing: 1.1px; text-transform: uppercase;
  color: var(--ink-2); font-weight: 600;
}
.panel-title .hint { letter-spacing: 0; }
.panel-aside {
  font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums;
}

.risk-strip {
  display: flex; gap: 2px; align-items: flex-end;
  height: 72px; padding: 0; position: relative;
  border-bottom: 1px solid var(--baseline);
  transition: height .25s;
}
/* Nothing to plot: collapse to a rail rather than reserving 72 px of empty
   box with a threshold line floating in the middle of it. The bars are still
   there and still clickable, they simply stop pretending to be a chart. */
.risk-strip.flat { height: 24px; }
.risk-seg {
  flex: 1 1 0; min-width: 0; height: 100%; display: flex; align-items: flex-end;
  background: none; border: none; padding: 0; cursor: pointer;
  border-radius: 3px 3px 0 0; transition: background .12s;
}
.risk-seg > i {
  display: block; width: 100%; min-height: 3px; border-radius: 3px 3px 0 0;
  background: var(--baseline); transition: height .35s, background .2s;
}
.risk-seg { position: relative; }
.risk-seg:hover { background: rgba(255, 255, 255, .05); }
/* Selection is marked at the BASE, never as a full-height fill: a filled
   column behind a 3 px bar reads as a tall bar, so selecting the quiet 0-5
   min bin looked exactly like a storm arriving in five minutes. */
.risk-seg.sel::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
  height: 2px; background: var(--s-blue); border-radius: 1px;
}
.risk-seg.sel > i { box-shadow: 0 0 0 1px rgba(255, 255, 255, .45); }
.risk-seg:focus-visible { outline: 2px solid var(--s-blue); outline-offset: 1px; }
/* the alert threshold, drawn once across the whole strip */
.risk-strip::after {
  content: ""; position: absolute; left: 0; right: 0; height: 0;
  border-top: 1px dashed rgba(236, 131, 90, .5);
  bottom: var(--thr, 35%); pointer-events: none;
}
.risk-strip.no-thr::after { display: none; }

.risk-axis {
  display: flex; justify-content: space-between; margin-top: 4px;
  font-size: 9.5px; color: var(--muted); letter-spacing: .3px;
}
.risk-readout {
  margin-top: 6px; font-size: 12px; color: var(--ink-2);
  font-variant-numeric: tabular-nums; min-height: 18px;
}
.risk-readout b { color: var(--ink); font-weight: 650; }
.risk-readout .quiet-note { color: var(--muted); }

/* ---------- next 24 hours ---------- */
.day-panel { margin-top: 15px; padding-top: 13px; border-top: 1px solid var(--grid); }
.day-chart { height: 132px; }
.day-note { font-size: 11px; color: var(--muted); margin-top: 3px; }

/* ---------- disclosure drawers ----------
   Dense reference tables stay on the page in full; they just no longer open
   in front of a reader who came to find out whether it is going to rain. */
.drawer { margin-top: 10px; border-top: 1px solid var(--grid); padding-top: 6px; }
.drawer > summary {
  cursor: pointer; font-size: 11.5px; letter-spacing: .8px;
  text-transform: uppercase; color: var(--muted); font-weight: 600;
  list-style: none; padding: 3px 0; user-select: none;
}
.drawer > summary::-webkit-details-marker { display: none; }
.drawer > summary::before {
  content: "▸"; display: inline-block; margin-right: 7px;
  transition: transform .18s; color: var(--baseline);
}
.drawer[open] > summary::before { transform: rotate(90deg); }
.drawer > summary:hover { color: var(--ink-2); }
.drawer > summary:focus-visible { outline: 2px solid var(--s-blue); outline-offset: 2px; }
.drawer-hint {
  text-transform: none; letter-spacing: 0; color: var(--baseline);
  font-weight: 400; margin-left: 6px;
}

/* ---------- tier 3 heading ---------- */
.engine-head {
  font-size: 11.5px; letter-spacing: 1.6px; text-transform: uppercase;
  color: var(--muted); font-weight: 600;
  margin: 12px 2px -2px; padding-top: 10px;
  border-top: 1px solid var(--grid);
}
.engine-head .hint { letter-spacing: 0; margin-left: 8px; }

@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; gap: 15px; }
  .now-panel { display: flex; flex-wrap: wrap; gap: 0 22px; align-items: flex-start; }
  .now-facts { margin-top: 4px; flex: 1 1 210px; }
  .now-src { flex-basis: 100%; margin-top: 6px; }
}

@media (max-width: 640px) {
  .hero { padding: 12px 11px 13px; }
  .hero-head { gap: 9px; margin-bottom: 10px; }
  .hero-place h2 { font-size: 18px; }
  .hero .city-search { flex: 1 1 100%; }
  .now-temp { font-size: 40px; }
  .now-temp .deg { font-size: 22px; }
  .risk-strip { height: 62px; gap: 1.5px; }
  .day-chart { height: 118px; }
  .engine-head { margin-top: 6px; }
}

/* ---------------- accessibility (phase 4, 2026-08-04) ----------------
   Findings from an audit of the live page, not a checklist copied in. */

/* The city search had a placeholder and no accessible name. A placeholder is
   not a label: it disappears on the first keystroke and screen readers are
   not required to announce it. This hides the label visually while leaving it
   in the accessibility tree - `display: none` would remove it from both. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* 11 transitions and none of them asked. Motion is a real accessibility
   problem for vestibular disorders, and the setting is one the user has
   already expressed at the OS level - honour it rather than making them ask
   twice. Not `animation: none`: a duration of ~0 keeps transitionend firing,
   so any code waiting on a transition still completes instead of hanging. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* The map keeps its own focus handling; everything else gets a visible ring
   that does not depend on the browser default surviving a reset. */
:where(button, a, input, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--s-blue);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ---------------- shared page chrome (phase 4.1) ----------------
   <sg-header> replaced four hand-written headers whose navigation had
   drifted apart: /models offered no way to reach /health or /api at all.
   The links are now a nav landmark rather than three loose anchors, so
   assistive tech can skip past them as a unit. */
.topbar-nav { display: flex; align-items: center; gap: 8px; }

/* The current page renders as a <span>, not an <a>: a link to where you
   already are is a keyboard stop that does nothing. It still needs to LOOK
   like part of the set, so it shares .topbar-api and marks itself. */
.topbar-api.current {
  color: var(--ink);
  border-color: var(--s-blue);
  background: rgba(63, 140, 255, .10);
  cursor: default;
}

@media (max-width: 640px) {
  /* A fourth nav item does not fit beside the brand at 375 px. Letting the
     nav wrap was not enough: `.brand` is `flex: 1` with a 0 basis, so it lost
     the space contest and got squeezed to 88 px, which shredded a 13 px title
     across three lines and made the header 135 px tall - a third of the
     phone screen before any content.
     Give the brand its own row instead, and let the nav and clock share the
     one below. Both then have room to stay on a single line. */
  #topbar { flex-wrap: wrap; row-gap: 6px; column-gap: 10px; }
  .brand { flex: 1 1 100%; }
  .topbar-nav { flex: 1 1 auto; flex-wrap: nowrap; gap: 5px; }
  .topbar-api { padding: 3px 7px; letter-spacing: .6px; }
  .clock { margin-left: auto; text-align: right; }
}

/* ---------------- 3D terrain view (phase 5) ---------------- */
#t3d-canvas {
  width: 100%; height: min(70vh, 640px);
  display: block; border-radius: 8px;
  background: #0d0e10; touch-action: none;   /* orbit, do not scroll the page */
  outline: none;
}
.t3d-row { display: flex; align-items: center; gap: 10px; margin: 8px 0 6px; }
.t3d-row .lead-slider { flex: 1; }

/* SMOOTH PLAYBACK ON A BUSY MAIN THREAD.
   The scrubber used to be stepped straight from the render loop, so the thumb
   inherited the 3D view's frame rate: fine at 60 fps (about 2 px a frame),
   visibly juddery at the 20-30 fps this scene actually runs at on a large
   display, where the same motion arrives in 4-8 px jumps.
   A native range thumb cannot be smoothed - its position comes from `value`,
   which is not a property a transition can touch. So during playback the
   native thumb is hidden and this ghost takes over: it is placed with a
   transform, which the compositor animates off the main thread, so it keeps
   gliding between updates however long the next frame takes. It is inert -
   pointer-events none - and the input underneath remains the real control. */
.t3d-scrub { position: relative; flex: 1; display: flex; align-items: center; }
.t3d-scrub .lead-slider { flex: 1; }
.t3d-scrub-ghost {
  position: absolute; left: 0; top: 50%;
  width: 22px; height: 22px; margin: -11px 0 0 -11px;
  border-radius: 50%; background: #fff; border: 2px solid var(--s-blue);
  box-shadow: 0 1px 5px rgba(0,0,0,0.6);
  pointer-events: none; opacity: 0;
  transform: translateX(var(--t3d-x, 0px));
}
/* Only while playing, and only then is a transition wanted: during a drag it
   would make the thumb lag the pointer. */
.t3d-scrub.is-playing .lead-slider::-webkit-slider-thumb { opacity: 0; }
.t3d-scrub.is-playing .lead-slider::-moz-range-thumb { opacity: 0; }
.t3d-scrub.is-playing .t3d-scrub-ghost {
  opacity: 1; transition: transform 140ms linear;
}
@media (prefers-reduced-motion: reduce) {
  .t3d-scrub.is-playing .t3d-scrub-ghost { transition: none; }
}
#t3d-bin-label { min-width: 92px; text-align: right;
                 font-variant-numeric: tabular-nums; }
/* Reserved, so the slider beside it keeps ONE width for the whole sweep. It
   may shrink on a narrow viewport - that is a resize, which the scrubber
   measures - but it never changes because the text inside it did. */
.t3d-readout { flex: 0 1 21rem; display: flex; gap: 10px;
               justify-content: flex-end; align-items: center;
               font-variant-numeric: tabular-nums; }
.t3d-readout #t3d-sun { text-align: right; }
/* The watermark is required to survive a screenshot. The canvas is created
   with preserveDrawingBuffer so a capture includes the render, and this sits
   over it in the same stacking context so it is captured too. */
/* Absolute inside .t3d-stage, bottom right, inheriting the 2D map's watermark
   styling so both views are marked identically. Below the loader (z-index 3)
   so it does not sit over the loading state, above the capital labels so it is
   never occluded by one. */
.t3d-watermark { right: 12px; bottom: 12px; z-index: 2; pointer-events: none; }

/* floating capital labels over the 3D canvas */
.t3d-stage { position: relative; }
#t3d-labels { position: absolute; inset: 0; pointer-events: none;
              overflow: hidden; border-radius: 8px; }
/* A pin, not a caption: the pill names the place, the stem carries the eye
   down to the exact point, and the dot marks the ground. All three are needed
   at an oblique angle - a floating pill alone is ambiguous about which valley
   it belongs to, and at 26x relief that ambiguity is a whole mountain range. */
.t3d-label {
  position: absolute; top: 0; left: 0; white-space: nowrap;
  font-size: 10px; letter-spacing: .4px; font-weight: 600;
  color: #eef2f7; padding: 1.5px 6px; border-radius: 999px;
  background: rgba(9, 11, 15, .68);
  border: 1px solid rgba(255, 255, 255, .13);
  /* the label must stay legible over both bright terrain and dark sea */
  text-shadow: 0 1px 2px rgba(0, 0, 0, .9);
  box-shadow: 0 1px 6px rgba(0, 0, 0, .45);
  /* transform only - never left/top - so moving a few dozen of these every
     frame stays off the layout path */
  will-change: transform, opacity;
}
.t3d-label::after {                 /* a stem down to the ground point */
  content: ""; position: absolute; left: 50%; top: 100%;
  width: 1px; height: 9px;
  background: linear-gradient(rgba(255, 255, 255, .55),
                              rgba(255, 255, 255, .18));
}
.t3d-label::before {                /* the ground point itself */
  content: ""; position: absolute; left: 50%; top: 100%;
  width: 5px; height: 5px; margin: 7px 0 0 -2.5px;
  border-radius: 50%; background: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .6), 0 0 5px rgba(0, 0, 0, .5);
}


/* ---- 3D loading state --------------------------------------------------
   The 3D view has a real cost to pay before it can show anything: a 785 KB
   heightmap, 72 forecast PNGs and an 8 MB engine. Hiding that behind a bare
   spinner reads as a hang. This says what it is doing and how far along it
   is, and it sits ON the stage so nothing jumps when it goes. */
#t3d-loader {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background:
    radial-gradient(120% 90% at 50% 15%, #1b2436 0%, #0d1119 55%, #080a0f 100%);
  z-index: 3;
  transition: opacity .5s ease;
}
#t3d-loader[hidden] { display: none; }
#t3d-loader.is-going { opacity: 0; pointer-events: none; }
.t3d-loader-inner { text-align: center; padding: 1.5rem; max-width: 28rem; }
.t3d-loader-inner h3 {
  margin: 1.1rem 0 .35rem;
  font-size: 1.05rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #e8eefc;
}
#t3d-loader-msg {
  margin: 0 0 1rem;
  color: #9fb0cc;
  font-size: .88rem;
  min-height: 1.2em;
}
.t3d-loader-foot {
  margin: .75rem 0 0;
  color: #63748f;
  font-size: .74rem;
  line-height: 1.5;
}
/* a small orrery: two rings and a lit core. It turns because something is
   actually happening, and it stops when that stops. */
.t3d-loader-mark {
  position: relative;
  width: 74px; height: 74px;
  margin: 0 auto;
}
.t3d-orbit {
  position: absolute; inset: 0;
  border: 1px solid rgba(120, 160, 220, .34);
  border-top-color: rgba(150, 200, 255, .95);
  border-radius: 50%;
  animation: t3d-spin 1.9s linear infinite;
}
.t3d-orbit:nth-child(2) {
  inset: 14px;
  border-top-color: rgba(255, 205, 130, .9);
  animation-duration: 3.1s;
  animation-direction: reverse;
}
.t3d-core {
  position: absolute; inset: 31px;
  border-radius: 50%;
  background: radial-gradient(circle at 38% 34%, #fff 0%, #ffd89a 45%, #e08a3c 100%);
  box-shadow: 0 0 18px rgba(255, 190, 120, .55);
}
.t3d-bar {
  height: 3px;
  background: rgba(255, 255, 255, .09);
  border-radius: 3px;
  overflow: hidden;
}
.t3d-bar > i {
  display: block;
  height: 100%;
  width: 0%;
  border-radius: 3px;
  background: linear-gradient(90deg, #4d7fd0, #8fc2ff);
  transition: width .3s ease;
}
/* Respect a stated preference for less motion: the rings stop, the bar still
   reports progress, because progress is information and not decoration. */
@media (prefers-reduced-motion: reduce) {
  /* Stopped, but not removed. Killing the animation outright means
     animationend never fires, so anything sequenced on it waits forever. One
     near-instant iteration settles the rings and still fires. */
  .t3d-orbit {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
  /* Near-zero duration rather than removing the transition. A removed
     transition never fires transitionend, so anything sequenced on it waits
     forever; 0.01ms is imperceptible and still fires. Same convention the
     rest of this sheet uses. */
  #t3d-loader, .t3d-bar > i { transition-duration: 0.01ms !important; }
}
@keyframes t3d-spin { to { transform: rotate(360deg); } }

/* ---- the 7-day outlook strip (city panel) --------------------------------
   Two visually distinct sources on one row: the modelled days carry a solid
   accent, the climatological ones a hatched, muted bar. Someone glancing at
   this must not read "typical for August" as "forecast for Thursday". */
.sc-outlook { display: flex; gap: 4px; align-items: flex-end; margin: 6px 0 4px; }
.sc-od { flex: 1 1 0; display: flex; flex-direction: column;
         align-items: center; gap: 2px; min-width: 0; }
.sc-od .sc-obar { width: 100%; height: 46px; display: flex; align-items: flex-end;
                  background: rgba(255,255,255,.05); border-radius: 3px; }
.sc-od .sc-obar i { display: block; width: 100%; border-radius: 3px;
                    background: #4b83c4; }
.sc-od .sc-obar i.mid { background: #d7a13b; }
.sc-od .sc-obar i.hi  { background: #cf5340; }
.sc-od.unver .sc-obar i { opacity: .55;
  background-image: repeating-linear-gradient(45deg,
    rgba(255,255,255,.25) 0 2px, transparent 2px 5px); }
.sc-od b { font-size: 11px; font-variant-numeric: tabular-nums; }
.sc-od span { font-size: 10px; opacity: .65; white-space: nowrap; }
/* rain icons above a day's bar (drops, plus a bolt from 51%). The row aligns
   at flex-end, so icon-less days simply lack the top row — bars stay level. */
.sc-orain { display: block; height: 10px; flex: none; }
.sc-orain .drop { fill: #4b83c4; }
.sc-orain .bolt { fill: #d7a13b; }

/* ---- model scorecard (health page) --------------------------------------- */
.sg-models td.num { text-align: right; font-variant-numeric: tabular-nums; }
.sg-models .served { color: #7fc47f; font-weight: 600; }
.sg-models .benched { opacity: .6; }

/* a day the model could not compute at all — visibly absent, never a zero bar */
.sc-od.gap .sc-obar { opacity: .35;
  background-image: repeating-linear-gradient(45deg,
    rgba(255,255,255,.10) 0 3px, transparent 3px 7px); }
.sc-od.gap b { color: var(--muted); }
