/*
 * Forecast Map v3 — THE FINDING COLUMN.
 *
 * v2's doctrine was "the map is the page, and the chrome floats on it". It is reversed here, and
 * the reversal is the whole change. Full bleed is right while the chrome is small; it was not.
 * Four cards — 320 px brand, 232 px tools, 400 px key, 560 px timeline — sat in all four corners
 * of the map, the discovery prompt was squeezed into `calc(100vw - 400px)` because that was the
 * only band they left free, and picking a place dropped an `inset: 0` scrim modal over the map
 * ENTIRELY. On a 390 px phone, expanding the controls covered 692 of 844 px: 82% of the screen.
 *
 * So the cards dissolve into ONE column in the house figure order (`includes/figure-frame.php`:
 * eyebrow → title → deck → legend → FINDING → plot → method), and the map becomes a contiguous
 * rectangle that nothing ever covers. Below 1024 px the same DOM is a bottom sheet with three
 * snap points, and the peek state is the one it loads in.
 *
 * WHAT LIVES WHERE. This file already stated the rule for three of its twelve controls and
 * ignored it for the other nine:
 *
 *     A control that changes the READING stays on the page.
 *     A control that changes the DRAWING or the SEARCH goes into Options.
 *
 * So the element switch, the threshold line and its ramp, the rider's alert, the explore rules,
 * the day chips and the playhead are all in the column or on the timeline; fill, the direction
 * overlay, the search horizon, the origin, the scope and the layer toggles are behind one button.
 * The map keeps exactly one floating instrument — the timeline — and one Options button.
 *
 * Light plate, not dark. NOAA's ramp runs pale pink through violet to blue and its calm end sits
 * close to white: on dark chrome those steps disappear, which is the trap the Shore Ribbon's
 * plate was rebuilt to escape.
 *
 * TYPE. Space Grotesk and JetBrains Mono, which is what every other WindStoke figure surface
 * names (`ws_ribbonStyles()`, `includes/figure-frame.php`) and what this page was the last one
 * not to load. Mono carries every number, every eyebrow and every timestamp — tabular figures
 * stop the spot list jittering as the playhead moves, and this page is an instrument, so the
 * numbers are the content. The old system stack stays as the declared fallback, so a blocked
 * font request leaves a readable page rather than a broken one.
 */

:root {
  --plate: #F8FAFC;
  --ink: #0F1B2A;
  --ink-2: #4A5C72;
  --ink-3: #7C8CA1;
  --line: #DCE4EE;
  --line-2: #C3CEDC;
  --signal: #EA580C;
  /* The row strips' three states, in the dashboard's colours. `--success` (#10B981) carries over
     unchanged; its `--warning` (#FBBF24) is mixed for a dark card and goes thin on this white
     plate, so the amber is one step deeper — and deliberately unlike `--signal`, which is
     annotation and never a reading. */
  --strip-good: #10B981;
  --strip-close: #E8991B;
  --strip-under: rgba(15,27,42,.085);
  /* THE FIFTH STATE: past the top of the gear ladder, where green stops climbing (README §0k).
     MEASURED, not chosen, because it has to clear SIX things at once — and the last two are what
     a first pass missed. In OKLab dE, against the ramp's own 5 kt step of 0.118 as the bar:

       plate  green  amber  --signal | wind ramp 26-45kt | wave ramp 8ft+ | BARBS over field
       0.518  0.411  0.383  0.293    | 0.213             | 0.141          | 0.173

     What each constraint rules out:
       - RED, which is what a reader expects: #DC2626 sits 0.087 from --signal, BELOW the ramp's
         own step. The ceiling band and the threshold contour would be two annotations on one map
         that nobody can tell apart — the §0j failure one layer up.
       - WHITE: 0.016 from the plate. Invisible on the strips.
       - INK (#0F1B2A), which this was written as first: 0.098 from the BARBS, which are
         rgba(24,36,54,.66) on the same canvas. render-test/tint-check.js caught it — tint OFF,
         542 "ceiling" pixels still on screen, every one of them a barb. A near-black cannot be an
         annotation on a map that already draws near-black annotations.
       - Every cool ink: the wind ramp runs indigo-through-teal exactly where this fires.
       - Every deep red and rust: the WAVE ramp is #D10067 onwards past its own 8 ft ceiling, so
         an ink that clears wind's capped range collides with wave's.

     Magenta is the region left. Its weakest link is wave's own top end (0.141, 1.2x the bar),
     accepted because that is the rarest field this ever draws over and because it clears the two
     things a reader must never confuse it with — the contour (0.293) and the barbs (0.173) — by
     two to three times. It reads as off the top of the scale without claiming a danger this map
     has no basis for. */
  --strip-over: #A21CAF;
  --card: rgba(255,255,255,.94);
  /* THE CHROME'S FOUR SURFACES, named so the dark block below can swap them (2026-09-18):
     `--paper` is a solid control face (buttons, chips, cards on the plate), `--glass` the
     near-opaque face of a control floating ON THE MAP (the scale, the switch, the timeline),
     `--on-ink` the text on an ink-filled control, `--halo` the light ring that separates a
     mark from what it sits on. Shadows stay shadows in both themes (`--shade`): a shadow is
     darkness, not ink, which is why it is not mixed from `--ink`. */
  --paper: #fff;
  --glass: rgba(255,255,255,.96);
  --on-ink: #fff;
  --halo: rgba(255,255,255,.9);
  --shade: rgba(15,27,42,.22);
  --shade-2: rgba(15,27,42,.18);
  --radius: 12px;
  --shadow: 0 1px 2px rgba(15,27,42,.08), 0 10px 30px -14px rgba(15,27,42,.35);
  --col-w: 360px;
  --sans: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

/*
 * `hidden` MEANS HIDDEN, and on this page it did not.
 *
 * The UA stylesheet gives `[hidden]` `display: none`, which any author rule with a display in it
 * beats on specificity — and `.ctl { display: grid }` is exactly such a rule. `#rider` is a
 * `.ctl` that ships `hidden` and is unhidden by applyRider(), so from stage 5 until this line
 * every SIGNED-OUT reader saw an empty "Your alert" dropdown: the element's `.hidden` property
 * was true (which is what render-test/call-check.js asserts, and why it passed), the computed
 * style said grid, and nothing threw. Found 2026-09-12 in a screenshot, not in a test.
 */
[hidden] { display: none !important; }

html, body { height: 100%; }
body {
  margin: 0;
  background: var(--plate);
  color: var(--ink);
  font: 14px/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }

/* `.colrule` WAS HERE — the hairline between the column's halves. The section headers carry
   their own rule now, and two kinds of divider in one column is one kind too many. */

/* -- the two zones --------------------------------------------------------------------------- */

/* THE SITE'S CHROME IS ABOVE AND (on a phone) BELOW THIS PAGE since 2026-09-18, when the map
   became the Explore landing. `--ws-top` is the site header's height and `--ws-bottom` the bottom
   nav's, both MEASURED by app.js (`publishChrome()`) rather than assumed — the header wraps at
   narrow widths and carries the iOS safe-area inset, and the nav is hidden above 768 px. Every
   snap height reads `--ws-avail`, the height left between them, for the same reason the sheet's
   own height was measured and not guessed (§0n). */
:root { --ws-top: 0px; --ws-bottom: 0px; --ws-avail: 100vh; }
#app {
  /* `max()` with the status-bar inset: on a phone the site header is not drawn (index.php) and
     `--ws-top` measures 0, but in the installed PWA the page starts under the iOS clock. When
     the header IS drawn its own padding already carries the inset, so it wins the max. */
  position: fixed; top: max(var(--ws-top), env(safe-area-inset-top, 0px)); right: 0; bottom: var(--ws-bottom); left: 0;
  display: grid; grid-template-columns: var(--col-w) 1fr;
}

#col {
  position: relative; z-index: 4; min-height: 0;
  background: var(--plate); border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
}
#colscroll {
  flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain;
  padding: 16px 18px 24px; display: grid; gap: 10px; align-content: start;
  /* GRID ITEMS DEFAULT TO `min-width: auto`, so one wide child — the three-chip element switch,
     a `white-space: nowrap` stat line, a long region name — stretches the implicit column past
     the plate and clips every line in the head. The fix belongs on the TRACK: an item can only
     shrink below its own content if the track it sits in allows it to. */
  grid-template-columns: minmax(0, 1fr);
}
#colscroll > * { min-width: 0; }
/* The grip is a phone control. A desktop has a column that is simply there. */
#grip { display: none; }

#pane { position: relative; overflow: hidden; }
#map { position: absolute; inset: 0; }

/* Direction and the threshold contour. ABSOLUTE INSIDE THE PANE, never fixed to the viewport:
   sizeOverlay() measures #map for the canvas dimensions, but a viewport-anchored canvas would
   draw every barb and the whole contour shifted left by the column's width, and nothing would
   throw — a wrong-looking map rather than an error. */
#dir { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
/* The spot cards and the discs are MapLibre markers, positioned with no z-index of their own, so
   they painted UNDER the overlay above — lattice, contour and barbs drawn across a card's text
   (Jeff's screenshot, 2026-09-15: "the text is behind the map stuff"). A marker is a reading and
   the overlay is annotation; the reading is on top. Still under the badge and the controls (3,
   later in the DOM) and the HUDs (4+). */
.maplibregl-marker { z-index: 3; }

/* The scale on the map — see index.html #mapkey. */
.mapkey { position: absolute; left: 12px; top: 10px; z-index: 3; display: flex; gap: 7px; align-items: stretch; }
.mk-ramp {
  position: relative; width: 150px; height: 34px; padding: 0; margin: 0; cursor: pointer;
  border: 1px solid var(--line-2); border-radius: 9px; background: var(--glass);
  box-shadow: 0 1px 8px var(--shade); text-align: left; overflow: visible;
}
.mk-bar { position: absolute; left: 8px; right: 8px; top: 12px; height: 8px; border-radius: 4px; }
.mk-mark {
  position: absolute; top: 9px; width: 2px; height: 14px; margin-left: -1px;
  background: var(--ink); box-shadow: 0 0 0 1px var(--halo);
}
.mk-val {
  position: absolute; top: 3px; transform: translateX(-50%); white-space: nowrap;
  font: 700 8.5px/1 var(--mono); letter-spacing: .04em; color: var(--ink);
}
.mk-ticks { position: absolute; left: 8px; right: 8px; top: 22px; height: 10px; }
.mk-ticks span {
  position: absolute; top: 0; transform: translateX(-50%);
  font: 500 8px/1 var(--mono); color: var(--ink-3); white-space: nowrap;
}
.mk-ticks span:first-child { transform: none; }
.mk-ticks span:last-child { transform: translateX(-100%); }
.linebtn {
  flex: none; padding: 0 9px; height: 34px; cursor: pointer;
  border: 1px solid var(--line-2); border-radius: 9px; background: var(--glass);
  color: var(--ink-2); font: 600 11.5px/1 var(--sans);
  box-shadow: 0 1px 8px var(--shade);
}
.linebtn[aria-pressed="true"] { background: var(--ink); color: var(--on-ink); }
/* The scale shares the top strip with the element switch, and the switch grew a fourth item on
   2026-09-15 (§1b) — so the plate gives up 16 px of ramp and the Line button 6 px of padding at
   the width where the two would otherwise meet. Measured at 390 px: scale 12–168, switch 178–380,
   10 px of clear strip between them. */
@media (max-width: 400px) { .mk-ramp { width: 104px; } .linebtn { padding: 0 6px; } }
@media (max-width: 360px) { .mk-ramp { width: 100px; } }

.offscreen { position: absolute; left: -20000px; top: 0; visibility: hidden; pointer-events: none; }

/* -- the column's head ----------------------------------------------------------------------- */

#colhead { display: grid; gap: 2px; }

/* THE MASTHEAD IS SMALL AND THE READING IS BIG. That is the vernacular this page is in: a chart
   prints its title in the margin and its value in the middle. `Forecast Map` at 16 px over a
   12.5 px answer had it exactly the wrong way round. */
.mast { display: flex; align-items: center; gap: 8px; }
.mark {
  width: 20px; height: 20px; border-radius: 5px; flex: none; position: relative;
  background: linear-gradient(#FB99F9 0 34%, #963EBF 34% 68%, #2254B7 68% 100%);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 12%, transparent);
}
.mark::after { content: ''; position: absolute; left: 0; right: 0; top: 32%; height: 2px; background: var(--signal); }
h1 {
  font: 600 10.5px/1 var(--mono); letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-3);
}
#brand-more { margin-left: auto; }

/* The deck describes the page. It folds everywhere, not only on a phone: a reader with the
   finding in front of them does not need the page described to them. */
.sub { display: none; margin: 6px 0 0; font-size: 12px; line-height: 1.45; color: var(--ink-2); }
#colhead.open .sub { display: block; }

.valid {
  margin: 12px 0 0; font: 600 11px/1.2 var(--mono); letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-2);
}
/* THE FINDING. It was 12.5 px, under a 16 px strapline, outweighed 7× by its own 90-word
   caption. It is the first thing on this page that answers rather than describes. */
.answer {
  margin: 5px 0 0; font-size: 19px; line-height: 1.3; font-weight: 600;
  letter-spacing: -0.015em; color: var(--ink);
}
/* `.meta` WAS HERE with `#answer-hour`, `#meta` and `.today`'s rows — three caption faces under
   one verdict and a second copy of the spot list. The head carries one sentence now (§0t).
   `.issued` survives because the issue times did: they moved into "How this is made". */
.issued { margin: 6px 0 0; font: 400 10.5px/1.45 var(--mono); color: var(--ink-3); }

/* -- THE THREE QUESTIONS ----------------------------------------------------------------------
   The column is three headed sections and nothing else at the top level. The headers ARE the
   questions the page answers, in the order a reader asks them, which is the cheapest possible
   answer to "nothing tells a reader what is where". */

.q { display: grid; gap: 8px; align-content: start; }
.qh {
  font: 600 11px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-3); margin: 14px 0 0;
  padding-top: 12px; border-top: 1px solid var(--line);
}
/* Section 1 opens the column: it needs no rule above it and no space to push it down. */
#sheet > .qh { margin-top: 0; padding-top: 0; border-top: 0; }
/* The element switch sits IN the second section's header, because it decides what that whole
   section is a legend of. The header keeps its own baseline; the switch is the wider thing. */
.qhrow {
  display: grid; gap: 7px; margin: 14px 0 0; padding-top: 12px; border-top: 1px solid var(--line);
}
.qhrow .qh { margin: 0; padding: 0; border: 0; }

/* -- the section strip (phone only; see wireSectionNav) ---------------------------------------
   Sticky INSIDE `#colscroll`, never fixed — §0o counts fixed panels outside `#col`, and this is
   neither fixed nor outside. Hidden on a desktop, where all three headers are visible anyway. */
#qnav { display: none; }

/* -- THE LEGEND: one row per mark, each carrying the mark itself ------------------------------
   The samples are the map's own classes (`.spot`, `.obsmark`), the map's own gradient (the
   lattice, above) and the map's own glyph function (the barb canvas, drawn by barbGlyph()). There
   is nothing here to restyle separately, which is the point: a legend that can drift from its
   figure is worse than no legend, because it is believed. */

.legend { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 7px; }
/* The sample column is 38 px, not 26, since 2026-09-15: the live mark became a pill wide enough
   to carry its unit (§0z) and a sample that had to be shrunk to fit its column would stop being
   the real mark, which is the one rule this legend has. Every row widened together — samples
   that do not share an axis read as four unrelated pictures. */
.lrow { display: grid; grid-template-columns: 38px minmax(0, 1fr); gap: 9px; align-items: center; }
.lrow[hidden] { display: none; }
.lsample { display: grid; place-items: center; height: 22px; }
.lrow p { margin: 0; font-size: 11px; line-height: 1.4; color: var(--ink-2); }
/* A row with no mark — why there is no live layer, or that this element has no field this far
   out. It keeps the legend's grid so its text lines up with every other row's. */
.lnote { grid-template-columns: 38px minmax(0, 1fr); }
.lnote p { color: var(--ink-3); }
.lsample .lattice { width: 22px; height: 14px; border-radius: 3px; }
/* The spot sample is the real `.spot`, which is a flex row built to carry a label; here it is the
   dot alone, so the label's gap has nothing to reserve. */
.lsample .spot { gap: 0; }
.lsample .obsmark { position: relative; pointer-events: none; cursor: default; }
/* The warning sample is the real mark's recipe: the NWS colour over a dark casing, a faint fill
   of the same. `--wc` is set per render to the worst outline in view; with none it is a grey
   ring, which is what an empty gate looks like on the map too (nothing). */
.lsample .warnswatch {
  width: 24px; height: 15px; border-radius: 3px; box-sizing: border-box;
  border: 2.2px solid var(--wc, rgba(255,255,255,.45));
  box-shadow: 0 0 0 1.2px color-mix(in srgb, var(--ink) 55%, transparent);
  background: color-mix(in srgb, var(--wc, transparent) 16%, transparent);
}
.lrow.warn-none .warnswatch { border-style: dashed; box-shadow: none; opacity: .6; }
.lrow.warn-none p { color: var(--ink-3); }

/* -- controls ---------------------------------------------------------------------------------
   One shape for a labelled control, wherever it is: in the column or in the Options popover. */

.ctl { display: grid; gap: 5px; }
.clab {
  font: 600 9.5px/1 var(--mono); letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-3);
}
.note { margin: 0; font-size: 11px; color: var(--ink-3); line-height: 1.45; }

.btn {
  border: 1px solid var(--line-2); background: var(--paper); color: var(--ink);
  border-radius: 8px; padding: 8px 12px; font: inherit; font-size: 12.5px; cursor: pointer;
}
.btn:hover { border-color: var(--ink-3); }
.btn.primary { background: var(--ink); color: var(--on-ink); border-color: var(--ink); font-weight: 600; }
.btn.primary:hover { background: #1B2A3D; }
.btn:focus-visible, .chev:focus-visible, .daychip:focus-visible, .place:focus-visible,
#grip:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }

.seg { display: flex; border: 1px solid var(--line-2); border-radius: 8px; overflow: hidden; }
.seg button {
  flex: 1; border: 0; background: var(--paper); color: var(--ink-2);
  font: inherit; font-size: 12px; padding: 7px 4px; cursor: pointer;
}
.seg button + button { border-left: 1px solid var(--line); }
.seg button.on { background: var(--ink); color: var(--on-ink); font-weight: 600; }

.sel {
  font: inherit; font-size: 12px; font-weight: 500; color: var(--ink);
  background: var(--paper); border: 1px solid var(--line-2); border-radius: 8px;
  padding: 6px 8px; width: 100%; max-width: 100%; cursor: pointer;
}
.sel:hover { border-color: var(--ink-3); }
.sel:disabled { color: var(--ink-3); background: #F6F8FB; }

.chev {
  display: grid; place-items: center; flex: none; padding: 0;
  width: 28px; height: 28px; border: 1px solid var(--line-2); border-radius: 8px;
  background: var(--paper); color: var(--ink-2); cursor: pointer;
}
.chev svg {
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round; transition: transform .15s ease;
}
#colhead.open #brand-more svg { transform: rotate(180deg); }

/* A DISCLOSURE THAT IS VISIBLY PRESENT. The claim stays on the page and the mechanism behind it
   is one click away — never deleted, never silently dropped. This is the shape that made it
   possible to take ~200 words of always-visible prose down to ~25 without losing a sentence. */
.fold > summary, .method > summary {
  font: 600 10.5px/1 var(--mono); letter-spacing: .06em; color: var(--ink-3);
  cursor: pointer; padding: 4px 0; list-style: none;
}
.fold > summary::-webkit-details-marker, .method > summary::-webkit-details-marker { display: none; }
.fold > summary::before, .method > summary::before { content: '▸ '; }
.fold[open] > summary::before, .method[open] > summary::before { content: '▾ '; }
.fold > summary:hover, .method > summary:hover { color: var(--ink-2); }
.fold p, .method p { margin: 6px 0 0; font-size: 11px; line-height: 1.55; color: var(--ink-2); }
/* The search scope lives in a fold now rather than in a popover, so its rows need the spacing the
   popover's `gap: 11px` used to give them. */
#search-fold .ctl { margin-top: 9px; }

/* -- the key, which is a reading control and therefore lives in the column -------------------- */

.key { display: grid; gap: 0; }
/* All that is left of the old threshold row. The slider that used to sit here is now the mark on
   the ramp below, and the number it printed rides with that mark — one instrument, which is the
   whole of Jeff's ask. The caption stays because it is the one thing the ramp cannot say about
   itself: whose line this is. */
.thr { display: flex; align-items: center; gap: 8px; }
.thr label { font-size: 12px; color: var(--ink-2); flex: none; }

/* The reading's own row, 18 px directly above the ramp. Positioned from the LEFT EDGE in px by
   `placeMark()` rather than by a percentage with translateX(-50%): at 5 kt on a 40 kt scale the
   centred bubble hangs off the column, and at 35 kt it hangs off the other end. */
.rampread { position: relative; height: 17px; }
.thr-read {
  position: absolute; top: 0; left: 0; display: inline-flex; align-items: baseline; gap: 5px;
  white-space: nowrap; transition: left .06s linear;
}
.thr-read output { font: 700 13px/1 var(--mono); font-variant-numeric: tabular-nums; }
.thr-read .unit { font: 500 10.5px/1 var(--mono); color: var(--ink-3); }

.rampwrap { position: relative; padding-bottom: 48px; }

/* THE NATIVE RANGE, KEPT AND DRIVEN. Transparent and stretched over the ramp, so every pointer,
   touch and keyboard affordance is the browser's own and none of it is reimplemented. It is
   deliberately NOT `display:none` or `visibility:hidden` — either would take the keyboard and the
   screen reader with it, which is the whole reason the real input stays in the DOM.
   `syncThreshold()` sets its left/width to the span of the ramp its min..max covers. */
.thr-range {
  position: absolute; top: -11px; height: 34px; margin: 0; padding: 0;
  -webkit-appearance: none; appearance: none; background: none; cursor: pointer;
  z-index: 3; touch-action: none;
}
.thr-range:focus { outline: none; }
/* A 1 px thumb keeps the native track's half-thumb inset off the scale: the mark is drawn by
   `placeMark()` at the ramp's own percentage, and a 16 px thumb would inset the travel by 8 px at
   each end and walk the pointer off the mark. The thumb itself is never seen. */
.thr-range::-webkit-slider-thumb { -webkit-appearance: none; width: 1px; height: 34px; opacity: 0; }
.thr-range::-moz-range-thumb { width: 1px; height: 34px; border: 0; opacity: 0; }
.thr-range::-webkit-slider-runnable-track { background: none; border: 0; }
.thr-range::-moz-range-track { background: none; border: 0; }
/* Focus is shown ON THE MARK, because the mark is what the control looks like now. */
.thr-range:focus-visible ~ .thr-mark {
  box-shadow: 0 0 0 1.5px var(--halo), 0 0 0 4px color-mix(in srgb, var(--signal) 45%, transparent);
}
.ramp { height: 12px; border-radius: 3px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 12%, transparent); }

/* The key's copy of the map's lattice. Drawn with the same pitch (6 px) and the same two radii
   as tint.js, off the same two tokens, so the band over the scale and the speckle over the water
   are the same instrument. It sits UNDER `.thr-mark`: the orange mark is where the line is, and
   the band is which side of it counts. */
.tintband { position: absolute; top: 0; height: 12px; pointer-events: none; }
/* The legend's lattice swatch shares this declaration rather than copying it: one gradient, two
   surfaces, and a restyle of the map's tint cannot leave a legend showing last month's dots. */
.tintband.good, .lsample .lattice {
  background: radial-gradient(circle at 3px 3px, var(--strip-good) 0 1.5px, transparent 1.7px)
              0 0 / 6px 6px;
  border-radius: 0 3px 3px 0;
}
/* Much smaller dot and much lower opacity — a third of the green's ink. Both numbers are
   tint.js's, and the reasoning for them being this quiet is written there: the near-enough band
   is 3 kt wide, so it covers most of the map on a soft day, and its colour is a neighbour of the
   contour's own orange. */
.tintband.close {
  background: radial-gradient(circle at 3px 3px, var(--strip-close) 0 1px, transparent 1.2px)
              0 0 / 6px 6px;
  opacity: .55;
}
/* The ceiling band, and deliberately the GREEN's dot at the GREEN's weight: only the ink changes,
   because it is the same kind of statement about the same field. It takes the green's right-hand
   end rather than sitting over it — a cell on the map is one state, and a key that overlapped two
   lattices would be showing a thing the map cannot draw. */
.tintband.cap {
  background: radial-gradient(circle at 3px 3px, var(--strip-over) 0 1.5px, transparent 1.7px)
              0 0 / 6px 6px;
  border-radius: 0 3px 3px 0;
}

/* The threshold is the one annotation on the scale, and it is orange because orange is this
   project's annotation signal — warm against a cool field reads as opposite, not as a second
   data series. It sits ON the ramp so the slider and the line in the field are visibly the same
   number. */
.thr-mark {
  position: absolute; top: -4px; width: 3px; height: 20px; margin-left: -1.5px;
  background: var(--signal); border-radius: 2px;
  box-shadow: 0 0 0 1.5px var(--halo);
  /* The input above it is the hit target; the mark is only the picture of where it is. */
  pointer-events: none; z-index: 2;
}

/* The gear marks are STAGGERED in two rows. Six of them inside 324 px is about 54 px each and
   "foil or 17m" does not fit in 54 px — laid out in one row they collide, and a collided label is
   a label nobody reads. Alternate rows buy each one twice the width for 12 px of height. */
.gear { position: relative; height: 46px; margin-top: 3px; }
.gear span { position: absolute; transform: translateX(-50%); text-align: center; white-space: nowrap; }
/* The end marks are anchored to the column, not centred on their tick: half of "0 flat" would
   otherwise hang off the left edge. */
.gear span.first { transform: none; text-align: left; }
.gear span.first i { margin-left: 0; }
.gear span.lo { top: 23px; }
.gear i { display: block; width: 1px; background: var(--line-2); margin: 0 auto 2px; height: 4px; }
.gear span.lo i { height: 27px; margin-top: -25px; }
.gear b { display: block; font: 600 10px/1.2 var(--mono); font-variant-numeric: tabular-nums; }
.gear em { display: block; font-style: normal; font-size: 9px; color: var(--ink-3); }
.ends { position: absolute; right: 0; bottom: -4px; font: 500 9.5px/1 var(--mono); color: var(--ink-3); }

/* The ramp's own key, drawn only when the gear marks are folded away — a colour bar with no
   number on it is decoration, and two numbers make it a key again. */
.mini { display: none; justify-content: space-between; margin: 4px 0 0;
        font: 500 10px/1 var(--mono); color: var(--ink-3); }

/* THE CLAIM, ON ONE LINE. `#spotnote` was 90 words of always-visible 11 px prose — the single
   heaviest block on the page, and it outweighed the finding seven to one. Its claim is a count,
   so the count is what stays on screen; the paragraph that backs it is inside the method panel
   directly below, with every sentence intact. */
.keyclaim {
  margin: 10px 0 0; font: 500 10.5px/1.45 var(--mono); color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.method { margin-top: 2px; }

/* -- the spot's week, hour by hour ------------------------------------------------------------ *
 *
 * A MIRROR of forecast.php's phone hour list (Jeff, 2026-09-13: "it should mirror the forecast
 * from forecast.php"), so the class names, the grid and the two channels are that file's:
 * `assets/css/forecast.css` § PHONE HOUR LIST. The left 4 px band is the VERDICT and the bar is
 * the MAGNITUDE, and they must never merge — a bar that turned green when the hour passed would
 * say the same thing twice and leave strength unsaid.
 *
 * WHAT IS NOT COPIED IS THE PALETTE. That page is near-black; this plate is #F8FAFC, so every
 * token is this page's own and the ramp's lightness runs the other way (js/hours.js). A straight
 * copy would put the windiest hour in the palest ink on a white plate — the same inversion
 * forecast.php's own ramp comment records inheriting from the Shore Ribbon and having to undo. */

.dtabs { display: flex; gap: 6px; margin: 8px 0 4px; }
.dtabs button {
  flex: 1; padding: 7px 8px; font: inherit; font-size: 12px; font-weight: 600;
  color: var(--ink-2); background: transparent; cursor: pointer;
  border: 1px solid var(--line-2); border-radius: 8px;
}
.dtabs button.on { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.dtabs[hidden] { display: none !important; }

.hour-lead { margin: 10px 0 2px; font-size: 11.5px; color: var(--ink-2); }
.hour-lead b { color: var(--ink); font-variant-numeric: tabular-nums; }
.hour-foot { margin-top: 12px; }
.hour-empty { font-size: 12px; color: var(--ink-2); margin: 12px 0; }

.hour-day + .hour-day { margin-top: 18px; }
.hour-day-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 0 2px 6px; border-bottom: 1px solid var(--line); margin-bottom: 2px;
}
.hday-label { font-size: 14px; font-weight: 700; color: var(--ink); }
.hour-day-head.today .hday-label { color: var(--signal); }
.hday-right { display: flex; align-items: baseline; gap: 8px; }
.hday-on  { font-size: 11px; color: var(--strip-good); font-weight: 700; }
.hday-off { font-size: 11px; color: var(--ink-3); }
/* The day's five-tier call, in the ladder's own words. A chip, because it is the SITE's verdict
   over the scored day and the count beside it is these rows against the reader's own line — two
   statements, and neither may be dressed as the other. */
.hday-tier {
  font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: 2px 6px; border-radius: 999px; background: var(--strip-under); color: var(--ink-2);
}
.hday-tier.t-fire, .hday-tier.t-solid { background: rgba(16,185,129,.14); color: #047857; }
.hday-tier.t-go { background: rgba(16,185,129,.10); color: #059669; }
.hday-tier.t-eyes { background: rgba(232,153,27,.14); color: #9A6410; }

/* Night is folded until asked for: a 24-row day is otherwise eight rows of nothing. */
.hour-rows .hour-row.is-night { display: none; }
.hour-rows.show-night .hour-row.is-night { display: grid; }

.hour-row {
  display: grid;
  /* band · time · sky · wind · wave · temp. The wind column carries a floor because wave and temp
     are both `auto`: with gust, rain, wave, period and temp all present they take the row and the
     wind number — the one thing the row exists to say — is squeezed to nothing. */
  grid-template-columns: 4px 56px 18px minmax(96px, 1fr) auto auto;
  align-items: center; column-gap: 8px;
  width: 100%; padding: 7px 8px 7px 0; position: relative; text-align: left;
  background: none; border: 0; border-bottom: 1px solid var(--line);
  font: inherit; color: inherit; cursor: pointer;
}
.hour-row:last-child { border-bottom: none; }
.hour-row:hover { background: color-mix(in srgb, var(--ink) 3%, transparent); }
.hour-row:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }

/* The band, carried 1 px past the row's own bottom border so a run of rideable hours fuses into
   ONE unbroken stripe — a stripe with hairline gaps in it reads as five marks, not one window. */
.hour-band { position: absolute; left: 0; top: 0; bottom: -1px; width: 4px; background: transparent; }
.hour-row:last-child .hour-band { bottom: 0; }
.hour-row.is-on   .hour-band { background: var(--strip-good); }
.hour-row.is-near .hour-band { background: var(--strip-close); opacity: .55; }

.hour-time {
  font-size: 11.5px; font-weight: 600; color: var(--ink-2);
  font-variant-numeric: tabular-nums; padding-left: 8px;
  /* One line. "11 AM" wrapping to two makes a 24-row day two hundred pixels taller and puts the
     hour number and its meridiem on different rows of a list read by scanning down it. */
  white-space: nowrap;
}
.hour-row.is-on .hour-time { color: var(--ink); }

/* `.hour-primary` is the wide (1fr) cell and `.hour-secondary` the compact one; WHICH quantity sits in
   each is what the element switch decides (hours.js). Layout is addressed by position so the
   quantity can keep its own class — `hour-wind` / `hour-wave` — wherever it lands. */
.hour-primary { display: grid; grid-template-columns: auto auto auto 1fr; align-items: baseline; column-gap: 4px; min-width: 0; }
.hour-arrow { font-size: 15px; line-height: 1; color: var(--ink-2); }
.hour-kt { font-size: 21px; font-weight: 700; line-height: 1.1; color: var(--ink); font-variant-numeric: tabular-nums; }
.hour-row.is-on .hour-kt { color: #047857; }
.hour-unit { font-size: 10.5px; color: var(--ink-3); font-weight: 500; }
/* Gust, direction and rain are modifiers on the wind number, not peers. */
.hour-sub {
  grid-column: 1 / -1; font-size: 10.5px; color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px;
}
/* The slot the sky icon will land in when WS_SKY_ICONS moves out of forecast.php into something
   shared. Held open rather than removed: the row's geometry is the mirror. */
.hour-sky { display: flex; align-items: center; justify-content: center; line-height: 0; }

.hour-bar {
  display: block; height: 6px; border-radius: 3px; min-width: 40px;
  background: color-mix(in srgb, var(--ink) 4%, transparent); overflow: hidden; align-self: center;
}
.hour-bar i { display: block; height: 100%; border-radius: 3px; }

.hour-precip {
  display: inline-flex; align-items: center; gap: 3px; margin-left: 6px; padding: 0 5px;
  border-radius: 6px; font-size: 9.5px; font-weight: 600; line-height: 1.5;
  background: color-mix(in srgb, var(--ink) 6%, transparent); color: var(--ink-2);
}
.hour-precip.precip-high { color: #B91C1C; background: rgba(185,28,28,.10); }
.hour-precip.precip-med  { color: #C2410C; background: rgba(194,65,12,.10); }
.hour-precip.precip-warn { color: #9A6410; background: rgba(232,153,27,.12); }
.hour-precip.precip-low  { opacity: .65; }

/* NOW is the clock; the ring is the PLAYHEAD. Two marks, two meanings, and deliberately not the
   left 4 px, which already belongs to the verdict band. */
.hour-row.is-now { background: color-mix(in srgb, var(--ink) 5%, transparent); }
.hour-row.is-now .hour-time { color: var(--ink); }
.hour-now-tag { display: block; font-size: 8.5px; font-weight: 800; letter-spacing: .08em; color: var(--ink-2); }
.hour-row.is-sel { box-shadow: inset 0 0 0 1.5px var(--signal); border-radius: 6px; }

.hour-temp-inline { display: none; margin-left: 6px; }
.hour-secondary, .hour-temp { font-size: 12px; font-weight: 600; color: var(--ink-2); font-variant-numeric: tabular-nums; text-align: right; }
.hour-secondary { display: grid; justify-items: end; row-gap: 1px; }
.hour-val { white-space: nowrap; }
.hour-val .hour-unit { margin-left: 2px; }
.hour-secondary .hour-sub { grid-column: auto; margin-top: 0; }
.hour-temp { min-width: 32px; }

.hour-night-toggle {
  display: block; width: 100%; margin-top: 6px; padding: 8px;
  background: transparent; border: 1px solid var(--line); border-radius: 8px;
  color: var(--ink-3); font: inherit; font-size: 11px; cursor: pointer;
}
.hour-night-toggle:hover { color: var(--ink-2); }

/* Below ~360px the wave and temp columns take the row. Temp moves into the sub-line, where it is
   still read but no longer competes with the wind — forecast.css makes the same move at the same
   width, because it is the same row. */
@media (max-width: 360px) {
  .hour-row { grid-template-columns: 4px 46px 16px minmax(88px, 1fr) auto; column-gap: 6px; }
  .hour-row .hour-temp { display: none; }
  .hour-temp-inline { display: inline; }
}

/* -- the map's furniture --------------------------------------------------------------------- *
 *
 * THERE IS NO SECOND PANEL (2026-09-13). `.optwrap` / `#options` — the Options button in the map's
 * top-right and the 288 px popover under it — are deleted, along with the phone rules that had to
 * lift them over the sheet and bound them under it. What is left over the map is the timeline, the
 * pin's clear button and the back button: one control each, and none of them covers anything.
 * See README §0o. */

#unpin { position: absolute; right: 12px; top: 54px; z-index: 5;
         background: var(--card); backdrop-filter: blur(8px); box-shadow: var(--shadow); }
#unpin[hidden] { display: none !important; }

.backbtn {
  position: absolute; left: 50%; top: 12px; transform: translateX(-50%); z-index: 5;
  font-size: 12.5px; font-weight: 600;
  background: var(--card); backdrop-filter: blur(8px); box-shadow: var(--shadow);
}

/* -- timeline: the map's only floating instrument --------------------------------------------- */

.hud {
  position: absolute; z-index: 4;
  background: var(--card); backdrop-filter: blur(8px);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 12px 14px;
}
.timeline {
  left: 50%; transform: translateX(-50%); bottom: 30px;
  width: min(640px, calc(100% - 80px));
  display: flex; align-items: center; gap: 12px; padding: 10px 16px 12px;
}
.play {
  width: 34px; height: 34px; flex: none; border-radius: 50%;
  border: 1px solid var(--line-2); background: var(--paper); color: var(--ink);
  display: grid; place-items: center; cursor: pointer;
}
.play svg { fill: currentColor; }
.track { position: relative; flex: 1; height: 44px; cursor: pointer; outline: none;
         touch-action: none; }
.track:focus-visible { box-shadow: 0 0 0 2px rgba(234,88,12,.4); border-radius: 6px; }
.days { position: absolute; inset: 0 0 auto; height: 14px; }
.days span { position: absolute; transform: translateX(1px); font: 600 10px/1 var(--mono); color: var(--ink-2); }
.days i { position: absolute; top: 14px; width: 1px; height: 12px; background: var(--line-2); }

/* The NDFD day-3 seam. Everything right of it is 6-hourly and was issued hours earlier, so it
   gets a rule of its own rather than being drawn as more of the same timeline. Dashed, and in
   the annotation colour, because it is a note about the data and not a reading. */
.days i.seam { top: 0; height: 26px; width: 0;
               border-left: 1px dashed var(--signal); background: none; opacity: .85; }
/* RIGHT-aligned, to the LEFT of the rule. Day names are placed at the start of each day, and the
   seam lands on a day boundary far more often than not — put the label to the right of the rule
   and it sits on top of the next day's name, which is how it first shipped ("Sun6HOURLY"). The
   space before a boundary is the tail of the previous day and is always empty. */
.days .seamlab { position: absolute; top: 0; transform: translateX(calc(-100% - 4px));
                 font: 700 9px/1 var(--mono); font-style: normal; letter-spacing: .04em;
                 white-space: nowrap; text-transform: uppercase; color: var(--signal); }
.rail { position: absolute; left: 0; right: 0; top: 21px; height: 5px; background: var(--line); border-radius: 3px; }
.fill { height: 100%; background: var(--ink); border-radius: 3px; }
.knob {
  position: absolute; top: 17px; width: 13px; height: 13px; margin-left: -6.5px;
  border-radius: 50%; background: var(--paper); border: 2px solid var(--ink);
}
.ticks { position: absolute; left: 0; right: 0; top: 30px; height: 12px; }
.ticks span { position: absolute; transform: translateX(-50%); font: 400 9.5px/1 var(--mono); color: var(--ink-3); }

/* -- the dock: the focused spot's hours as columns, under the track ---------------------------
   One column per FRAME, centred on the tick's own `i/(n-1)` (dock.js says why not per hour). It
   lives INSIDE the track, so the track grows to hold it and the HUD widens so a column gets the
   width forecast.php's `.hour-cell` gets (28 px; 22 on a 1440 screen with the Next-match button
   up). Widened only while a spot is focused: the 640 px instrument is what ships with no spot.
   Capped short of the zoom control and the attribution — 75 px a side clears both. */
body.has-dock .timeline { width: min(1100px, calc(100% - 150px)); align-items: flex-start; }
body.has-dock .play { margin-top: 6px; }
body.has-dock .nextmatch { margin-top: 8px; }
body.has-dock .timeline .track { height: 156px; }  /* outranks `body.has-band .track` below */
.dock-unit {
  position: absolute; right: 0; top: 53px; font-style: normal; font-size: 9.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink-3); white-space: nowrap;
}
.dock { position: absolute; left: 0; right: 0; top: 68px; height: 86px; }
.dc {
  position: absolute; top: 0; height: 100%; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
  padding: 2px 0 0; box-sizing: border-box; border-radius: 4px;
}
/* The bar is forecast.php's: a faint well, the fill from the floor, the gust as a ghost behind. */
.dc-bar {
  position: relative; width: calc(100% - 3px); height: 44px; overflow: hidden;
  background: color-mix(in srgb, var(--ink) 5%, transparent); border-radius: 3px;
}
.dc-bar i { position: absolute; left: 0; right: 0; bottom: 0; border-radius: 3px 3px 0 0; }
/* The gust is a CAP, not a second bar: a 2 px rule at the gust's height over the wind's fill,
   which at 22 px wide reads as "the gust reaches here" where a hatched ghost read as a smudge. */
.dc-gust { background: none; border-top: 2px solid color-mix(in srgb, var(--ink) 42%, transparent); border-radius: 0; }
.dc-arrow { font-size: 10px; line-height: 12px; height: 12px; color: var(--ink-2); }
.dc-val   { font: 700 10.5px/12px var(--mono); color: var(--ink); }
.dc-sub   { font: 500 9px/11px var(--mono); color: var(--ink-3); min-height: 11px; }
.dc.is-empty .dc-val { color: var(--ink-3); font-weight: 500; }
/* The two states hours.js draws, in the same two ways: on is a band, near is the band at half. */
.dc.is-on   { box-shadow: inset 0 -2px 0 var(--strip-good); }
.dc.is-near { box-shadow: inset 0 -2px 0 var(--strip-close); }
/* Night is dimmed, not folded — there is no room under a track for a toggle, and a dim column
   still says its number. */
.dc.is-night { opacity: .48; }
.dc.is-sel   { background: color-mix(in srgb, var(--ink) 6%, transparent); opacity: 1; }
.dc.is-sel .dc-val { color: var(--signal); }
.dc:hover    { background: color-mix(in srgb, var(--ink) 4%, transparent); }
/* Under 15 px a column: bars only (renderDock() measures and sets this). */
.dock.thin .dc-arrow, .dock.thin .dc-val, .dock.thin .dc-sub { display: none; }
.dock.thin { height: 50px; }
body.has-dock .timeline .track:has(.dock.thin) { height: 120px; }

/* -- Explore's search box, in the head (2026-09-18, js/search.js) --------------------------- */

.mapsearch { position: relative; display: flex; align-items: center; margin-top: 8px; color: var(--ink-3); }
.mapsearch svg { position: absolute; left: 10px; pointer-events: none; }
.mapsearch input {
  width: 100%; box-sizing: border-box; height: 36px; padding: 0 10px 0 32px;
  border: 1px solid var(--line-2); border-radius: 9px; background: var(--paper); color: var(--ink);
  font: 500 13px/1 var(--sans); -webkit-appearance: none; appearance: none;
}
.mapsearch input::placeholder { color: var(--ink-3); }
.mapsearch input:focus { outline: none; border-color: var(--signal); box-shadow: 0 0 0 3px color-mix(in srgb, var(--signal) 22%, transparent); }
.mapsearch input::-webkit-search-cancel-button { -webkit-appearance: none; }
.sr-list {
  position: absolute; z-index: 9; left: 0; right: 0; top: calc(100% + 5px); display: none;
  background: var(--paper); border: 1px solid var(--line-2); border-radius: 10px; overflow: hidden;
  box-shadow: var(--shadow);
}
.sr-list.open { display: block; }
.sr { display: flex; align-items: stretch; border-bottom: 1px solid var(--line); }
.sr:last-of-type { border-bottom: 0; }
.sr-go {
  flex: 1; min-width: 0; display: grid; grid-template-columns: 1fr auto; column-gap: 8px;
  padding: 8px 10px; border: 0; background: none; color: var(--ink); text-align: left; cursor: pointer;
  font: inherit;
}
.sr-go .sr-name { grid-column: 1; font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sr-go .sr-sub  { grid-column: 1; font-size: 11px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sr-go .sr-kind { grid-column: 2; grid-row: 1 / span 2; align-self: start; font: 600 9px/1.2 var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3); padding-top: 3px; }
.sr:hover, .sr.active { background: color-mix(in srgb, var(--ink) 5%, transparent); }
.sr-full {
  flex: none; display: flex; align-items: center; padding: 0 10px; border-left: 1px solid var(--line);
  font: 600 11px/1 var(--sans); color: var(--signal); text-decoration: none; white-space: nowrap;
}
.sr-full:hover { background: color-mix(in srgb, var(--signal) 10%, transparent); }
.sr-attrib { font: 500 9.5px/1.3 var(--sans); color: var(--ink-3); padding: 5px 10px; }
/* The pinned readout carries a link once a search put the pin there, so it takes the pointer. */
.readout.pinned { pointer-events: auto; }
.readout .place { display: block; font-weight: 700; margin-bottom: 2px; }
.readout .full { display: inline-block; margin-top: 4px; color: #FFC08A; font-weight: 600; text-decoration: none; }
.readout .full:hover { text-decoration: underline; }

/* -- readout -------------------------------------------------------------------------------- */

.readout {
  /* Absolute in #pane, not fixed: it is placed off `map.project()`, which is container-relative,
     and since the site header sits above the pane (2026-09-18) the two frames differ by its height. */
  position: absolute; z-index: 7; pointer-events: none;
  transform: translate(14px, -50%);
  background: rgba(15,27,42,.93); color: #fff;
  border-radius: 8px; padding: 7px 10px; font-size: 12px; line-height: 1.45;
  white-space: nowrap; box-shadow: var(--shadow);
}
.readout b { font: 700 14px/1.3 var(--mono); }
/* THE METER'S CARD — what a live disc says when tapped. Anchored to the disc and moved with the
   map like the pin's readout, but it is a control (a link out to the meter's live page and a
   close button), so it takes pointer events and wraps a long NDBC name instead of running off a
   phone. Sits inside the pane, positioned from map.project(), which is container-relative;
   .readout is fixed to the viewport and is a different anchor. */
.metercard {
  position: absolute; z-index: 7; pointer-events: auto; white-space: normal;
  max-width: 240px; transform: translate(14px, -50%); padding: 8px 12px 9px 10px;
}
.metercard.flip { transform: translate(calc(-100% - 14px), -50%); }
.metercard .mc-name { display: block; font: 600 12px/1.3 var(--sans); margin-right: 18px; }
.metercard .mc-id { font: 500 10px/1 var(--mono); letter-spacing: .06em; color: rgba(255,255,255,.7); }
.metercard .mc-kt { font: 700 20px/1.1 var(--mono); color: #FFC08A; margin: 4px 0 1px; }
.metercard .mc-kt small { font: 600 12px/1 var(--mono); color: #fff; margin-left: 3px; }
.metercard .mc-row { font: 500 11.5px/1.45 var(--sans); color: rgba(255,255,255,.88); }
.metercard a { color: #FFC08A; font: 600 12px/1.6 var(--sans); text-decoration: underline; text-underline-offset: 2px; }
.metercard .mc-x {
  position: absolute; right: 4px; top: 3px; width: 22px; height: 22px; padding: 0; border: 0;
  background: transparent; color: rgba(255,255,255,.75); font: 600 15px/1 var(--sans); cursor: pointer;
}
.readout .yes { color: #FFC08A; }
/* An NWS product named in the readout wears its own colour as a leading bar, not as its text —
   thistle and yellow on navy are unreadable as type, and the bar is the outline's colour said
   once more where the reader is looking. */
.readout .warn { font-weight: 700; padding-left: 8px; border-left: 3px solid var(--wc, #fff); }
.readout .no { color: #A9B8CB; }
.readout.pinned { transform: translate(14px, -110%); }

/* -- markers -------------------------------------------------------------------------------- */

/* ONE PILL PER SPOT, THE METERS' OWN CHASSIS (2026-09-15). The spot marker was a 9 px dot with a
   white plate beside it carrying the number; the live meters are a filled pill with the number
   in it, and Jeff read the two side by side: the pills are easy to read, the dot-and-bubble is
   not — "use the same setup for both but make my stations look slightly different with a
   different color". So `.pill` here is `.obsmark` to the pixel — 20 px tall, 999 px radius,
   white keyline, the same two-size mono pair — and the colour is the whole difference: NAVY for a
   forecast, the meters' ORANGE only when the pin is wearing its own meter's live reading (§0w,
   where the two are meant to read as one statement), and the correction ring stays an orange
   ring, because a ring was already "a property of the marker" rather than a second marker.
   The plate beside it now carries only what a pill has no room for, and only at named zoom. */
.spot { display: flex; align-items: center; gap: 5px; cursor: default; }
.spot .pill {
  position: relative; min-width: 20px; height: 20px; border-radius: 999px; padding: 0 6px;
  display: flex; align-items: center; justify-content: center; flex: none;
  background: #0F1B2A;
  box-shadow: 0 0 0 1.5px #fff, 0 1px 4px rgba(0,0,0,.4);
}
.spot.corrected .pill {
  box-shadow: 0 0 0 1.5px #fff, 0 0 0 3px var(--signal), 0 1px 4px rgba(0,0,0,.4);
}
/* The label is a PLATE, not stroked text over the map. Text-stroke was enough on v2's bare
   number, but our label lands within a few pixels of the basemap's own name for the same town —
   "Muskegon" printed twice, overlapping, one of them ours — and two labels fighting for the same
   twelve pixels is unreadable however heavy the halo. A plate says which one is the forecast. */
.spot .lab {
  display: none; line-height: 1.2;
  background: var(--glass); border: 1px solid var(--line-2);
  border-radius: 7px; padding: 3px 7px; box-shadow: 0 1px 5px var(--shade-2);
}
/* The plate only where there is room, like everything on it: the number it used to hold is in
   the pill now, so at basin zoom the marker IS the pill. */
body.named .spot .lab { display: grid; }
.spot.corrected .lab { border-color: rgba(234,88,12,.45); }
.spot .val { position: relative; display: flex; align-items: baseline; gap: 2px; }
.spot .kt { font: 700 10px/1 var(--mono); font-variant-numeric: tabular-nums; color: #fff;
            letter-spacing: -.02em; }
/* The unit rides with the number. "13" beside a town on a weather map could be a temperature, a
   wave height or a time; "13 kt" could not. */
.spot .u { font: 600 8px/1 var(--mono); color: rgba(255,255,255,.82); }
.spot .nm { font-size: 9.5px; font-weight: 600; letter-spacing: .02em; white-space: nowrap;
            color: var(--ink-2); }
.spot .fix { font: 600 9px/1.2 var(--mono); letter-spacing: .01em; white-space: nowrap;
             color: var(--signal); font-variant-numeric: tabular-nums; }
/* Names and the size of the correction only where there is room: seven spots share ninety miles
   of one shore, and at basin zoom their labels would overlap into a smear. */
.spot .nm, .spot .fix { display: none; }
body.named .spot .nm { display: block; }
body.named .spot.corrected .fix { display: block; }
/* The wetsuit rung under a water temperature (2026-09-15): the same line, in plain ink rather
   than the correction's orange, because it is a reading of the legend and not a claim about a
   meter. */
body.named .spot.suited .fix { display: block; color: var(--ink-2); }
/* A rider's number crossing their own line is worth saying twice — the marker as well as the
   contour — because the marker is the number they will actually read. On the NUMBER, not the
   fill: the fill is spent on whose marker this is (navy = your spot, orange = a meter), and a
   green pill among orange ones would be a third kind of marker rather than a state of this one. */
.spot.over .kt { color: #86EFAC; }
/* ON RADAR a pin with no live number has nothing to say but "here is your spot", and an empty
   20 px pill says it as a black blob over the echoes. It shrinks to a dot; a live reading
   restores the pill (`updateMarkers()` sets `nonum`). */
.spot.nonum .pill { min-width: 9px; height: 9px; padding: 0; }

.pinmark {
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--signal); box-shadow: 0 0 0 2.5px #fff, 0 2px 6px rgba(0,0,0,.35);
}

/* -- live meter readings ------------------------------------------------------------------------
   A FILLED PILL, never a barb — a barb sits 0.173 dE from this field (README §0d) and a live
   reading must not be mistaken for a forecast direction glyph. `--signal` orange because it is
   already this page's one annotation channel (the correction ring, the contour, the tint's cap
   band) and warm against a cool field is the whole of that channel's argument.

   IT WAS A 20 px DISC WITH A BARE INTEGER IN IT until 2026-09-15 (Jeff: "discs become '18 kt'
   pills in the discs' orange"). Same shape as `.spot .lab`, the live spot pin's plate, for the
   same reason that plate has carried its unit since v3: "13" beside a town on a weather map
   could be a temperature, a wave height or a time, and "13 kt" could not. The pill is sized by
   its text, so a 9 and a 24 both sit in one. */
.obsmark {
  /* A CONTROL, not annotation, since 2026-09-15 (Jeff: "why can't I click on the real time wind
     meters. It's natural to want to do that"). Tapping one opens #metercard. */
  min-width: 20px; height: 20px; border-radius: 999px; padding: 0 6px;
  pointer-events: auto; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  /* NO `position` here. The disc IS the MapLibre marker element, which is `position: absolute`
     and moved by a transform; `position: relative` (c3db379, 2026-09-14) overrode that on the
     same element and put every disc back in the map container's FLOW — each one 20 px below the
     last, plus its transform — so the longitudes were right and the latitudes marched south in
     file order: Saginaw Bay's meter over the Yucatán on Jeff's phone (2026-09-15). The `.dot`
     inside is `inset: 0` against the marker's own absolute box, which is what it wants. */
}
/* The fill is this element, absolutely placed against the marker's own box — NOT a background on
   `.obsmark`, which would need a `position` on the marker element and that is the one thing this
   block must never grow (see the note above). `inset: 0` stretches it to whatever width the text
   gave the pill. */
.obsmark .dot {
  position: absolute; inset: 0; border-radius: 999px; background: var(--signal);
  box-shadow: 0 0 0 1.5px #fff, 0 1px 4px rgba(0,0,0,.4);
}
/* -- the spot pin at NOW: its own meter's reading in it (§0w) -----------------------------------
   Jeff, at the now frame: "is it showing the now wind speed for my spots? My spots look different
   from the rest so it's hard to tell." They DID look different — the live discs were orange and
   the spot pins were the forecast, side by side, at the one hour a reader most wants the measured
   number. A live pin now wears the discs' own orange so the two read as one statement, and the
   forecast it replaces moves under it rather than disappearing. */
/* A LIVE PIN STAYS NAVY. §0w turned a live pin orange so it would read as one statement with
   the meters, back when the two chassis differed; now the chassis is the same and the colour is
   the ONLY thing telling a reader whose marker it is (Jeff, 2026-09-15), so it is not spent
   twice. That the number is live is said where it can be read: the `fcst` line under it on the
   plate, the plate's own warm tint, and the legend's "your spot, reading now" row. */
.spot.livenow .lab { border-color: rgba(234,88,12,.55); background: color-mix(in srgb, var(--glass) 88%, var(--signal)); }
.spot .fcst { display: none; font: 600 9px/1.2 var(--mono); color: var(--ink-3); white-space: nowrap; }
body.named .spot.livenow .fcst { display: block; }

/* -- THE MAP'S OWN CONTROLS — phone only (§0w) --------------------------------------------------
   The element switch and the key button. Tiny on purpose: §0o's invariant is about fixed PANELS
   over the map (40,000 px²) and this is a 132x30 switch beside a 30x30 button. They are here
   rather than in the sheet because they act on the MAP, and a control you have to open a drawer
   to reach is a control nobody uses. */
#mapctl { display: none; }

/* The two phone views' back links. A desktop column has no views to go back from. */
#key-close, #else-close { display: none; }

/* Discovery's phone door. Hidden on a desktop, where the whole "Where else?" section is on screen
   and nobody has to travel to reach it; `[hidden]` still wins inside the phone block, because a
   door to an empty room is worse than no door. */
.discoline { display: none; }

/* -- "these are current conditions", said on the map (§0v) -------------------------------------
   Orange because the discs are orange: the badge and the marks it vouches for are one statement,
   and colour is the cheapest way to say so. Absolutely positioned inside the pane and
   pointer-events:none — annotation, like the barbs and the contour, never a control. It is shown
   only when discs were actually drawn, so it can never sit over a forecast frame. */
.livebadge {
  /* Second row, under the scale and the switches: the badge is annotation and those are
     controls, and on a 390-px phone it is wider than the room beside them — it used to sit ON
     the element switch (Jeff, 2026-09-15). */
  position: absolute; left: 12px; top: 52px; z-index: 3; pointer-events: none;
  display: flex; align-items: center; gap: 6px;
  background: var(--glass); border: 1px solid rgba(234,88,12,.45);
  border-radius: 999px; padding: 4px 10px 4px 8px;
  font: 700 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-2); white-space: nowrap;
  box-shadow: 0 1px 6px var(--shade-2);
}
.livebadge i {
  width: 7px; height: 7px; border-radius: 50%; background: var(--signal); flex: none;
  box-shadow: 0 0 0 2px rgba(234,88,12,.20);
}
/* THE BADGE GREW A SECOND COUNT (§0z) AND RAN OFF A 390 px PHONE — measured at 397 px against a
   366 px shelf with the real CONUS file in it. It is one line by construction (`nowrap`), and it
   has to stay one: wrapped at this width it is ~12,400 px², over §0o's 12,000 px² line between
   annotation and a panel. So the type tightens on a narrow screen rather than the sentence
   losing a word — every count on this page is load-bearing, and the tracking is not. */
@media (max-width: 460px) {
  .livebadge { font-size: 9px; letter-spacing: .04em; padding: 4px 8px 4px 7px; gap: 5px; }
}
/* -- A STATIC ELEMENT: what it does not have, and the one line that takes its place ------------
   2026-09-15, README §1b. The Water element is a daily satellite analysis — one raster, no lead,
   nothing to scrub — so the playhead, the day chips, the dock and the hour list are HIDDEN rather
   than disabled (§0o: a control the page can answer for should not exist), along with everything
   built out of a rider's LINE, which a temperature does not have.

   ONE CLASS, not a dozen `hidden` flags, and that is the whole reason switching back restores the
   page exactly: what CSS hid, CSS gives back. The flags would be set by five different render
   functions and cleared by four of them. */
body.static-elem .timeline,
body.static-elem #daypick,
body.static-elem #dock,
body.static-elem #dock-unit,
body.static-elem .thr,
body.static-elem #rampread,
body.static-elem .rampwrap .thr-range,
body.static-elem .rampwrap .thr-mark,
body.static-elem .rampwrap .tintband,
body.static-elem .mapkey .mk-mark,
body.static-elem .mapkey .mk-val,
body.static-elem #linebtn,
body.static-elem #q-else,
body.static-elem .discoline,
body.static-elem #detail-tabs [data-tab="hours"],
body.static-elem #colhead .valid { display: none !important; }

/* THE LINE, IN THE TIMELINE'S OWN PLACE. Same position, same plate, same absolute-inside-the-pane
   rule as the badge above — annotation, so no pointer events and nothing to tap. It wraps where a
   timeline would not, because it is a sentence and a phone is 390 px; two lines of 10 px type is
   ~7,000 px² at that width, inside §0o's 12,000 px² line between annotation and a panel. */
.staticline {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 30px; z-index: 3;
  width: min(640px, calc(100% - 80px)); margin: 0; pointer-events: none;
  background: var(--glass); border: 1px solid var(--line-2); border-radius: 10px;
  padding: 8px 12px; box-shadow: 0 1px 8px var(--shade-2);
  font: 500 11px/1.45 var(--sans); color: var(--ink-2); text-align: center;
}

/* THE RADAR BAR wears the static line's plate and gets its pointer events back: it is a control.
   Dots are the eleven frames, oldest left; the lit one is the frame on the map. */
.radarbar {
  display: flex; align-items: center; gap: 10px; pointer-events: auto; text-align: left;
  width: auto; max-width: min(640px, calc(100% - 80px)); padding: 6px 12px 6px 6px;
}
.radarbar .play { width: 28px; height: 28px; }
.radar-dots { display: flex; gap: 2px; }
.radar-dots button {
  width: 18px; height: 24px; padding: 0; border: 0; background: transparent; cursor: pointer;
  display: grid; place-items: center;
}
.radar-dots button::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: var(--line-2); transition: transform .12s;
}
.radar-dots button.on::before { background: var(--ink); transform: scale(1.45); }
.radar-dots button:last-child::before { box-shadow: 0 0 0 1.5px var(--ink); background: var(--paper); }
.radar-dots button:last-child.on::before { background: var(--ink); }
.radar-when { white-space: nowrap; font-weight: 600; color: var(--ink); }

/* The same claim on the control that selects the frame, so the two cannot disagree about which
   hour is now. A dot, not a word: the track is 290 px on a phone and already carries days. */
.knob.isnow::after {
  content: ''; position: absolute; left: 50%; top: -7px; width: 5px; height: 5px;
  margin-left: -2.5px; border-radius: 50%; background: var(--signal);
}
.valid.isnow::after {
  content: ' · NOW'; color: var(--signal); font-weight: 700; letter-spacing: .14em;
}

/* `.obsmark.stale` WAS HERE — first a grey filled disc, then a hollow ring (§0t). A stale meter
   is not drawn at all since §0v, so there is no mark left to style: Jeff, on the rings, "they
   just look like some weird ass artifact", and he was right that a mark carrying no number and no
   reading is our plumbing drawn on the weather. The count lives in the legend now, and a spot
   whose own meter has gone quiet still says so in its hour list, which is the one place it
   changes what a rider does. */
.obsmark .v {
  position: relative; display: flex; align-items: baseline; gap: 2px;
  font: 700 10px/1 var(--mono); color: #fff;
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
/* The unit, smaller and quieter than the reading — the same two-size pair `.spot .val` uses, so
   the live pill and the forecast plate read as one family rather than as two conventions. */
.obsmark .u {
  font: 600 8px/1 var(--mono); color: rgba(255,255,255,.82); letter-spacing: 0;
}

/* The now-row's own reading, patched in beside the forecast number — see syncNowLive(). Its own
   row (grid-column spans the whole primary cell, same as .hour-sub) rather than crowding the
   number itself, which is still the thing a reader scans the column for. */
.hour-live {
  grid-column: 1 / -1; display: block; font: 700 10px var(--mono); color: var(--signal);
  margin-top: 2px; letter-spacing: 0;
}
.hour-live i { font-style: italic; font-weight: 500; color: var(--ink-3); }
.hour-live.stale { color: var(--ink-3); }
.hour-live.stale b { color: var(--ink-2); }

/* -- the list, which is permanent and is not a modal ------------------------------------------
 *
 * `#sheet` was a `position: fixed; inset: 0` scrim that covered the whole map while a reader
 * chose where to go — the one moment they most want to see the map. It is now a section of the
 * column, and `#detail` REPLACES it in place rather than sliding over anything.
 *
 * The ids and the row markup are unchanged on purpose: `render-test/call-check.js` counts
 * `#places .place` and `.spot` before and after every operation, and those counts are the proof
 * that this layout hid nothing. A rename would have made the proof unavailable.
 */

#sheet, #detail { display: block; }
#sheet[hidden], #detail[hidden] { display: none !important; }
.sheet-in { display: block; }
.sheet-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sheet-top .spacer { flex: 1; }
.sheet-top h2 { font-size: 15px; font-weight: 600; }
/* On a desktop the list is simply there, so there is nothing to close. On a phone it drops the
   sheet back to its peek. */
#close-places { display: none; }

.backlink {
  border: 0; background: none; padding: 0 0 8px; cursor: pointer;
  font: 600 10.5px/1 var(--mono); letter-spacing: .06em; color: var(--ink-3);
}
.backlink:hover { color: var(--ink); }

.places { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 5px; }
.place {
  width: 100%; min-width: 0; text-align: left; cursor: pointer; font: inherit;
  border: 1px solid var(--line); border-radius: 10px; background: var(--paper);
  padding: 9px 11px; display: grid; gap: 5px;
  grid-template-columns: 1fr; align-items: center;
}
.place:hover { border-color: var(--ink-3); }
.pname { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 5px;
         flex-wrap: wrap; min-width: 0; }
.pwho { min-width: 0; }
.ctag {
  font-style: normal; font: 600 9.5px/1.4 var(--mono); font-variant-numeric: tabular-nums;
  color: var(--signal); background: rgba(234,88,12,.09); border-radius: 5px;
  padding: 2px 5px; white-space: nowrap; flex: none;
}
/* -- the row's week strip ----------------------------------------------------------------------
 *
 * THE MAP'S RAMP DOES NOT COME DOWN HERE, deliberately (Jeff, 2026-09-13: "these little candy bar
 * strips are too hard to read on their own"). Six bands of one hue are legible across a basin and
 * illegible across eleven pixels: at that size the ramp says something about every hour and
 * nothing about the question the list is answering, which is whether a spot is ON. So the strip
 * speaks the dashboard's language instead — `.spot-tl-seg` in `dashboard.php`, green over the
 * line and amber within reach of it — and carries the one state a forecast grid has that a
 * dashboard row does not: no forecast at all, which is a gap and not a calm.
 *
 * `gap: 0` is load-bearing. A cell is as wide as the HOURS it stands for, and the ruler above the
 * list is flexed the same way; per-cell gaps would accumulate differently in a 3-hourly day (7 of
 * them) than in a 12-hourly one (1), and the ruler would drift off the days it names.
 */
.pstrip { display: flex; gap: 0; height: 12px; }
.pstrip i { flex: 0 1 0; min-width: 0; background: var(--strip-under); }
.pstrip i:first-child { border-radius: 2px 0 0 2px; }
.pstrip i:last-child { border-radius: 0 2px 2px 0; }
.pstrip i.good { background: var(--strip-good); }
.pstrip i.close { background: var(--strip-close); opacity: .72; }
/* Past the ceiling. A FILL, which is what keeps it clear of `.none` below — that one is a
   hairline box over nothing, and the two states are opposites: this hour has a forecast and it is
   a big one. */
.pstrip i.over { background: var(--strip-over); }
/* Nothing forecast here — an empty cell, not a faint one. Gust stops at day 3 and wave a few
   frames short of wind, and a strip that drew those as "under the line" would be inventing a
   calm. Boxed rather than blank so the reader can see the strip continues. */
.pstrip i.none { background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 6%, transparent); }
.pstrip i.dcut { margin-left: 3px; }
/* The playhead. A 3-hour cell is SIX pixels wide on a phone, so neither an outline (painted
   outside the box, overlapping both neighbours) nor a ring (1.5 px of ink each side of a 6 px
   cell is a black square) survives at that size. A tick along the bottom edge does: it marks the
   column without repainting it. */
.pstrip i.now { box-shadow: inset 0 -2.5px 0 var(--ink); }
/* The playhead on a capped cell, which is ink on ink and therefore invisible. Same tick, the
   plate's colour, so the column still marks itself on the one hour a reader is looking at. */
.pstrip i.over.now { box-shadow: inset 0 -2.5px 0 var(--plate); }

/* The key and the day ruler, above the rows and shared by all of them. `padding: 0 12px` is the
   row's own inset (9px 11px plus a 1px border) — the ruler is only useful if it lands over the
   days it names. */
.pblock { position: relative; }
/* Pinned, because the axis is no use on row nine if it scrolled away at row two — and the
   background is not decoration: without it the white rows read straight through the letters. */
.pkeys { position: sticky; top: -1px; z-index: 2; background: var(--plate); padding-bottom: 5px; }
.pkey {
  display: flex; flex-wrap: wrap; gap: 3px 11px; margin: 9px 0 0; padding: 0 12px;
  font: 500 9.5px/1.4 var(--mono); letter-spacing: .04em; color: var(--ink-3);
  text-transform: uppercase;
}
.pkey span { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.pkey i { width: 9px; height: 9px; border-radius: 2px; background: var(--strip-under); flex: none; }
.pkey i.good { background: var(--strip-good); }
.pkey i.close { background: var(--strip-close); }
.pkey i.over { background: var(--strip-over); }
.pkey i.none { background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 22%, transparent); }
.pruler {
  display: flex; gap: 0; margin: 6px 0 0; padding: 0 12px;
  font: 600 9px/1.2 var(--mono); letter-spacing: .05em; color: var(--ink-3);
}
.pruler span { flex: 0 1 0; min-width: 0; overflow: hidden; text-align: center; white-space: nowrap; }
.pruler span + span { margin-left: 3px; }
.pruler span.on { color: var(--ink); }
/* The list sits under its own pinned axis, not under the 12 px the regions list wants. */
#places { margin-top: 8px; }
/* Wrapping, not nowrap: in a 322 px column "week: 78 h over 15 · peak 29 kt" is wider than the
   row, and a nowrap stat is exactly the kind of child that blows a grid track open. */
.pstat { font: 400 10.5px/1.4 var(--mono); color: var(--ink-2); }
.pstat b { font-weight: 700; font-variant-numeric: tabular-nums; }
.foot { margin: 12px 0 0; font-size: 11px; color: var(--ink-3); line-height: 1.5; }
/* The sort key, above the sorted thing. "A sort whose key is invisible reads as arbitrary" is
   the rule this line exists for, and it cannot do that job underneath sixteen rows. */
.foot.lead { margin: 10px 0 0; color: var(--ink-2); }

/* -- fault ---------------------------------------------------------------------------------- */

.fault {
  position: fixed; z-index: 9; left: 50%; top: 50%; transform: translate(-50%, -50%);
  background: var(--paper); border: 1px solid var(--line-2); border-left: 3px solid var(--signal);
  border-radius: 10px; padding: 14px 18px; box-shadow: var(--shadow); max-width: 460px;
}

/* -- the one surface that is still a modal ---------------------------------------------------
   The exploration panel is a FORM with a sixteen-checkbox compass grid. It is entered
   deliberately and left deliberately, which is what a dialog is for — unlike the places list,
   which a reader wants open at the same time as the map. */

.sheet.xpanel {
  position: fixed; top: max(var(--ws-top), env(safe-area-inset-top, 0px)); right: 0; bottom: var(--ws-bottom); left: 0; z-index: 8;
  background: rgba(15,27,42,.34); backdrop-filter: blur(3px);
  display: grid; place-items: center; padding: 20px;
}
.sheet.xpanel[hidden] { display: none !important; }
.xpanel .sheet-in {
  width: min(520px, 100%); max-height: min(84vh, 760px); overflow: auto;
  background: var(--paper); border-radius: 14px; box-shadow: var(--shadow); padding: 18px 20px 20px;
  display: grid; gap: 2px;
}
/* ==============================================================================================
   THE CALL — Epic / Solid / Go / Maybe / Skip, and the one word that is not a tier.

   ONE CHANNEL, ONE MEANING. The wind ramp owns colour-as-magnitude on this page, so the tier
   scale is deliberately a different hue family (green) and is ORDINAL in lightness, not a second
   opinion about how windy it is. It never appears on the raster and never near the legend: a
   verdict needs a spot — a playbook, a shore normal, a checked meter — and off-spot none of that
   exists, which is the same rule that confines the bias correction to points.

   "Can't say" is grey and dashed on purpose. It is a STATE, not a rung: the engine had nothing to
   evaluate at that lead. Giving it a colour from the ramp would make it look like a weak call.
   ============================================================================================== */
:root {
  --t-fire:    #0F5132;
  --t-solid:   #198754;
  --t-go:      #57B894;
  --t-eyes:    #A9B6C4;
  --t-sleep:   #D7DEE7;
  --t-unknown: #EEF2F6;
}
/* DARK, which is the site's default (assets/js/theme.js) and was not a state this page had until
   it became the Explore landing. The values are theme.css's own where one exists — the plate is
   `--ws-bg-page`, the inks are the site's three text tones, the paper is `--ws-bg-card-solid` —
   so the sheet is the same surface as every other card on the site. What does NOT change is the
   field: the ramp is the data, and its inks (the barbs, the contour, the lattice, the ceiling
   magenta) were measured against the RAMP, not the plate (§0j, §0k). The basemap swaps with it
   (app.js `basemapStyle()`), so the plate under the field is dark on both sides of the seam.
   The four light ladder greys become dark ones: a #EEF2F6 dot on a #0F172A card is a lamp, and
   "unknown" and "sleep" are the two states that must NOT draw the eye. The strips' amber goes
   back to the dashboard's own `--warning`, which was deepened in light only because it went
   thin on white. */
[data-theme="dark"] {
  --plate: #0F172A;
  --ink: #E2E8F0;
  --ink-2: #94A3B8;
  --ink-3: #64748B;
  --line: rgba(255,255,255,.10);
  --line-2: rgba(255,255,255,.17);
  --strip-close: #FBBF24;
  --strip-under: rgba(226,232,240,.11);
  --card: rgba(15,23,42,.94);
  --paper: #1E293B;
  --glass: rgba(30,41,59,.96);
  --on-ink: #0F172A;
  --halo: rgba(15,23,42,.9);
  --shade: rgba(0,0,0,.5);
  --shade-2: rgba(0,0,0,.45);
  --shadow: 0 1px 2px rgba(0,0,0,.35), 0 10px 30px -14px rgba(0,0,0,.7);
  --t-eyes:    #64748B;
  --t-sleep:   #334155;
  --t-unknown: #1E293B;
}

/* -- on the marker ---------------------------------------------------------------------------
   Under the number, never instead of it: a Skip day keeps its 22 kt (Guardrail 0). Same zoom
   gate as the name, because at basin zoom seven of these would smear into each other. */
.spot .call {
  font-size: 9.5px; font-weight: 700; letter-spacing: .02em; white-space: nowrap;
  display: none; color: var(--t-fire);
}
body.named .spot.called .call { display: block; }
.spot .call.t-fire    { color: var(--t-fire); }
.spot .call.t-solid   { color: var(--t-solid); }
.spot .call.t-go      { color: #2F8F6B; }
.spot .call.t-eyes    { color: var(--ink-3); }
.spot .call.t-sleep   { color: var(--ink-3); font-weight: 600; }
.spot .call.t-unknown { color: var(--ink-3); font-weight: 600; font-style: italic; }

/* -- the day selector ------------------------------------------------------------------------ */
.daypick { display: flex; flex-wrap: wrap; gap: 5px; margin: 12px 0 0; }
.daychip {
  font: inherit; cursor: pointer; border: 1px solid var(--line); background: var(--paper);
  border-radius: 8px; padding: 5px 9px; display: grid; gap: 1px; text-align: center;
  line-height: 1.15; color: var(--ink-2);
}
.daychip span { font-size: 12px; font-weight: 650; color: var(--ink); }
.daychip em { font-style: normal; font-size: 9.5px; color: var(--ink-3); letter-spacing: .02em; }
.daychip:hover { border-color: var(--ink-3); }
.daychip.on { background: var(--ink); border-color: var(--ink); }
.daychip.on span, .daychip.on em { color: var(--on-ink); }

/* THE DAY'S BEST CALL, as a dot on its chip (§0u). It wears the timeline band's `.t-*` palette —
   the same five rungs, from the same `callFace()` — because a chip promising something coarser
   than the list it opens is two scales for one judgement.

   It has to read on the SELECTED chip too, which is near-black: a 5 px dot in `--t-sleep` on
   `--ink` is invisible, so the selected chip gives its dot a light ring rather than a different
   colour. Changing the colour would make the same day two tiers depending on which chip is on. */
.cdot {
  justify-self: center; width: 16px; height: 4px; border-radius: 2px; margin-top: 2px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 10%, transparent);
}
.cdot.t-fire    { background: var(--t-fire); }
.cdot.t-solid   { background: var(--t-solid); }
.cdot.t-go      { background: var(--t-go); }
.cdot.t-eyes    { background: var(--t-eyes); }
.cdot.t-sleep   { background: var(--t-sleep); }
/* A STATE, NOT A RUNG — the band's own hatch for a call nobody could make. */
.cdot.t-withheld,
.cdot.t-unknown {
  background: repeating-linear-gradient(45deg, var(--t-sleep) 0 3px, transparent 3px 6px);
  box-shadow: inset 0 0 0 1px var(--line-2);
}
.daychip.on .cdot { box-shadow: 0 0 0 1px color-mix(in srgb, var(--on-ink) 75%, transparent); }

/* -- the call on a sheet row ------------------------------------------------------------------ */
.pwho { display: grid; gap: 3px; min-width: 0; }
.pcall { font-size: 11px; font-weight: 620; color: var(--ink-2); line-height: 1.3; }
.pcall.t-fire    { color: var(--t-fire); }
.pcall.t-solid   { color: var(--t-solid); }
.pcall.t-go      { color: #2F8F6B; }
.pcall.t-eyes,
.pcall.t-sleep   { color: var(--ink-3); font-weight: 600; }
.pcall.t-unknown { color: var(--ink-3); font-weight: 600; font-style: italic; }

/* -- the focused spot's week, under the timeline ----------------------------------------------
   Day-wide bands, drawn across the frames each spot-local day actually owns — so they stretch
   with the cadence rather than pretending the far half of the week is as finely sampled as the
   near half. The brighter inset is the good window, positioned by its fraction of the day. */
/* The 44px track is FULL — days 0-14, their bars and the rail to 26, tick labels 30-42 — so the
   band gets its own room rather than being squeezed into the 3px gap, where it landed on top of
   the hour labels and the spot's name landed on top of the day names. The track only grows when
   a spot is focused, so a reader who never opens the sheet sees the timeline that shipped. */
body.has-band .track { height: 66px; }
.callband { position: absolute; left: 0; right: 0; top: 46px; height: 5px; }
.callband .cb {
  position: absolute; top: 0; height: 5px; border-radius: 2px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 6%, transparent);
}
.callband .cb b { position: absolute; top: -1px; height: 7px; border-radius: 2px;
                  background: color-mix(in srgb, var(--paper) 55%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 18%, transparent); }
.callband .t-fire    { background: var(--t-fire); }
.callband .t-solid   { background: var(--t-solid); }
.callband .t-go      { background: var(--t-go); }
.callband .t-eyes    { background: var(--t-eyes); }
.callband .t-sleep   { background: var(--t-sleep); }
.callband .t-unknown { background: transparent; box-shadow: inset 0 0 0 1px var(--line-2); }
.callname {
  position: absolute; left: 0; top: 53px; font-style: normal; font-size: 9.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink-3); white-space: nowrap;
}

@media (max-width: 720px) {
  .pcall { font-size: 11.5px; }
}

/* The line where the calls stop — see renderChooser(). A caption, not a row: it must not look
   clickable, because there is nothing under it to click. */
.pgroup {
  font-size: 10.5px; font-weight: 650; letter-spacing: .03em; text-transform: uppercase;
  color: var(--ink-3); padding: 8px 2px 2px; border-top: 1px solid var(--line); margin-top: 4px;
}

/* ==============================================================================================
   THE RIDER LAYER — stage 5.

   Everything here is ADDITIVE, like the rest of the call layer: signing in adds a strip, a
   second line under a shaded call and a reason, and removes nothing at all. The rider's word and
   the spot's word are STACKED rather than swapped, because a map that stopped saying what it
   says to everybody the moment somebody signed in would be hiding the public answer behind an
   account — the exact thing Guardrail 0 rules out.

   One colour rule holds here too: a shaded call is drawn from the SAME five-tier palette as an
   unshaded one. It is the same ladder read against a different set of numbers, not a second
   opinion, and a separate hue for "yours" would read as a sixth rung.
   ============================================================================================== */

/* -- whose call it is ------------------------------------------------------------------------ */
/* The slider's what-if line. It appears only once the rider has dragged the line off their own
   alert's number, and it is the smallest thing on the row because it is a footnote to a control
   that already says what it is set to. */
.thrvs {
  font-style: normal; font-size: 10.5px; font-weight: 600; color: var(--signal);
  white-space: nowrap;
}

/* -- the spot's own call, under the rider's ---------------------------------------------------
   Present ONLY when the two differ. Deliberately quiet: the rider asked a question and this is
   the footnote, not the answer. */
.spot .call2 {
  font-size: 9px; font-weight: 600; letter-spacing: .01em; white-space: nowrap;
  display: none; color: var(--ink-3);
}
body.named .spot.called .call2 { display: block; }

/* A withdrawn call: the layer could not stand behind it, which is not the same as the engine
   having had nothing to work with. Same grey italic as "Can't say" — both are states rather than
   rungs — with a dotted underline, because this one has a reason a reader can go and read. */
.spot .call.t-withheld,
.pcall.t-withheld {
  color: var(--ink-3); font-weight: 600; font-style: italic;
  text-decoration: underline dotted color-mix(in srgb, var(--ink) 30%, transparent); text-underline-offset: 2px;
}

/* -- why it moved, on a sheet row -------------------------------------------------------------
   The rider's own numbers, printed. A shaded call that said only "not for you" would be asking
   the reader to take the map's word for it, which is what this whole feature exists not to do. */
.pwhy { font-size: 10.5px; line-height: 1.4; color: var(--ink-3); }

/* On the week band a withdrawn day is hollow like "can't say", because it is one — but hatched,
   so a reader scanning the band can tell "the engine had nothing" from "your own criteria could
   not be checked" without hovering either of them. */
.callband .t-withheld {
  background: repeating-linear-gradient(45deg, var(--t-sleep) 0 3px, transparent 3px 6px);
  box-shadow: inset 0 0 0 1px var(--line-2);
}

@media (max-width: 720px) {
  .pwhy { font-size: 11px; }
}

/* ==============================================================================================
   STAGE 7 — THE WINDOW, THE AREAS, AND THE PANEL THAT EXPLAINS EITHER

   Three visual rules hold everything below together, and each of them exists because breaking it
   is how this layer would lie:

   1. **A saved-spot match and a forecast area never look the same.** A spot that matches gets the
      teal accent and a TICK on a solid pin; an area gets a thin DASHED outline and an uppercase
      eyebrow. Two different claims, two different shapes, legible apart in greyscale and at a
      glance. The brief asks for it and it is also the only thing stopping "an area of the lake is
      windy" from reading like "your alert fired".
   2. **Nothing new is opaque over the field.** The region wash is 6% and the outline 1.2 px,
      because the wind ramp is the figure on this page. A discovery layer that hid the weather
      would be answering the question by covering it up.
   3. **The colour channel stays spoken for.** The five-rung ladder owns tier colour and the wind
      ramp owns magnitude, so the region layer is drawn in the ink greys with ONE teal accent for
      selection, and carries its meaning in shape, dash and words.
   ============================================================================================ */

/* Visually hidden, still announced. The live region and nothing else. */
.sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* -- the window line on a pin ---------------------------------------------------------------- */

.spot .win {
  display: none; font-size: 10px; line-height: 1.25; font-weight: 600; letter-spacing: .005em;
  color: var(--ink-3);
}
body.named .spot.called .win { display: block; }
.spot .win.w-match   { color: #0F766E; font-weight: 700; }
.spot .win.w-later   { color: var(--ink-2); font-weight: 600; }
.spot .win.w-unknown { color: var(--ink-3); font-style: italic; }
.spot .win.w-none    { color: var(--ink-3); font-weight: 500; }
/* The accent is on the PILL, so a matching spot is findable at a zoom where no label draws. A
   matching spot that is also corrected keeps the orange ring — the teal one sits outside it. */
.spot.matching .pill { box-shadow: 0 0 0 1.5px #fff, 0 0 0 3px #0F766E, 0 1px 4px rgba(0,0,0,.4); }
.spot.matching.corrected .pill {
  box-shadow: 0 0 0 1.5px #fff, 0 0 0 3px var(--signal), 0 0 0 4.5px #0F766E, 0 1px 4px rgba(0,0,0,.4);
}

/* -- explore control ------------------------------------------------------------------------- */

#ctl-explore .btn {
  display: flex; align-items: center; gap: 6px; justify-content: space-between;
  width: 100%; text-align: left; padding: 7px 10px;
}
#ctl-explore .btn b { font-weight: 620; font-size: 12px; }
.chevi { color: var(--ink-3); font-size: 10px; }
#explore-src.personal { color: #0F766E; font-weight: 600; }

/* -- the discovery prompt ---------------------------------------------------------------------
 *
 * ONE proactive prompt, never a stack of them: everything else the sweep found is in the list
 * directly below it. v2 floated this over the map at `bottom: 100px` in a band of
 * `calc(100vw - 400px)`, which was not a design decision — it was the only strip of screen the
 * four cards left unoccupied. Here it is simply the row above the list.
 */

.disco { display: block; padding: 10px 12px; background: var(--paper);
         border: 1px solid var(--line); border-radius: 10px; }
.disco[hidden] { display: none !important; }
.dcard { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; align-items: start; }
.dwhere {
  grid-column: 1 / -1; grid-row: 1; margin: 0;
  font-size: 9.5px; letter-spacing: .10em; text-transform: uppercase;
  color: var(--signal); font-weight: 700;
}
.dname { grid-column: 1; grid-row: 2; margin: 1px 0 0; font-size: 15px; font-weight: 650;
         letter-spacing: -0.01em; }
.dwhen { grid-column: 1; grid-row: 3; margin: 0; font-size: 12px; color: var(--ink-2);
         font-variant-numeric: tabular-nums; }
.dgo   { grid-column: 2; grid-row: 2 / span 2; display: flex; gap: 6px; align-items: center; }
.dgo .btn { white-space: nowrap; }
.dwhy  { grid-column: 1 / -1; grid-row: 4; margin: 5px 0 0; font-size: 11.5px;
         color: var(--ink-2); line-height: 1.45; }
.drules { display: none; grid-column: 1 / -1; grid-row: 5; margin: 2px 0 0;
          font-size: 10.5px; color: var(--ink-3); }
/* -- next match ------------------------------------------------------------------------------
 * A flex item at the end of the timeline row rather than a card floating above it: the space
 * above the timeline belongs to the discovery prompt, and two absolutely-positioned things
 * sharing it is a collision waiting for a long region name. */

.nextmatch {
  flex: none; font-size: 11px; padding: 5px 9px; white-space: nowrap; font-weight: 600;
  max-width: 150px; overflow: hidden; text-overflow: ellipsis;
}
.nextmatch[hidden] { display: none; }

/* -- the region label on the map -------------------------------------------------------------- */

.rlab {
  border: 1px solid var(--line-2); border-left: 2px solid #334155;
  background: color-mix(in srgb, var(--paper) 93%, transparent); backdrop-filter: blur(6px);
  border-radius: 8px; padding: 5px 9px; cursor: pointer; font: inherit;
  display: grid; gap: 1px; text-align: left; max-width: 200px;
  box-shadow: 0 1px 2px var(--shade-2), 0 6px 18px -10px var(--shade);
}
.rlab em {
  font-style: normal; font-size: 8.5px; letter-spacing: .10em; text-transform: uppercase;
  color: var(--ink-3); font-weight: 700;
}
.rlab b { font-size: 12px; font-weight: 650; letter-spacing: -0.01em; }
.rlab span { font-size: 10.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.rlab.on { border-left-color: #0F766E; box-shadow: 0 0 0 1.5px #0F766E, var(--shadow); }
.rlab.on em { color: #0F766E; }

/* -- the aggregate and region bands ------------------------------------------------------------ */

.callband.agg .cb { background: var(--t-eyes); }
.callband.agg .cb.ag1 { background: #A7D8C6; }
.callband.agg .cb.ag2 { background: #57B894; }
.callband.agg .cb.ag3 { background: var(--t-solid); }
.callband.agg .cb.ag-unknown {
  background: repeating-linear-gradient(45deg, var(--t-sleep) 0 3px, transparent 3px 6px);
}
.callband.region .cb.rg { background: #0F766E; }
.callband.region .cb.rg-out { background: #CBD5E1; }
.callname.rgname { color: #0F766E; }

/* -- the detail sheet --------------------------------------------------------------------------- */

.dkind {
  margin: 0; font-size: 9.5px; letter-spacing: .10em; text-transform: uppercase;
  color: var(--ink-3); font-weight: 700;
}
.dline { margin: 10px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--ink-2); }
.dline b { color: var(--ink); }
.dline.strong { font-size: 14px; color: var(--ink); font-weight: 620; margin-top: 12px; }
.dcrit { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 4px; }
.dcrit li {
  font-size: 12px; line-height: 1.45; padding-left: 20px; position: relative; color: var(--ink-2);
}
/* An icon AND a word: colour alone never carries the verdict. */
.dcrit li::before { position: absolute; left: 0; top: 0; font-weight: 700; }
.dcrit li.y::before { content: '\2713'; color: #0F766E; }
.dcrit li.n::before { content: '\2715'; color: #B4413B; }
.dcrit li.u::before { content: '?'; color: var(--ink-3); }
.dcrit li.u { font-style: italic; }
.dacts { margin: 14px 0 0; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.dacts .btn { text-decoration: none; display: inline-block; }

/* -- the exploration panel ---------------------------------------------------------------------- */

.xrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 12px;
        font-size: 12.5px; color: var(--ink-2); }
.xrow input[type=number] {
  width: 68px; border: 1px solid var(--line-2); border-radius: 8px; padding: 6px 8px;
  font: inherit; font-size: 13px; font-variant-numeric: tabular-nums;
}
.xdir { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; display: block; }
.xdir legend { font-size: 11px; color: var(--ink-3); font-weight: 650;
               text-transform: uppercase; letter-spacing: .06em; padding: 0 4px; }
.xdir > label { display: inline-flex; align-items: center; gap: 5px; margin-right: 14px; font-size: 12.5px; }
.xdirs { display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; margin-top: 8px; }
.xdirs label {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  font-size: 10px; color: var(--ink-2); border: 1px solid var(--line); border-radius: 7px;
  padding: 4px 2px; cursor: pointer;
}
.xdirs label:has(input:checked) { border-color: #0F766E; color: #0F766E; font-weight: 650; }
.xseed { margin-top: 14px; display: grid; gap: 6px; }
.btn.seed { display: grid; gap: 2px; text-align: left; padding: 8px 10px; }
.btn.seed b { font-size: 12.5px; font-weight: 620; }
.btn.seed em { font-style: normal; font-size: 11px; color: var(--ink-2); }
.btn.seed i { font-style: normal; font-size: 10.5px; color: var(--signal); }

/* -- the second list in the places sheet -------------------------------------------------------- */

.rhead { margin: 22px 0 0; font-size: 14px; }
/* A 322 px column is not a 680 px modal: a region row stacks like a spot row rather than
   holding a name and a stat range side by side in 150 px each. */
.places.regions .place { grid-template-columns: minmax(0, 1fr); }
.place.region.on { border-color: #0F766E; box-shadow: inset 0 0 0 1px #0F766E; }
.rcall { font-size: 11.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; font-weight: 600; }


/* ==============================================================================================
   BELOW 1024 px — ONE SHEET, THREE SNAP POINTS
   ==============================================================================================

   A phone is not a small desktop, and this page proved it twice. v2 tiled four cards over 496 of
   844 px before anyone had read anything. v2's fix — a chevron over one dock plate — was better
   until you pressed the chevron, at which point the dock measured 692 of 844 px: 82% of the
   screen, which is what Jeff was looking at when he said the menus were taking over.

   The fault was the control, not the plate. A chevron has two states and the open one is
   whatever the content happens to add up to, so "show me a bit more" and "bury the map" are the
   same gesture. A sheet with three SNAP points makes the middle state real: peek is what it
   loads in, half is the list, full is the list plus everything that explains it — and a reader
   who wants the map back drags down instead of hunting for the control they pressed.

   The timeline MOVES into the column here (wireColumn() re-parents it), because on a phone it is
   the primary control and it belongs under the thumb with the rest of them — not floating over
   the map with the basemap attribution trying to share the same twenty pixels.
   ============================================================================================ */

/* =============================================================================================
   THE SHEET IS THE LAYOUT AT EVERY WIDTH (2026-09-16).

   Jeff, on the desktop build: "I don't like the full screen version with the menu on the left. I
   think I'd prefer just a menu on the bottom. Right now you can open up a forecast and then it
   goes across the bottom anyway. So why not have the whole menu on the bottom." And of the column:
   "I don't see the selector for waves water temp gust etc" — on a loaded page the switch sat
   below seven spot rows, which is the §0w finding ("everything below the fold is a thing you have
   been told exists and cannot find") arriving on the desktop.

   So this block, which was `@media (max-width: 1023px)`, is now unconditional: the phone's
   information architecture (§0w) is the page's. A wide screen differs from a phone in the block
   AFTER this one — the list is a grid of cards rather than a stack, the half snap is the
   content's height rather than 60vh, and the dock (§0x) is kept, because a 1400 px track has room
   for a number a column.
   ============================================================================================= */
@media all {
  #app { grid-template-columns: 1fr; }

  #col {
    position: fixed; left: 0; right: 0; bottom: var(--ws-bottom); z-index: 6;
    border-right: 0; border-top: 1px solid var(--line);
    border-radius: 16px 16px 0 0;
    background: var(--card); backdrop-filter: blur(10px);
    box-shadow: 0 -2px 34px -16px var(--shade);
    transition: height .2s ease;
    /* The home-indicator inset, ONLY where the sheet is the thing on the screen edge. Since the
       page joined the site (§1g) the bottom nav sits under the sheet and already carries the
       inset; paying it here too was a 34 px band of nothing under the verdict (Jeff's phone,
       2026-09-18). `--ws-bottom` is 0 exactly when there is no nav. */
    padding-bottom: max(0px, calc(env(safe-area-inset-bottom) - var(--ws-bottom)));
  }
  /* MIN IS ABOUT 90 px: the grip bar and one sentence (§0v). Jeff, on a screenshot of the
     deployed page: "look how much real estate the menu takes up even when minimized" — peek was
     46vh, nearly half the phone, because it had quietly become the state you READ the map with
     rather than the state you put the sheet away in. The cap is a backstop for a three-line
     verdict on a narrow phone; the height is the content's. */
  body.col-min #col { height: auto; max-height: calc(var(--ws-avail) * .22); }
  body.col-half #col { height: calc(var(--ws-avail) * .60); }
  body.col-full #col { height: calc(var(--ws-avail) * .88); }

  #colscroll { padding: 2px 14px 12px; gap: 7px; }
  /* Three chips reading Wind, Gust and Wave do not need a word above them saying "Show". The
     ones in Options do: "Smooth / Cells" says nothing on its own. */
  /* The track wants the row. "Next match" is a JUMP, not a reading: squeezed in beside a 290 px
     track it took half of it and still ellipsised its own sentence. Wrapped below the track it is
     full width and legible — and in peek it is not drawn at all, because peek is the three things
     you read the map with and this is a shortcut to a place the list one drag up already names.
     It comes back at half, where it has a row of its own. */
  .timeline { flex-wrap: wrap; }
  /* PEEK IS FOR THE MAP (Jeff, 2026-09-18: "is there a way to show more map"). At min the
     timeline drops the band's caption — the dots still say what they say, and the label is back
     at half — and its padding goes to the minimum the play button needs. ~24 px more map. */
  body.col-min .timeline { padding: 6px 12px 7px; }
  body.col-min .timeline .callname { display: none; }
  body.col-min.has-band:not(.has-dock) .track { height: 54px; }
  .timeline .nextmatch { order: 3; width: 100%; max-width: none; text-align: left; }
  /* On the map at min, and lifted over the sheet by the height JS measured — the same variable
     the attribution and the back button ride on, for the same reason: guessing it was wrong by
     13 px the one time anybody checked. */
  #pane .timeline {
    bottom: calc(var(--dock-h, 110px) + 10px);
    width: min(640px, calc(100% - 28px));
  }
  /* The static element's line rides the same measured lift as the timeline it replaces — the
     sheet's own height, not a guess at it (§0n: guessing was wrong by 13 px the one time anybody
     checked). */
  .staticline {
    bottom: calc(var(--dock-h, 110px) + 10px);
    width: min(640px, calc(100% - 28px));
    font-size: 10.5px; padding: 7px 10px;
  }
  .radarbar { width: auto; max-width: calc(100% - 28px); padding: 4px 10px 4px 4px; gap: 8px; }
  .radar-dots button { width: 15px; }
  body.col-min .timeline .nextmatch { display: none; }
  /* THREE SNAPS, AND EACH ONE MEANS SOMETHING.
   *
   *   peek — the finding, the subject switch, your line, and the playhead. The four things you
   *          read the map WITH, and nothing else.
   *   half — THE LIST. Which is the point of the state: at half the old build showed a ramp, a
   *          provenance note and no rows at all, which is a middle state that answers nothing.
   *   full — everything, including what explains it.
   *
   * The threshold control is in peek by Jeff's ask of 2026-09-11 — "make it so I can adjust the
   * threshold without expanding the menu". It used to cost 34 px as a slider row, and the RAMP
   * was the thing that waited for half: "a ramp with no number on it is decoration" was an
   * argument about a bare colour bar, and the slider printed its own number.
   *
   * COMBINING THEM REVERSES THAT (2026-09-13). The ramp IS the control now, so hiding it at peek
   * would take the control with it. What waits for half instead is the gear ladder and the end
   * labels — the parts that decode the whole scale rather than the rider's own line. The reading
   * rides on the mark, so the bar is not a bare colour bar at peek: it has exactly one number on
   * it, which is the one peek exists to show. */
  /* WHAT MIN SHOWS: the grip, and the verdict. Nothing else (§0v).

     This was the "peek allow-list" and it had five exceptions in it, which is the tell that peek
     had stopped being a minimum — the head, the section strip, the whole list and the scale were
     all in it. Min is one `:not()`: `#colhead`, and inside it only `#answer`. A reader who wants
     more has a grip to pull and a map they can stop touching.

     Leaving something IN this list by accident is how the sheet grows back. */
  body.col-min #colscroll > *:not(#colhead) { display: none; }
  body.col-min .mast, body.col-min .valid, body.col-min .issued, body.col-min .mapsearch { display: none; }
  body.col-min .answer { font-size: 14.5px; line-height: 1.28; margin: 0; }
  body.col-min #colscroll { padding: 0 14px 8px; }
  /* The grip's own padding is most of what is left once the sentence is one line of type; at min
     it gives back six pixels of map and loses nothing — the handle is still a 30 px target. */
  body.col-min #grip { padding: 6px 14px 3px; }

  /* At half the list is the answer, so what is left of the key, the two rule-set controls and
     the provenance all wait for full. Nothing is removed: a drag up brings every one of them
     back, and the desktop column shows them all at once. */
  /* AT HALF, EVERY SECTION THE STRIP LINKS TO EXISTS. The strip offers three destinations and a
     tap that lands on `display: none` is a dead control — "a dead gesture is a missing feature".
     What still waits for full is the detail inside them: the gear ladder, and the fold. The grip
     only toggles min and half, so anything hidden at half is reachable by DRAG alone, which is
     how the search scope became unreachable-by-button the first time this was written. */
  body.col-half .gear, body.col-half .ends, body.col-half .method { display: none; }
  body.col-half .rampwrap { padding-bottom: 0; }
  body.col-half .mini { display: flex; }
  /* The head has already been read: min showed the verdict and nothing else, and half is the
     state a reader entered by asking for the list. The name, the stats line and the provenance
     are one more drag up. */
  body.col-half .mast, body.col-half .issued { display: none; }
  body.col-half .valid { margin-top: 0; }

  /* ===========================================================================================
     THE PHONE'S OWN INFORMATION ARCHITECTURE (§0w).

     Jeff: the sheet is "one hell of a menu… not very well organized… like we're hiding a bunch of
     shit the user can do because we needed a dumping ground." It was the DESKTOP COLUMN in a
     drawer — three headed sections, a legend, a slider, a preferences form and two lists, reached
     by scrolling past each other. That is a fine shape for a plate permanently beside the map and
     the wrong one for a drawer pulled over it.

     Here the sheet is the LIST. The other two sections become VIEWS of it, each entered from a
     control that names what it opens and left by a link that says where it goes. The section strip
     goes with them: three headings over one list is chrome explaining chrome.
     =========================================================================================== */
  #qnav { display: none; }
  #q-what, #q-else { display: none; }
  body.view-key #q-what { display: grid; }
  body.view-regions #q-else { display: grid; }
  /* ONE THING OVER THE MAP AT A TIME — §0o's invariant, kept by the view rather than by a z-index
     argument between two panels that do not know about each other. */
  body.view-key #sheet, body.view-key #detail,
  body.view-regions #sheet, body.view-regions #detail { display: none !important; }
  /* The regions view is the RANKED AREAS and nothing else: the exploration form, the three search
     selects and their provenance lines are desktop-only at this width. A phone reader who tapped
     one line gets the answer, not a panel of controls under it; the rules the sweep ran on are
     whatever is stored, which is what they were before they tapped. */
  body.view-regions #ctl-explore, body.view-regions #explore-src,
  body.view-regions #explore-drop, body.view-regions #scope-note,
  body.view-regions #q-else > .ctl { display: none; }

  /* The map's own controls, floated where they act. */
  #mapctl {
    display: flex; gap: 7px; align-items: flex-start;
    position: absolute; right: 10px; top: 10px; z-index: 3;
  }
  #mapctl .seg { background: var(--glass); box-shadow: 0 1px 8px var(--shade); }
  /* The live badge shares the top strip with these controls and is wider than the space left
     beside them on a 390-px phone, and being later in the DOM it sat ON TOP of the element
     switch — Jeff, 2026-09-15: "the selector for wave, water and gust is hidden behind the
     status bar." The badge is annotation and the switch is a control; the control keeps the
     strip and the badge drops under it. */
  /* FOUR ITEMS ON A 390 px STRIP (2026-09-15, the Water element — README §1b). The switch grew
     a fourth button and the strip did not: measured at 390 px, `#mapctl` went 181 px wide to 231
     and its left edge crossed 41 px INTO the scale at the other end — which is Jeff's own §0y
     complaint ("the selector for wave, water and gust is hidden behind the status bar") arriving
     by a different road. Both ends give a little rather than one end losing a word: the buttons
     lose three pixels of padding each and the scale plate 30 px of its 150, which still leaves
     five tick labels legible on it. Measured after: switch 163 px, `#mapctl` 202, and 8 px of
     clear strip between the two plates. */
  #mapctl .seg button { padding: 6px 6px; font-size: 11px; background: transparent; }
  /* FIVE ITEMS (2026-09-17, the Radar element — README §1e). The same trade again, measured the
     same way: at 390 px five buttons at the four-item sizes ran 246 px and the switch crossed
     34 px into the scale's plate (the Line button up). Each button gives 2 px of padding a side
     and half a point of type, and the plate gives 8 px more (112 → 104 at ≤400 px, still four
     ticks legible at 7 px mono). Measured after: see §1e. */
  #mapctl .seg button { padding: 6px 4px; font-size: 10.5px; }
  #mapctl .seg button.on { background: var(--ink); color: var(--on-ink); }
  .keybtn {
    flex: none; width: 32px; height: 32px; display: grid; place-items: center; padding: 0;
    border: 1px solid var(--line-2); border-radius: 9px; cursor: pointer;
    background: var(--glass); color: var(--ink-2);
    box-shadow: 0 1px 8px var(--shade);
  }
  .keybtn svg { fill: currentColor; stroke: currentColor; }
  .keybtn:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }

  #key-close, #else-close { display: block; margin-bottom: 2px; }

  /* The rider's chooser, at the head of the LIST rather than in a section that is not there. */
  #rider-slot #rider { margin: 2px 0 6px; }

  /* Discovery's door: one line at the foot of the list, and a door only when there is a room. */
  .discoline {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    width: 100%; margin: 14px 0 2px; padding: 11px 12px; cursor: pointer;
    border: 1px dashed var(--line-2); border-radius: 10px; background: var(--paper);
    font: inherit; font-size: 12.5px; color: var(--ink-2); text-align: left;
  }
  .discoline[hidden] { display: none !important; }
  .discoline i { font-style: normal; color: var(--ink-3); }
  .discoline:hover { border-color: var(--ink-3); }

  /* The strips' own sticky axis has nothing above it now that the question strip is gone. */
  .pkeys { top: -1px; }
  /* Section 1 opens the scroll directly under the strip, so its heading has no room to claim. */
  #sheet > .qh { display: none; }

  /* Eight day chips wrap to two rows and cost 80 px of a sheet whose job is to show rows. One
     row that scrolls sideways is the same eight chips in half the height. */
  .daypick {
    flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none; padding-bottom: 2px;
  }
  .daypick::-webkit-scrollbar { display: none; }
  .daychip { flex: none; }

  /* The grip is the sheet's own lid and wears the sheet's own ground, for the same reason as the
     strip above: `background: none` let the first scrolled row show through the handle in Jeff's
     screenshot. It also sits above the scroller in the stack, so a tall first row cannot ride up
     into the one control that puts the sheet away. */
  #grip {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    width: 100%; flex: none; border: 0; cursor: pointer;
    background: var(--plate); position: relative; z-index: 8;
    border-radius: 16px 16px 0 0;
    padding: 8px 14px 4px; font: inherit; color: var(--ink-2);
    touch-action: none;
  }
  .griphandle {
    width: 34px; height: 4px; border-radius: 2px; background: var(--line-2); flex: none;
  }
  .griplab { font: 600 11px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
  /* Once the sheet is open the label has been answered; the handle alone says "drag me". */
  body:not(.col-min) .griplab { display: none; }

  /* The timeline is a row of the sheet here, not a card floating on the map — WHILE IT IS IN THE
     SHEET. At min it moves back onto the map (§0v: min is the grip and the verdict, and a
     playhead hidden inside a closed sheet is a control a reader cannot reach), so the rules that
     flatten it are scoped to the parent it is flattened INSIDE. */
  #colscroll .timeline {
    position: static; transform: none; width: auto; left: auto; bottom: auto;
    background: none; border: 0; border-radius: 0; box-shadow: none; backdrop-filter: none;
    padding: 2px 0 0;
  }

  /* Pinch is the gesture on a phone and the zoom buttons were 68 px of map. Attribution is not
     optional and stays: it is lifted clear of the sheet, whose height JS measures. Guessing it
     is what the rules this replaced did (`bottom: 276px`) and it was wrong by 13 px in the one
     case anybody looked at. */
  .maplibregl-ctrl-group { display: none; }
  .maplibregl-ctrl-bottom-right, .maplibregl-ctrl-bottom-left { bottom: var(--dock-h, 186px); }

  .backbtn { top: auto; bottom: calc(var(--dock-h, 186px) + 10px); }
  .xdirs { grid-template-columns: repeat(4, 1fr); }
  .rlab { max-width: 160px; }
  .rlab b { font-size: 11.5px; }
}
/* No dock on a phone (renderDock() gates it too): 290 px of track is 9 px a column. */
@media (max-width: 1023px) {
  .dock, .dock-unit { display: none; }
}
/* -- a wide screen's sheet ------------------------------------------------------------------- */
@media (min-width: 1024px) {
  /* Half is the LIST, and on a wide screen the list is one or two rows of cards, so the snap is
     the content's height with a backstop — not 60vh of a 900 px desktop, which is a curtain over
     the lake the list is about. Full stays a drag away for the key and the fold. */
  body.col-half #col { height: auto; max-height: calc(var(--ws-avail) * .56); }
  body.col-full #col { height: calc(var(--ws-avail) * .80); }
  #colscroll { padding: 2px 18px 0; gap: 8px; }
  /* THE PLAYHEAD IS PINNED TO THE SHEET'S FOOT. In the sheet it is the last row (the phone's
     order), and on a wide screen the list above it is a grid that can be four rows deep — which
     put the one control a reader scrubs with below the fold of its own scroller, the first time
     this was drawn. Sticky, on the sheet's own ground, so the list scrolls under it. */
  #colscroll .timeline {
    position: sticky; bottom: 0; z-index: 3; margin-top: 4px;
    padding: 6px 0 14px; background: var(--plate);
  }
  /* The alert chooser and the day chips share a row: a 1400 px select is a select with 1000 px
     of nothing in it, and the chips are one row of eight. Everything under them spans. */
  #sheet .sheet-in { display: grid; grid-template-columns: auto 1fr; column-gap: 18px; align-items: end; }
  #sheet .sheet-in > * { grid-column: 1 / -1; }
  #sheet .sheet-in > #rider-slot { grid-column: 1; }
  #sheet .sheet-in > #rider-slot:empty { display: none; }
  #sheet .sheet-in > #daypick { grid-column: 2; }
  #rider-slot #rider { width: 320px; margin: 2px 0 0; }
  /* A stack of seven full-width rows at 1440 px is a stack of seven 1400 px lines. Cards, in as
     many columns as fit: three to seven spots is one row. */
  .places { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 6px; }
  .pgroup { grid-column: 1 / -1; }
  /* The day ruler labelled ONE strip's columns in a column of rows. Over a grid it lines up with
     nothing; the strip's days are the day chips' days, one row up. */
  .pruler { display: none; }
  /* THE EXPLORATION RULES COME BACK IN THE REGIONS VIEW. The phone hides them there because a
     reader who tapped one line wants the answer, not a form under it (§0w) — and it had a desktop
     that showed them. It no longer has one, so the four controls sit in a row over the ranked
     areas, where a wide sheet has the room the phone did not. */
  body.view-regions #ctl-explore, body.view-regions #explore-src, body.view-regions #explore-drop,
  body.view-regions #scope-note { display: block; }
  body.view-regions #explore-drop[hidden] { display: none; }
  body.view-regions #q-else > .ctl { display: grid; }
  body.view-regions #q-else { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: end; }
  body.view-regions #q-else > * { flex: 1 1 100%; min-width: 0; }
  body.view-regions #q-else > .ctl { flex: 0 1 240px; }
  /* With the dock under the timeline, "Hour by hour" is the same hours a second time. */
  body.has-dock #detail-tabs { display: none; }
  /* The key and a spot's detail are prose and a ramp: a measure, not the width of the screen. */
  body.view-key #q-what, #detail > .sheet-in, .staticline { max-width: 760px; }
  /* Pinch is not the gesture here. */
  .maplibregl-ctrl-group { display: block; }
  /* The timeline floats on the map at min; with a spot's dock under it, it is the desktop's own
     width again rather than the phone's 640 px. */
  body.has-dock #pane .timeline { width: min(1100px, calc(100% - 150px)); }
}

/* A phone in landscape is 390 px TALL. There the ramp is worth more as map than as key — but the
   slider is not the ramp, and folding away the one control the peek exists to keep reachable
   would be answering the wrong question. */
@media (max-width: 1023px) and (max-height: 560px) {
  /* Min is already one sentence; in 390 px of height it is a third of the screen rather than a
     fifth, and the sentence is the one thing that must not fold — it is the whole of what min
     says. */
  body.col-min #col { max-height: calc(var(--ws-avail) * .34); }
  body.col-half #col { height: calc(var(--ws-avail) * .76); }
  body.col-full #col { height: calc(var(--ws-avail) * .92); }
}

/* Reduced motion: the map flights already pass `speed` high enough to be instant, and nothing
   here animates on its own. What is left is the sheet's height transition, the smooth scroll in
   the list (opted out in JS) and any transition a browser would add. */
@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}
