/* SeaState design system — data-journalism clean.
   The discipline of NYT, the block formatting of Our World in Data, the spartan
   clarity of Climate Action Tracker. Navy ink on cool ivory. COLOR LIVES IN THE
   DATA, never the chrome — the only saturated hues are the model lines and the
   go/marginal/no-go status. Whitespace + fine hairlines do the separating. Every
   data block cites its source. Light theme only (see docs/CONVENTIONS.md). */

:root {
  /* surfaces & ink */
  --paper: #f3f4f2;
  --surface: #ffffff;
  --surface-2: #f7f8f7;
  --icon-halo: #ffffff;   /* map-glyph halo — white reads on the daytime blue sea */
  --ink: #16232e;         /* deep navy — reads near-black, on-brand */
  --ink-2: #48575f;
  --muted: #8a969b;
  --line: rgba(22, 35, 46, 0.11);
  --line-2: rgba(22, 35, 46, 0.20);
  --navy: #16232e;        /* brand structural color */
  --accent: #0f6b74;      /* interactive/links only, used sparingly */

  /* DATA ONLY — categorical model slots (fixed order = CVD mechanism; never reassign) */
  --m1: #2f6f9f;
  --m2: #2f9e78;
  --m3: #d98a2b;
  --m4: #7b5ea7;
  /* status ramp */
  --good: #2e8b57;
  --warn: #b9791a;
  --serious: #d9772b;
  --crit: #b23b3b;
  --now: #b23b3b;         /* current-time line */
  --obs: #48575f;         /* buoy observations */
  /* chart chrome */
  --grid: rgba(22, 35, 46, 0.09);
  --axis: rgba(22, 35, 46, 0.28);

  --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --serif: Georgia, "Times New Roman", "Iowan Old Style", serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  --r: 3px;
  color-scheme: light; /* native checkboxes / selects / scrollbars follow the theme */
}

/* ECDIS-style Dusk / Night color tables (after IHO S-52). Same token names, only
   luminance/hue re-cast: hue is kept constant so nothing shifts meaning between
   modes. Night is dim, blue-suppressed warm-neutral on near-black; red (--crit)
   stays the one reserved alarm token. :root above is the Day table. */
:root[data-theme="dusk"] {
  --paper: #22282a;
  --surface: #2b3234;
  --surface-2: #262c2e;
  --icon-halo: #dfe4e4;   /* light halo, reads on the dusk slate without glare */
  --ink: #b9c2c2;
  --ink-2: #9aa4a4;
  --muted: #7e8789;
  --line: rgba(220, 225, 225, 0.12);
  --line-2: rgba(220, 225, 225, 0.22);
  --navy: #cdd6d6;
  --accent: #5fbcc4;
  --m1: #4f93c8; --m2: #5aa06e; --m3: #c68a2e; --m4: #9a6fc0;
  --good: #4f9d5a; --warn: #c9a52e; --serious: #d97a1e; --crit: #d64a3a;
  --now: #9aa4a4; --obs: #9aa4a4;
  --grid: rgba(220, 225, 225, 0.10);
  --axis: rgba(220, 225, 225, 0.30);
  color-scheme: dark;
}
:root[data-theme="night"] {
  --paper: #070707;
  --surface: #100e0a;
  --surface-2: #16130d;
  --icon-halo: #221f18;   /* near-ground dark halo — no white glare; the warm glyph carries contrast */
  --ink: #b7a488;
  --ink-2: #97896f;
  --muted: #6f6656;
  --line: rgba(183, 164, 136, 0.14);
  --line-2: rgba(183, 164, 136, 0.24);
  --navy: #b7a488;
  --accent: #c9923f;
  --m1: #c9923f; --m2: #8a9b6b; --m3: #b98d6a; --m4: #7f938c;
  --good: #6f9f5b; --warn: #c9a13a; --serious: #d1741f; --crit: #e0503a;
  --now: #6f6656; --obs: #97896f;   /* now-line dim, NOT the reserved alarm red */
  --grid: rgba(183, 164, 136, 0.09);
  --axis: rgba(183, 164, 136, 0.28);
  color-scheme: dark;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  min-height: 100vh;
  font-family: var(--sans);
  background: var(--paper);
  color: var(--ink);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--navy); color: #fff; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }

.mono { font-family: var(--mono); }
.source { font-family: var(--mono); font-size: 11px; color: var(--muted); letter-spacing: 0.02em; }

/* ---- app layout ---- */
#app { min-height: 100vh; display: flex; flex-direction: column; }

.apphead {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; gap: 20px;
  height: 52px; padding: 0 20px;
  background: var(--surface); border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; letter-spacing: 0.01em; }
.brand .mark { display: inline-flex; color: var(--navy); }
/* Client badge beside the brand. Day shows the client's full-colour lockup on
   its own white plate; Dusk/Night swap to the inverse (white) mark, dimmed so a
   bright logo can't become the brightest thing on a night bridge. */
.clientbadge { display: inline-flex; align-items: center; margin-left: -6px; padding-left: 14px; border-left: 1px solid var(--line); }
.clientbadge img { height: 24px; display: block; }
.clientbadge .lg-night { display: none; }
:root[data-theme="dusk"] .clientbadge .lg-day,
:root[data-theme="night"] .clientbadge .lg-day { display: none; }
:root[data-theme="dusk"] .clientbadge .lg-night { display: block; opacity: 0.8; }
:root[data-theme="night"] .clientbadge .lg-night { display: block; opacity: 0.5; }
@media (max-width: 880px) { .clientbadge { display: none; } } /* phone header has no room */

.apphead nav { display: flex; gap: 2px; margin-left: 10px; }
.apphead nav a { font-size: 13px; font-weight: 500; color: var(--ink-2); padding: 6px 11px; border-radius: 2px; text-decoration: none; }
.apphead nav a.on { background: var(--surface-2); color: var(--ink); box-shadow: inset 0 -2px 0 var(--navy); }
.apphead nav a:hover { color: var(--ink); }
.coordbox { margin-left: auto; font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); display: flex; gap: 16px; align-items: center; }
.coordbox .k { color: var(--muted); }
.coordbox .health { color: var(--muted); }

/* Segmented toggles: the top bar's Day/Dusk/Night/Auto and the settings
   panel's generic .seg (clock Local/UTC today, units next) share one look. */
.themeseg, .seg { display: inline-flex; border: 1px solid var(--line-2); border-radius: 4px; overflow: hidden; flex: none; }
.themeseg-btn, .seg-btn { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em; padding: 4px 8px; background: transparent; color: var(--muted); border: 0; border-left: 1px solid var(--line); cursor: pointer; line-height: 1.4; }
.themeseg-btn:first-child, .seg-btn:first-child { border-left: 0; }
.themeseg-btn.on, .seg-btn.on { background: var(--navy); color: var(--surface); }
.themeseg-btn:hover:not(.on), .seg-btn:hover:not(.on) { color: var(--ink); }
.themeseg-btn:focus-visible, .seg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Settings: a gear in the top bar opening a compact panel of labelled rows. */
.settings { position: relative; display: inline-flex; flex: none; }
.settings-btn { font-size: 13px; line-height: 1.2; padding: 3px 7px; background: transparent; color: var(--muted); border: 1px solid var(--line-2); border-radius: 4px; cursor: pointer; }
.settings-btn:hover { color: var(--ink); }
.settings-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.settings-panel { position: absolute; top: calc(100% + 8px); right: 0; z-index: 80; min-width: 210px; background: var(--surface); border: 1px solid var(--line-2); border-radius: 4px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14); padding: 10px 12px; display: flex; flex-direction: column; gap: 10px; }
.settings-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.settings-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }

/* Coordinate entry: the header readout becomes an input on click. */
.coordedit { display: inline-flex; align-items: center; gap: 6px; }
.coordedit .editable { cursor: pointer; border-bottom: 1px dashed transparent; }
.coordedit .editable:hover { border-bottom-color: var(--muted); }
.coordinput { font-family: var(--mono); font-size: 11.5px; color: var(--ink); background: var(--surface); border: 1px solid var(--line-2); border-radius: 3px; padding: 2px 6px; width: 24ch; }
.coordinput:focus { outline: none; border-color: var(--accent); }
.coordinput.err { border-color: var(--crit); }
.cerr { color: var(--crit); font-size: 10.5px; white-space: nowrap; }
.avatar { width: 26px; height: 26px; border-radius: 50%; background: var(--navy); color: #fff; display: grid; place-items: center; font-family: var(--mono); font-size: 11px; }

/* map-dominant console: map fills the left, a scrolling column of blocks on the
   right. The rail grows with the viewport so meteograms stay wide-and-short
   (a squarish time series is unreadable); charts keep a ~2.8:1 aspect. */
/* rail · workspace panel · splitter · map · splitter · track column.
   Every child is PINNED to a named area — auto-placement once slid the map into
   a 7px splitter track whenever a layout mode dropped a column while one of its
   elements was still in flow (the "sliver map" bug). With areas, an element in
   a mode that has no slot for it must leave the flow (display:none / absolute),
   and the map always owns the map track. The map track's minmax() floor must
   match MAP_MIN_W in main.ts. */
/* TWO rows now. The conditions table is pinned across the bottom of the content
   area — map, splitter and track column — because that is what "full width
   below the map" means once the shell has a right-hand column: a strip under
   the map alone would be a third instrument competing with the tracks instead
   of the single wide table the client asked for.
   The row is `auto`, so a folded table costs its header bar and nothing more,
   and the first row is minmax(0, 1fr) rather than 1fr — without the zero floor a
   tall table would push the map's own min-content height off the bottom of the
   viewport instead of taking space from it. Every height below that used to read
   calc(100vh - 52px) is now 100%, because the grid row, not the viewport, is
   what those elements fill. */
.appbody {
  position: relative; display: grid;
  height: calc(100vh - 52px);
  grid-template-columns: var(--rail-w, 74px) var(--wspanel-w, 300px) 7px minmax(280px, 1fr) 7px var(--col2-w, clamp(380px, 34vw, 560px));
  grid-template-rows: minmax(0, 1fr) auto;
  grid-template-areas:
    "rail wspanel lgutter map     gutter  col2"
    "rail wspanel lgutter ctable  ctable  ctable";
}
.appbody > .rail { grid-area: rail; }
.appbody > .wspanel { grid-area: wspanel; }
.appbody > .map { grid-area: map; }
.appbody > .col2 { grid-area: col2; }
.appbody > .gutter { grid-area: gutter; }
.appbody > .lgutter { grid-area: lgutter; } /* after .gutter — the element carries both classes */
.appbody > .ctable { grid-area: ctable; }
/* Closed panel: the template loses the wspanel and lgutter areas, and both
   elements leave the flow ([hidden] / display:none) so neither can force an
   implicit track. */
.appbody.wsclosed {
  grid-template-columns: var(--rail-w, 74px) minmax(280px, 1fr) 7px var(--col2-w, clamp(380px, 34vw, 560px));
  grid-template-areas:
    "rail map    gutter col2"
    "rail ctable ctable ctable";
}
.appbody.wsclosed .lgutter { display: none; }
.map { height: 100%; background: var(--paper); }
.map .maplibregl-ctrl-attrib { font-size: 10px; }
/* MapLibre ships white controls that ignore our tokens — theme them, and flip
   their dark glyphs to read on the dusk/night grounds. */
.map .maplibregl-ctrl-group { background: var(--surface); }
.map .maplibregl-ctrl-group button + button { border-top-color: var(--line); }
.map .maplibregl-ctrl-group button:hover { background: var(--surface-2); }
.map .maplibregl-ctrl-attrib { background: var(--surface); color: var(--muted); }
.map .maplibregl-ctrl-attrib a { color: var(--ink-2); }
:root[data-theme="dusk"] .map .maplibregl-ctrl-icon,
:root[data-theme="night"] .map .maplibregl-ctrl-icon { filter: invert(0.82); }

/* ---- map brightness dimmer (map/dimmer.ts) ----
   The filter lands on MapLibre's two inner containers, not on .map. The canvas
   container carries everything that is map information — field raster, arrows,
   overlays, symbols, and the DOM markers, which MapLibre parents to that same
   element. The control container (zoom, compass, attribution) goes down with it
   because S-52 §3.4.3 is explicit that added non-chart information must never
   generate more light than the chart display itself, and measurement in
   dev/nightfield.ts found exactly that failure: undimmed, MapLibre's white zoom
   buttons became the brightest object on the screen below about 40% dim, out-
   glowing the entire map.

   What stays lit is .map's own children — the layer control and the dimmer's own
   slider. Dimming the widget that undoes the dimming is a trap, and at the
   bottom of the range an unrecoverable one; IEC 62288 §7.2.1.2(c) wants a way
   back from night dimming, and this is it.

   The class gate keeps an undimmed map off the compositing layer a standing
   `filter` would otherwise force. */
.mapdim .maplibregl-canvas-container,
.mapdim .maplibregl-control-container { filter: brightness(var(--map-dim, 1)); }
.mapdimctl {
  position: absolute; right: 10px; bottom: 24px; z-index: 6;
  display: flex; align-items: center; gap: 7px;
  padding: 4px 9px; background: var(--surface); border: 1px solid var(--line);
  border-radius: 2px; font-size: 11px; color: var(--ink-2);
}
.mapdimctl[hidden] { display: none; }
.mapdimctl .mdc-icon { font-size: 12px; color: var(--muted); }
.mapdimctl .mdc-range { width: 104px; accent-color: var(--accent); }
.mapdimctl .mdc-val { font-family: var(--mono); font-size: 10.5px; min-width: 3.2em; text-align: right; }

/* on-brand checked state for the layer-control / vessel checkboxes */
input[type="checkbox"] { accent-color: var(--accent); }

/* tropical callout in the location primer / window briefing */
.trop-note { font-size: 13px; line-height: 1.45; margin: 0 0 10px; padding: 7px 10px; border-radius: 3px; border: 1px solid; }
.trop-note.warn { color: var(--warn); border-color: var(--warn); background: color-mix(in srgb, var(--warn) 8%, transparent); }
.trop-note.crit { color: var(--crit); border-color: var(--crit); background: color-mix(in srgb, var(--crit) 9%, transparent); }
.trop-note strong { color: inherit; }

/* sound-speed structure block in the location primer (survey acoustics) */
.svp-note { font-size: 13px; line-height: 1.5; margin: 10px 0 0; color: var(--ink-2); }
.svp-body .svp-note { margin-top: 2px; } /* first child of its own panel body — no big gap */
.svp-note .svp-k { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--m1); margin-right: 6px; }
.svp-note .svp-src { display: block; font-size: 11px; color: var(--muted); margin-top: 3px; }
/* --- operation-context panels (panel/factpanel.ts — SEEP, EBS) --------------
   A list of stated facts, not a chart. Each row is label / reading / what it is
   FOR, because a number with no operational consequence is decoration in a panel
   whose whole claim is that it is relevant to one job.

   .fp-none is the load-bearing style here. An unavailable reading is PRINTED,
   with its reason, never dropped — "we did not fetch that" and "that quantity is
   benign here" look identical once a row disappears, and only one of them is
   safe to act on. So it is muted and italic (present, not a value) rather than
   hidden, and it keeps the row's full label so the gap is nameable. */
.fp-body { display: flex; flex-direction: column; gap: 11px; margin-top: 2px; }
.fp-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px; }
.fp-lab {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--muted);
}
.fp-val { font-size: 13.5px; color: var(--ink); font-variant-numeric: tabular-nums; }
.fp-val.fp-none { font-size: 12.5px; color: var(--muted); font-style: italic; }
.fp-why { font-size: 11.5px; line-height: 1.45; color: var(--ink-2); margin-top: 2px; }
/* A caution row is qualified, not alarming: the accent is an edge, and the
   reading itself stays in ink. Amber rather than red — none of these rows is a
   go/no-go verdict, and a red bar beside a workability panel would read as one. */
.fp-caution { border-left: 2px solid var(--warn); padding-left: 8px; margin-left: -10px; }
.fp-empty { font-size: 12.5px; color: var(--muted); margin: 2px 0 0; }
.fp-note {
  font-size: 11.5px; line-height: 1.45; color: var(--ink-2);
  margin: 4px 0 0; padding-top: 9px; border-top: 1px solid var(--line);
}
/* Sound-speed equation selector */
.svp-ctl { display: flex; align-items: center; gap: 8px; margin: 6px 0 2px; }
.svp-ctl-lab { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
.svp-eq { font-size: 12px; padding: 3px 8px; }
/* Depth-range zoom: segmented presets sharing the rungseg look, but compact —
 * buttons size to their labels instead of filling the rail. The row hides when
 * the column is too shallow to offer any cut (see syncZoomCtl in main.ts). */
.svp-zoom { margin: 0; }
.svp-zoom .rungseg-btn { flex: 0 0 auto; min-height: 24px; padding: 3px 9px; font-size: 10.5px; }
/* Off-screen mount for the full-column PNG source while zoomed: in the document
 * (getComputedStyle must resolve the curve classes) but never visible or in flow. */
.svp-png-scratch { position: absolute; visibility: hidden; pointer-events: none; }
/* SVP profile: three depth-down panels — temperature / salinity / sound velocity */
.svp-svg { display: block; width: 100%; height: auto; margin: 8px 0 2px; }
/* Curve styles are shared by the plot (.svp-svg) and the legend keys (.svp-lgkey):
 *  one rule per curve means the legend sample IS the drawn line's pattern — dash,
 *  weight and colour cannot drift apart (Les, 2026-07-21: generic CSS dashed keys
 *  made the float and the climatology indistinguishable). */
:is(.svp-svg, .svp-lgkey) .svp-band { fill: var(--m3); opacity: 0.13; }
.svp-svg .svp-grid { stroke: var(--grid); stroke-width: 1; }
:is(.svp-svg, .svp-lgkey) .svp-mld { stroke: var(--axis); stroke-width: 1; stroke-dasharray: 3 3; }
.svp-svg .svp-axisln { stroke: var(--serious); stroke-width: 1; stroke-dasharray: 2 2; }
/* Panel separators — a clear vertical line between the T / S / SV panels. */
.svp-svg .svp-sep { stroke: var(--axis); stroke-width: 1; opacity: 0.5; }
/* Seabed (NCEI charted depth) — the survey's floor, heavier than any gridline. */
.svp-svg .svp-seabed { stroke: var(--ink-2); stroke-width: 1.5; }
.svp-svg .svp-seabed-lab { fill: var(--ink-2); font-family: var(--mono); font-size: 8.5px; }
:is(.svp-svg, .svp-lgkey) .svp-sound { fill: none; stroke: var(--m1); stroke-width: 1.7; stroke-linejoin: round; stroke-linecap: round; }
:is(.svp-svg, .svp-lgkey) .svp-temp { fill: none; stroke: var(--m3); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; }
:is(.svp-svg, .svp-lgkey) .svp-sal { fill: none; stroke: var(--m2); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; }
/* Observed (Argo float) curve — the reserved observations colour, dashed so it
 *  reads as measured-and-separate against the solid modelled curves it sits beside. */
:is(.svp-svg, .svp-lgkey) .svp-obs { fill: none; stroke: var(--obs); stroke-width: 1.6; stroke-dasharray: 4 3; stroke-linejoin: round; stroke-linecap: round; }
/* Climatology (WOA23) curve — the "usual" reference. Muted and thin, dashed finely so
 *  it reads as quieter than both the solid model and the heavier dashed observed cast. */
:is(.svp-svg, .svp-lgkey) .svp-clim { fill: none; stroke: var(--muted); stroke-width: 1.2; stroke-dasharray: 2 3; stroke-linejoin: round; stroke-linecap: round; opacity: 0.85; }
.svp-svg .svp-ax { fill: var(--muted); font-family: var(--mono); font-size: 9px; }
.svp-svg .svp-lab { fill: var(--muted); font-family: var(--mono); font-size: 9px; }
.svp-svg .svp-ptitle { fill: var(--ink-2); font-family: var(--mono); font-size: 10px; letter-spacing: 0.02em; }
.svp-svg .svp-unit { fill: var(--muted); font-family: var(--mono); font-size: 8.5px; }
.svp-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; margin: 0; font-size: 11px; color: var(--muted); }
.svp-legend .svp-lg { display: inline-flex; align-items: center; }
/* Each entry's key is an inline SVG sample drawn with the plot's own curve class
 *  (see the :is(.svp-svg, .svp-lgkey) rules above) — no ::before approximations. */
.svp-legend .svp-lgkey { width: 20px; height: 8px; margin-right: 5px; flex: 0 0 auto; }
/* The band's in-plot opacity (0.13) vanishes at swatch size; the key needs more. */
.svp-lgkey .svp-band { opacity: 0.35; }
.svp-legend .svp-lg-sound { color: var(--m1); }
.svp-legend .svp-lg-temp { color: var(--m3); }
.svp-legend .svp-lg-sal { color: var(--m2); }
.svp-legend .svp-lg-mld { color: var(--muted); }
.svp-legend .svp-lg-band { color: var(--muted); }
.svp-legend .svp-lg-obs { color: var(--obs); }
.svp-legend .svp-lg-clim { color: var(--muted); }
/* Provenance for the observed overlay — float, age, distance, reach — in the
 *  observations colour so the line and the curve read as one statement. */
.svp-obs-src { font-size: 11px; color: var(--obs); margin: 4px 0 0; }
/* Actions row: ship's-surface-SV input (Task 2) + the CSV download (Task 1). Wraps on
 *  narrow rails; the download sits at the end, pushed right when there is room. */
.svp-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin: 6px 0 2px; }
.svp-ship-ctl { display: flex; align-items: center; gap: 8px; }
.svp-ship { font-size: 12px; padding: 3px 8px; width: 8.5em; flex: 0 0 auto; }
.svp-dl { margin-left: auto; padding: 5px 12px; font-size: 12px; }
/* Δ chips ride on the legend entries: the closest-match one keyed to --good, the rest
 *  muted. Small, monospace, tight — a read at a glance, not a callout. */
.svp-dchip { font-family: var(--mono); font-size: 10px; letter-spacing: 0.02em; margin-left: 6px; white-space: nowrap; }
.svp-dchip.ok { color: var(--good); }
.svp-dchip.muted { color: var(--muted); }
.svp-match-note { font-size: 11px; line-height: 1.45; color: var(--ink-2); margin: 4px 0 0; }
/* Draggable splitter between map and sidebar. Its width is the CSS var above. */
.gutter { height: 100%; background: var(--line); cursor: col-resize; position: relative; touch-action: none; user-select: none; }
.gutter::after { content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 2px; height: 28px; border-radius: 2px; background: var(--muted); opacity: 0.5; }
.gutter:hover, .gutter.dragging { background: var(--accent); }
.gutter:hover::after, .gutter.dragging::after { background: #fff; opacity: 0.9; }
.col2 { height: 100%; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; }
@media (max-width: 880px) {
  /* The .wsclosed selector outweighs a bare .appbody, so it must be named here
     too or a closed panel would resurrect the 4-column template on phones. */
  .appbody, .appbody.wsclosed {
    height: auto;
    grid-template-columns: 1fr;
    grid-template-rows: none;
    grid-template-areas: "rail" "wspanel" "map" "ctable" "col2";
  }
  .map { height: 52vh; border-bottom: 1px solid var(--line); }
  .gutter { display: none; }
  .col2 { height: auto; }
}

/* ---- data block (OWID-style: title · subtitle · content · source) ---- */
.block { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); padding: 14px 16px 12px; }
.block .bh { display: flex; align-items: baseline; gap: 10px; padding-bottom: 10px; }
/* collapsible: the chevron header folds the block to its title (bridge screens are small) */
.block .bh.clickable { cursor: pointer; user-select: none; }
/* The disclosure chevron. It was --muted at 13px, which on the Night table is a
   hairline glyph at about 2:1 against the paper — present in the DOM and absent
   on the bridge. Ink-2 at 15px with a padded box gives it both contrast and a
   target you can hit on a moving vessel. */
.block .bh .chev { margin-left: auto; color: var(--ink-2); font-family: var(--mono); font-size: 15px; line-height: 1; padding: 1px 3px; display: inline-block; transform: rotate(90deg); transition: transform 0.15s; }
.block .bh.clickable:hover .chev { color: var(--ink); }
.block.collapsed .bh .chev { transform: rotate(0deg); }
.block.collapsed > *:not(.bh) { display: none; }
.block.collapsed .bh { padding-bottom: 0; }
.block .bt { font-size: 14px; font-weight: 600; letter-spacing: -0.01em; }
.block .bsub { font-family: var(--mono); font-size: 11px; color: var(--muted); letter-spacing: 0.02em; }
.block .bsrc { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--line); }
.block .note { font-size: 13px; color: var(--muted); }

.chart-wrap { width: 100%; height: 175px; }
.chart-wrap canvas { width: 100%; height: 100%; display: block; }
/* A plot you can open on its own (chart/expand.ts). The affordance is a cursor
   and a focus ring, nothing painted — a chart card with a button on it is a
   chart card with less chart on it. */
.chart-wrap.expandable { cursor: zoom-in; }
.chart-wrap.expandable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- values at the cursor (chart/readout.ts) ----
   Was a box drawn inside the canvas at the pointer: it covered the traces it
   described, and once observations pushed it past the canvas height the clamp
   pinned it to the top and the last rows — the map field's own line — were
   drawn off the bottom edge and lost. It is DOM now, above the plot, and it
   wraps. Nothing here can clip: no fixed height, no overflow rule. */
.creadout {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 14px;
  font-family: var(--mono); font-size: 11px; line-height: 1.5;
  color: var(--ink-2); padding: 0 0 7px;
}
.creadout[hidden] { display: none; }
.cr-time { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
/* Nothing pointed at, nothing scrubbed: the strip states the present hour and
   says that is what it is doing, muted, so "now" is never read as "here". */
.cr-time.idle { color: var(--muted); font-weight: 500; }
.cr-row { display: inline-flex; align-items: baseline; gap: 5px; min-width: 0; }
.cr-sw { flex: none; width: 9px; height: 9px; border-radius: 1px; align-self: center; }
.cr-lab { color: var(--muted); }
.cr-val { color: var(--ink); font-variant-numeric: tabular-nums; }
/* The squall reasoning. Full width so a sentence stays a sentence rather than
   being dealt out between the model chips. */
.cr-extra { flex-basis: 100%; color: var(--muted); font-size: 10.5px; }

/* ---- one chart, enlarged (chart/expand.ts) ----
   Same overlay idiom as the operations-password gate above: a centered .block
   on a dimmed backdrop. The card is on <body>, never inside the map container —
   the night dimmer's CSS filter would make that container the containing block
   for anything fixed inside it. */
.cmodal-backdrop {
  position: fixed; inset: 0; z-index: 70;
  background: rgba(22, 35, 46, 0.38);
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.cmodal {
  width: min(1100px, 100%); max-height: 92vh; overflow-y: auto;
  box-shadow: 0 12px 40px rgba(22, 35, 46, 0.25);
}
.cmodal .bh { padding-bottom: 8px; }
.cmodal .creadout { font-size: 12px; padding-bottom: 10px; }
.cmodal .cr-extra { font-size: 11.5px; }
/* The room the modal exists to give: every trace separated, the limit line and
   the no-go bands legible, the ensemble spread readable as a band. */
.cmodal .chart-wrap { height: min(56vh, 560px); }
.cmodal-x {
  margin-left: auto; background: none; border: 0; padding: 2px 4px;
  font-family: var(--mono); font-size: 14px; line-height: 1;
  color: var(--muted); cursor: pointer;
}
.cmodal-x:hover { color: var(--ink); }
.cmodal-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- forms (bottom-border field) — for the settings/vessels views ---- */
label.fld { display: block; }
label.fld .lab { display: block; font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); margin-bottom: 7px; }
label.fld input, label.fld select {
  width: 100%; font: inherit; font-size: 14px; padding: 8px 2px;
  background: transparent; color: var(--ink); border: 0; border-bottom: 1.5px solid var(--line-2);
}
label.fld input:focus, label.fld select:focus { outline: none; border-bottom-color: var(--accent); }

.btn { font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; padding: 9px 16px; border-radius: var(--r); border: 1px solid var(--line-2); background: var(--surface); color: var(--ink); }
.btn.primary { background: var(--navy); color: #fff; border-color: var(--navy); }
.btn.primary:hover { background: #0f1a23; }
.btn:disabled { opacity: 0.5; cursor: default; }

/* ---- header extras ---- */
.coordbox .disputed { color: var(--crit); font-weight: 600; }
.site-chip { display: inline-block; padding: 1px 7px; border: 1px solid var(--line-2); border-radius: 2px; letter-spacing: 0.04em; }
.site-chip.land { color: var(--warn); border-color: var(--warn); }
.site-chip.near { color: var(--accent); border-color: var(--accent); }
.site-chip.water { color: var(--ink-2); }
.landnote { border-left: 3px solid var(--warn); }
.landnote .note { color: var(--ink-2); font-size: 13.5px; }
.loginlink { font-size: 13px; font-weight: 500; color: var(--ink-2); text-decoration: none; }
.loginlink:hover { color: var(--ink); }

/* ---- interior pages (vessels, login) ---- */
.page { flex: 1; width: 100%; max-width: 64rem; margin: 0 auto; padding: 22px 20px 60px; display: flex; flex-direction: column; gap: 16px; overflow-y: auto; }
.eyebrow { font-family: var(--mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--accent); }
.login-wrap { flex: 1; display: grid; place-items: center; padding: 40px 20px; }
.login-card { width: 100%; max-width: 340px; }
.field-err { font-family: var(--mono); font-size: 11.5px; color: var(--crit); margin-top: 8px; }
/* .hold is a WAIT, not a refusal — a throttle or a busy server, neither of which
   looked at the password (ui/retry.ts). Amber rather than red, because a red
   line on a password field reads as "wrong password" whatever it says. */
.field-err.hold { color: var(--warn); }

/* ---- vessels page ---- */
.lim-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 6px; }
.lim-table th { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); text-align: left; font-weight: 500; padding: 6px 8px 6px 0; border-bottom: 1px solid var(--line); }
.lim-table td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line); vertical-align: middle; }
.lim-table input[type="number"] { font: inherit; font-size: 13px; padding: 4px 6px; border: 0; border-bottom: 1.5px solid var(--line-2); background: transparent; color: var(--ink); }
.lim-table input[type="number"]:focus { outline: none; border-bottom-color: var(--accent); }
.lim-table .btn { padding: 4px 10px; font-size: 12px; }
.vrow-dim { opacity: 0.45; }
.chip { display: inline-block; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.03em; padding: 2px 8px; border: 1px solid var(--line-2); border-radius: 2px; white-space: nowrap; }
.chip.warn { color: var(--warn); border-color: var(--warn); }
.chip.ok { color: var(--good); border-color: var(--good); }
.catalog-form { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 8px 0; }
.catalog-form input[type="text"], .catalog-form select { font: inherit; font-size: 13px; padding: 6px 8px; border: 1px solid var(--line-2); border-radius: 2px; background: var(--surface); color: var(--ink); }

/* header action buttons (Edit / Delete on a vessel) */
.bh-actions { margin-left: auto; display: flex; gap: 8px; }
.bh-actions .btn { padding: 4px 12px; font-size: 12px; }
.vessel-gear { margin-bottom: 4px; }

/* vessel particulars form — a responsive field grid shared by add + edit */
.vessel-edit { margin: 4px 0 2px; }
.vform { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px 16px; align-items: end; }
.vform .vform-wide { grid-column: 1 / -1; }
.vform label.fld input, .vform label.fld textarea { font: inherit; font-size: 13px; width: 100%; padding: 6px 2px; border: 0; border-bottom: 1.5px solid var(--line-2); background: transparent; color: var(--ink); resize: vertical; }
.vform label.fld input:focus, .vform label.fld textarea:focus { outline: none; border-bottom-color: var(--accent); }
.vform-checks { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 8px 20px; }
.vform-check { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; cursor: pointer; }
.vform-check input { margin: 0; }
.vform-actions { display: flex; gap: 10px; margin-top: 14px; }

/* ---- operations-password gate (ui/opsgate.ts) ----
   The confirm dialog for spec changes: a centered .block over a dimmed page.
   Chrome stays achromatic per the system rule; the only color is .field-err —
   red when a password is refused, amber (.hold) when the server named a wait
   instead of checking it. */
.opsgate-backdrop {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(22, 35, 46, 0.38);
  display: flex; align-items: center; justify-content: center; padding: 18px;
}
.opsgate { width: min(460px, 100%); max-height: 90vh; overflow-y: auto; box-shadow: 0 12px 40px rgba(22, 35, 46, 0.25); }
.opsgate .opsgate-summary { font-size: 13.5px; line-height: 1.5; margin-bottom: 10px; }
.opsgate .opsgate-lines {
  list-style: none; font-family: var(--mono); font-size: 12px; line-height: 1.7;
  border: 1px solid var(--line); border-radius: var(--r); background: var(--surface-2);
  padding: 8px 12px; margin-bottom: 10px;
}
.opsgate .note { margin-bottom: 12px; }
.opsgate label.fld { margin-bottom: 12px; }
.opsgate label.fld input {
  font: inherit; font-size: 13px; width: 100%; padding: 6px 2px;
  border: 0; border-bottom: 1.5px solid var(--line-2); background: transparent; color: var(--ink);
}
.opsgate label.fld input:focus { outline: none; border-bottom-color: var(--accent); }
.opsgate .vform-actions { justify-content: flex-end; }

/* ---- workability / briefing panels ---- */
.fieldrow { display: flex; gap: 10px; padding: 2px 0 8px; }
.sel { font: inherit; font-size: 13px; padding: 5px 8px; border: 1px solid var(--line-2); border-radius: 2px; background: var(--surface); color: var(--ink); max-width: 100%; flex: 1; min-width: 0; }
/* The workability ribbon itself is a canvas in the track stack (chart/ribbon.ts),
   drawn on the shared axis; it has no styles here. Its day labels went with it:
   the stack's ruler already draws them once for every track. Only the colour key
   is still CSS, and it is shared with the sea-character legend. */
.ribbon-key { display: flex; gap: 12px; flex-wrap: wrap; font-family: var(--mono); font-size: 10.5px; color: var(--ink-2); margin-top: 8px; }
.ribbon-key i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }
.windows { display: flex; gap: 6px; flex-wrap: wrap; min-height: 22px; }
.warnrow { font-family: var(--mono); font-size: 11px; color: var(--warn); margin-top: 9px; letter-spacing: 0.02em; }
/* Fleet roster: one row per hull, click to arm. The armed row carries the same
   accent stripe the map-fed track carries — one visual grammar for "this is the
   one everything is keyed to". */
.fleetlist { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }
.fleetrow {
  display: grid; grid-template-columns: 1fr auto; gap: 1px 8px; align-items: baseline;
  text-align: left; padding: 7px 9px; border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface); cursor: pointer; font: inherit; color: var(--ink);
}
.fleetrow:hover { background: var(--surface-2); }
.fleetrow.armed { box-shadow: inset 3px 0 0 var(--accent); background: var(--surface-2); }
.fleetname { font-weight: 600; font-size: 13px; }
.fleetspecs { grid-row: 2; font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
.fleetchip { grid-row: 1 / span 2; align-self: center; font-family: var(--mono); font-size: 10px; padding: 2px 6px; border-radius: 3px; }
.fleetchip.warn { color: var(--warn); border: 1px solid var(--warn); }
.fleetchip.ok { color: var(--good); border: 1px solid var(--good); }

/* Systems board: one row per upstream. Dot = derived status; the detail line is
   the reason, already worded. */
.syslist { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; }
.sysrow {
  display: grid; grid-template-columns: 12px 1fr; gap: 0 8px; align-items: baseline;
  padding: 6px 4px; border-top: 1px solid var(--line);
}
.sysdot { width: 9px; height: 9px; border-radius: 50%; align-self: center; }
.sysdot.ok { background: var(--good); }
.sysdot.warn { background: var(--warn); }
.sysdot.down { background: var(--crit); }
.sysdot.idle { background: transparent; border: 1px solid var(--muted); }
.sysname { font-weight: 600; font-size: 12.5px; }
.sysdetail { grid-column: 2; font-family: var(--mono); font-size: 10.5px; color: var(--ink-2); }
.sysfeeds { grid-column: 2; font-family: var(--mono); font-size: 10px; color: var(--muted); }

/* Tropical workspace: one card per system/outlook — thumbnail answers WHERE,
   chip answers WHAT, the mono line answers HOW HARD. Cards that can fly the
   map are <button>s; the reset below makes the two tags read identically. */
.tropcards { display: flex; flex-direction: column; gap: 8px; }
.tropcard { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center; width: 100%; text-align: left; background: var(--surface); border: 1px solid var(--line); border-radius: 3px; padding: 8px; margin: 0; color: var(--ink); font: inherit; }
button.tropcard { cursor: pointer; }
button.tropcard:hover, button.tropcard:focus-visible { border-color: var(--accent); }
.tropthumb { display: block; width: 72px; height: 72px; border: 1px solid var(--line); border-radius: 2px; }
.tc-head { display: flex; align-items: center; gap: 7px; min-width: 0; }
.tc-chip { flex: none; }
.chip.crit { color: var(--crit); border-color: var(--crit); }
.tc-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tc-fact { font-family: var(--mono); font-size: 11px; color: var(--ink-2); margin-top: 4px; }
.tc-note { font-size: 10.5px; color: var(--muted); margin-top: 3px; }
/* One-line hazard flag inside a track header. Never wraps: the header column is
   150 px, and prose allowed to wrap there stacks into a tall skinny block that
   breaks the track rhythm — the full sentence lives in the title attribute and
   the chart tooltip instead. */
.trackflag {
  font-family: var(--mono); font-size: 10px; color: var(--warn);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0; cursor: help;
}
.sliders { display: flex; gap: 18px; flex-wrap: wrap; padding-bottom: 8px; }
.sliders .sld { display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); flex: 1; min-width: 180px; }
.sliders input[type="range"] { flex: 1; accent-color: var(--navy); min-width: 80px; }
.sliders output { min-width: 44px; color: var(--ink); }
.brief { font-size: 14px; line-height: 1.6; }
.brief p { margin: 8px 0 0; color: var(--ink-2); }
.brief .lede { font-weight: 600; color: var(--ink); }
.brief .verdict { display: inline-block; color: #fff; font-size: 11.5px; font-weight: 600; padding: 1px 8px; border-radius: 2px; margin-left: 8px; vertical-align: 1px; }
.brief .b-note { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.brief .quiet { color: var(--muted); font-size: 12px; }

/* ---- public site ---- */
.bare { position: relative; min-height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.contour-bg { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0.45; pointer-events: none; }
.bare-top { position: relative; display: flex; justify-content: flex-end; padding: 26px 40px 0; }
.bare-menu { display: flex; gap: 28px; font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.05em; }
.bare-menu a { color: var(--muted); text-decoration: none; }
.bare-menu a:hover { color: var(--ink); }
.bare-menu a.login { color: var(--ink-2); }
.bare-center { position: relative; flex: 1; display: grid; place-items: center; padding: 40px 40px 16vh; }

.waves-bg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.lockup { position: relative; display: flex; flex-direction: column; align-items: center; }
/* The mark is an inline SVG now, so it takes its colour from here and its size
   from the width alone — the viewBox carries the aspect, at any scale. */
.lockup-mark { width: clamp(130px, 17vw, 200px); display: block; color: var(--navy); }
.lockup-mark svg { display: block; width: 100%; height: auto; }
.lockup-word { margin-top: 30px; font-size: clamp(28px, 4.6vw, 44px); font-weight: 300; letter-spacing: 0.3em; text-indent: 0.3em; color: var(--navy); }
.lockup-tag { margin-top: 12px; font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.3em; text-indent: 0.3em; text-transform: uppercase; color: var(--muted); }

.site-page { max-width: 1040px; margin: 0 auto; padding: 0 40px; width: 100%; }
.pubnav { display: flex; align-items: center; padding: 26px 0; gap: 40px; border-bottom: 1px solid var(--line); }
.pubnav .brand { text-decoration: none; color: var(--ink); }
.pubnav .brand .mark { color: var(--navy); }
.pubnav-links { margin-left: auto; display: flex; align-items: center; gap: 30px; font-size: 14.5px; }
.pubnav-links a { color: var(--ink-2); text-decoration: none; }
.pubnav-links a:hover { color: var(--ink); }
.pubnav-links a.loginlink { color: var(--ink); font-weight: 500; }

.lead { padding: 56px 0 26px; max-width: 62ch; }
.lead h2 { font-family: var(--serif); font-size: clamp(28px, 4.4vw, 42px); font-weight: 400; letter-spacing: -0.015em; line-height: 1.12; margin: 14px 0 0; text-wrap: balance; }
.lead p { color: var(--ink-2); font-size: 17.5px; margin: 20px 0 0; line-height: 1.62; }

.doc { display: grid; grid-template-columns: 188px 1fr; gap: 56px; padding: 26px 0 80px; border-top: 1px solid var(--line); }
.doc aside { position: sticky; top: 24px; align-self: start; font-family: var(--mono); font-size: 12px; display: flex; flex-direction: column; }
.doc aside a { color: var(--muted); padding: 6px 0 6px 12px; letter-spacing: 0.03em; border-left: 1px solid var(--line-2); text-decoration: none; }
.doc aside a:hover { color: var(--ink); border-color: var(--accent); }
.doc article { max-width: 64ch; }
.doc article h3 { font-size: 13px; font-family: var(--mono); font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink); margin: 42px 0 0; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.doc article h3:first-child { margin-top: 0; }
.doc article p { color: var(--ink-2); margin: 14px 0 0; font-size: 15.5px; line-height: 1.7; }
.doc article .callout { border-left: 2px solid var(--navy); padding: 2px 0 2px 18px; margin-top: 22px; color: var(--ink); font-size: 15.5px; }

.sitefoot { border-top: 1px solid var(--line); padding: 24px 0 56px; display: flex; gap: 18px; flex-wrap: wrap; color: var(--muted); font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.03em; }
.sitefoot .right { margin-left: auto; }

@media (max-width: 760px) {
  .doc { grid-template-columns: 1fr; gap: 22px; }
  .doc aside { position: static; flex-direction: row; flex-wrap: wrap; gap: 4px 16px; }
  .doc aside a { border-left: 0; padding-left: 0; }
  .site-page, .bare-top { padding-left: 22px; padding-right: 22px; }
}

/* ---- map overlay chrome (layer control + tropical badge) ---- */
/* Clickable: the badge is also the door to the Tropical workspace's storm list. */
.trop-badge { position: absolute; top: 10px; right: 10px; z-index: 5; background: var(--surface); border: 1px solid var(--line); border-radius: 2px; padding: 4px 10px; font-family: var(--mono); font-size: 11px; color: var(--ink-2); cursor: pointer; letter-spacing: 0.02em; }
.trop-badge:hover { border-color: var(--accent); color: var(--ink); }
.layerctl { position: absolute; left: 10px; bottom: 24px; z-index: 6; background: var(--surface); border: 1px solid var(--line); border-radius: 2px; font-size: 12px; color: var(--ink-2); max-width: 250px; overflow: hidden; box-shadow: 0 1px 4px rgba(22,35,46,0.10); }
.layerctl > summary { cursor: pointer; padding: 6px 11px; font-weight: 600; list-style: none; user-select: none; display: flex; align-items: center; gap: 6px; }
.layerctl > summary::-webkit-details-marker { display: none; }
.layerctl > summary::before { content: "▸"; font-size: 10px; color: var(--muted); }
.layerctl[open] > summary::before { content: "▾"; }
.layerctl .lc-body { padding: 4px 11px 9px; display: flex; flex-direction: column; gap: 7px; border-top: 1px solid var(--line); }
.layerctl label { display: flex; gap: 7px; align-items: flex-start; cursor: pointer; line-height: 1.25; }
.layerctl label small { display: block; color: var(--muted); font-size: 10.5px; }
.layerctl input { margin: 2px 0 0; flex: none; }
.layerctl .lc-swatch { width: 10px; height: 10px; border-radius: 2px; margin: 2px 0 0; flex: none; }
.layerctl .lc-range { border-top: 1px solid var(--line); padding-top: 8px; margin-top: 1px; }
.layerctl .lc-swatch-range { background: linear-gradient(90deg, #12405f, #2f6f9f 55%, #8fb8d4); }
.layerctl .lc-range-legend { display: none; flex-direction: column; gap: 5px; padding: 1px 0 0 17px; }
.layerctl .lc-rl-row { display: flex; gap: 7px; align-items: center; font-size: 11px; color: var(--ink-2); }
.layerctl .lc-rl-row .lc-swatch { margin: 0; }

/* full port-record popup */
.portcard .maplibregl-popup-content { font-family: var(--sans); font-size: 12.5px; line-height: 1.5; color: var(--ink-2); padding: 12px 14px; max-height: 320px; overflow-y: auto; }
.portcard .port-head { font-size: 14px; color: var(--ink); }
.portcard .port-sec { font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-top: 8px; border-bottom: 1px solid var(--line); padding-bottom: 2px; }
/* .port-fit is the card's plain statement row (home port, tide-dependent entry).
   It carries no colour: the .ok/.bad pair went with the draft-fit verdict. */
.portcard .port-fit { font-family: var(--mono); font-size: 11px; margin-top: 6px; }
.portcard .quiet { color: var(--muted); font-size: 11px; }

/* --- Help surface: inline ⓘ affordance, popover, deep-link, doc pages --------- */
/* All token-driven, so it re-casts across the Day/Dusk/Night ECDIS palettes. */

/* the ⓘ glyph: a small circled italic i beside a title/label/legend row */
.help-i { display: inline-grid; place-items: center; width: 14px; height: 14px; margin-left: 5px; padding: 0; border: 1px solid var(--line-2); border-radius: 50%; background: transparent; color: var(--muted); font-family: var(--serif); font-style: italic; font-size: 10px; line-height: 1; cursor: help; vertical-align: middle; flex: none; }
.help-i:hover, .help-i:focus-visible, .help-i[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }

/* the single reused popover (fixed, rides above the map canvas) */
.help-pop { position: fixed; z-index: 60; max-width: 300px; background: var(--surface); border: 1px solid var(--line-2); border-radius: 4px; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18); padding: 11px 13px; font-family: var(--sans); }
.help-pop[hidden] { display: none; }
.help-pop-term { margin: 0; font-size: 12.5px; font-weight: 600; color: var(--ink); }
.help-pop-short { margin: 5px 0 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-2); }
.help-pop-meta { margin: 6px 0 0; font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
.help-pop-limit { margin: 8px 0 0; padding-left: 8px; border-left: 2px solid var(--warn); font-size: 11.5px; line-height: 1.4; color: var(--ink-2); }
.help-pop-more { display: inline-block; margin-top: 9px; font-size: 11.5px; color: var(--accent); text-decoration: none; }
.help-pop-more:hover { text-decoration: underline; }

/* inline text deep-link into the Methods page */
.help-link { color: var(--accent); text-decoration: none; border-bottom: 1px dotted var(--accent); }
.help-link:hover { border-bottom-style: solid; }

/* Link out to a buoy's own NDBC station page — sits to the right in the panel header. */
.ndbc-link { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--accent); text-decoration: none; border-bottom: 1px dotted var(--accent); white-space: nowrap; }
.ndbc-link:hover { border-bottom-style: solid; }

/* Wind-field control + hourly scrubber — floats over the map, bottom-centre. */
.windctl { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 5;
  display: flex; flex-direction: column; align-items: center; gap: 6px; max-width: 90%;
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18); }
.windctl-toggles { display: flex; align-items: center; gap: 12px; }
.windctl-title { font-family: var(--mono); font-size: 11px; letter-spacing: 0.03em; text-transform: uppercase; color: var(--ink-2); }
.windctl-toggle { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.03em; text-transform: uppercase; color: var(--ink); }
.windctl-legend { display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
.windctl-ramp { width: 150px; height: 9px; border-radius: 2px; border: 1px solid var(--line);
  background: linear-gradient(90deg, rgb(26,120,60) 0%, rgb(46,160,90) 34%, rgb(166,217,106) 55%,
    rgb(253,206,80) 75%, rgb(244,140,60) 88%, rgb(214,47,39) 100%); }
.windctl-leg-hi { color: var(--crit); }
/* The active layer's name, large, on the map itself — the colours mean nothing
   without knowing which quantity they encode, and a checkbox in a side panel is
   not an answer at a glance. Display-only; the layer list stays the one control. */
.windctl-layerbig {
  font-family: var(--mono); font-size: 17px; font-weight: 700;
  letter-spacing: 0.14em; color: var(--ink); line-height: 1.1;
  cursor: help;
}
.windctl-layerbig:empty { display: none; }
/* WHICH FIELD the direction arrows are. Load-bearing, not decoration: every
   arrow the map draws is the same ordinary shafted arrow, so shape says nothing
   about which variable a heading belongs to and only these words do. Sat under
   the layer name, in the layer name's ink rather than the muted source grey —
   it is part of the statement of what is on screen. */
.windctl-arrows { font-family: var(--mono); font-size: 10.5px; color: var(--ink-2);
  letter-spacing: 0.06em; text-transform: uppercase; cursor: help; }
.windctl-arrows:empty { display: none; }
/* The wind/wave choice for those arrows, shown on Workability alone — the one
   layer whose raster blends both fields and so cannot answer it itself. Its own
   row under the layer toggles, indented to read as their subordinate. */
.windctl-arrowpick { display: flex; align-items: center; gap: 10px; padding-left: 20px; }
.windctl-arrowpick .windctl-toggle { font-size: 10px; color: var(--ink-2); }
.windctl-toggles-hosted .windctl-arrowpick { flex-basis: 100%; padding-left: 22px; }
/* The map's provenance line: model + cycle, muted — a source citation, not a headline. */
.windctl-src { font-family: var(--mono); font-size: 10px; color: var(--muted); letter-spacing: 0.02em; cursor: help; }
.windctl-src:empty { display: none; }
.windctl-bar { display: flex; align-items: center; gap: 6px; }
.windctl-btn { border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  border-radius: 5px; padding: 2px 9px; font-size: 12px; line-height: 1.4; cursor: pointer; }
.windctl-btn:hover, .windctl-btn.windctl-playing { background: var(--accent); color: #fff; border-color: var(--accent); }
.windctl-fwd { transform: none; }
.windctl-now { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; }
.windctl-time { font-family: var(--mono); font-size: 12px; min-width: 172px; text-align: center; color: var(--ink); }
.windctl-status { font-size: 11px; color: var(--muted); }
.windctl-status:empty { display: none; }

/* persistent header disclaimer — the ethical anchor, always one glance away */
.apphead .disclaimer { font-family: var(--mono); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); text-decoration: none; border: 1px solid var(--line); border-radius: 3px; padding: 2px 6px; white-space: nowrap; }
.apphead .disclaimer:hover { color: var(--warn); border-color: var(--warn); }

/* Guide / Methods reference pages — scoped under .helppage so the single-column
   reading layout doesn't collide with the site pages' own two-column .doc grid. */
.helppage .doc { display: block; max-width: 760px; margin: 0 auto; padding: 20px 24px 96px; font-family: var(--sans); color: var(--ink-2); line-height: 1.6; }
.helppage .doc h1 { font-size: 26px; color: var(--ink); margin: 0 0 6px; letter-spacing: -0.01em; }
.helppage .doc-lede { font-size: 15px; color: var(--ink-2); margin: 0 0 18px; }
.helppage .doc-lede h2 { border: 0; padding: 0; margin: 0 0 8px; font-size: 25px; letter-spacing: -0.01em; }
.helppage .doc h2 { font-size: 18px; color: var(--ink); margin: 36px 0 10px; padding-bottom: 6px; border-bottom: 1px solid var(--line); scroll-margin-top: 64px; }
.helppage .doc h3 { font-size: 14.5px; color: var(--ink); margin: 22px 0 6px; scroll-margin-top: 64px; }
.helppage .doc p { margin: 8px 0; font-size: 13.5px; }
.helppage .doc a { color: var(--accent); }
.helppage .doc-meta { font-family: var(--mono); font-size: 11px; color: var(--muted); margin: 2px 0 6px; }
.helppage .doc-limit { border-left: 3px solid var(--warn); background: var(--surface-2); border-radius: 0 3px 3px 0; padding: 8px 12px; margin: 10px 0; font-size: 13px; color: var(--ink-2); }
.helppage .doc-warn { border: 1px solid var(--warn); background: var(--surface-2); border-radius: 4px; padding: 12px 14px; margin: 14px 0 8px; font-size: 13.5px; color: var(--ink); }
.helppage .doc-warn strong { color: var(--warn); }
.helppage .doc-toc { margin: 8px 0 26px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface-2); display: flex; flex-wrap: wrap; gap: 6px 16px; }
.helppage .doc-toc a { font-size: 12.5px; color: var(--accent); text-decoration: none; }
.helppage .doc-toc a:hover { text-decoration: underline; }
.helppage .doc-seealso { font-size: 12px; color: var(--muted); margin: 8px 0 0; }
.helppage .doc dl { margin: 8px 0; }
.helppage .doc dt { margin-top: 12px; font-weight: 600; font-size: 13.5px; color: var(--ink); }
.helppage .doc dd { margin: 2px 0 0; font-size: 13px; }

/* ---- manual figures (generated by tools/manual/capture.mjs) ----
   --fig-ink MUST match INK in capture.mjs: the list markers below are the same
   numbers as the badges burnt into the PNG, and a reader matches them by colour
   before they match them by digit. It is deliberately outside the safety palette
   — an amber callout box would read as a warning about the thing it points at. */
.helppage { --fig-ink: #c026d3; }
.helppage .doc-fig { margin: 20px 0 10px; }
.helppage .doc-fig img { display: block; width: 100%; height: auto; border: 1px solid var(--line-2); border-radius: 4px; background: var(--surface); }
.helppage .fig-cap { font-family: var(--mono); font-size: 11.5px; color: var(--muted); letter-spacing: 0.02em; margin: 8px 0 0; }
.helppage .fig-callouts { margin: 10px 0 0; padding-left: 24px; }
.helppage .fig-callouts li { font-size: 13px; color: var(--ink-2); margin: 7px 0; line-height: 1.65; }
.helppage .fig-callouts li::marker { color: var(--fig-ink); font-weight: 700; font-variant-numeric: tabular-nums; }

/* A console screenshot carries real detail — panel text, chart traces, the
   legend — and is unreadable squeezed into a 760px prose column. Only the
   picture breaks out; the caption and callouts stay at reading width.

   The bleed is measured against .page, NOT the viewport: .page is max-width 64rem
   with 20px padding (984px of content) and sets overflow-y, which makes overflow-x
   compute to auto — so a figure sized off the viewport is CLIPPED, not merely wide.
   712px prose + 2 x 136px = 984px, exactly filling the page and no more. */
@media (min-width: 1040px) {
  .helppage .doc-fig { margin-inline: -136px; }
  .helppage .doc-fig figcaption { max-width: 712px; margin: 0 auto; }
}

/* forecast freshness stamp — how stale the currently-loaded view is */
.freshness { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.02em; color: var(--muted); padding: 1px 2px 7px; }
.freshness[hidden] { display: none; }

/* --- track stack --------------------------------------------------------
   The forecast charts, contiguous, under one shared time ruler. They are
   grouped so the ruler heads them all; the briefing, primer, validation and
   SVP panels sit BELOW the stack rather than between the charts, which is
   what lets a single x-axis serve every track. */
/* One column width for the whole stack. The ruler, the confidence strip and every
   track derive their plot origin from this single number — when the track header
   column was introduced, the ruler kept its own older inset and its ticks sat
   150px off the data they label. A shared axis that does not share the axis is
   worse than no shared axis, so there is now exactly one place to change it. */
.trackstack { --track-hdr: 150px; display: flex; flex-direction: column; }

.trackstack .rulerrow,
.trackstack .confrow {
  display: grid;
  grid-template-columns: var(--track-hdr) minmax(0, 1fr);
  align-items: stretch;
  padding: 0;
  border-left: 1px solid var(--line);
  border-right: 1px solid var(--line);
  background: var(--surface);
}
.trackstack .rulerrow {
  position: sticky; top: 0; z-index: 3;
  height: 38px;
  /* A DEFINITE row track. Left auto, the track sizes to the canvas's intrinsic
     (device-pixel) height, the canvas's own height:100% has nothing to resolve
     against, and the ruler spills 150px down over the first two charts. */
  grid-template-rows: 38px;
  border-top: 1px solid var(--line);
  border-radius: var(--r) var(--r) 0 0;
}
.trackstack .rulerrow canvas { grid-column: 2; display: block; width: 100%; height: 100%; min-height: 0; }
/* The header cell of the ruler and the confidence strip: same width as a track's,
   so the three left edges are one line. */
.trackstack .stubnm {
  grid-column: 1;
  display: flex; align-items: center;
  padding: 0 10px;
  border-right: 1px solid var(--line);
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted);
}

/* ---- the conditions table (panel/table.ts) ------------------------------
   A full-width grid pinned under the map: header bar with the tabs, then a
   fixed label column beside a horizontally scrolling canvas, then the marker
   key and the source line. Row heights come from panel/rows.ts and are written
   as inline styles onto the labels, so the label column and the drawn rows
   cannot drift apart — the same one-source-of-truth arrangement the retired
   conditions strip used. */
.ctable {
  border-top: 1px solid var(--line-2);
  background: var(--surface);
  display: flex; flex-direction: column;
  min-width: 0;
}
.ct-head {
  display: flex; align-items: center; gap: 12px;
  height: 32px; padding: 0 12px; flex: none;
  border-bottom: 1px solid var(--line);
}
.ct-title {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted);
  cursor: pointer; user-select: none; white-space: nowrap;
}
.ct-tabs { display: inline-flex; border: 1px solid var(--line-2); border-radius: 4px; overflow: hidden; flex: none; }
.ct-tab {
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em;
  padding: 3px 9px; background: transparent; color: var(--muted);
  border: 0; border-left: 1px solid var(--line); cursor: pointer; line-height: 1.4;
}
.ct-tab:first-child { border-left: 0; }
.ct-tab.on { background: var(--navy); color: var(--surface); }
.ct-tab:hover:not(.on) { color: var(--ink); }
.ct-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The status pair: a grade dot AND its word. Never the dot alone — see the
   accessibility rule in docs/CONVENTIONS.md and the greyscale discipline the
   printed report keeps. */
.ct-tab .ct-dot { width: 6px; height: 6px; border-radius: 50%; flex: none; background: var(--muted); }
.ct-tab .ct-dot.wk-good { background: var(--good); }
.ct-tab .ct-dot.wk-warn { background: var(--warn); }
.ct-tab .ct-dot.wk-serious { background: var(--serious); }
.ct-tab .ct-dot.wk-crit { background: var(--crit); }
.ct-tab em { font-style: normal; font-size: 9.5px; opacity: 0.85; }
.ct-scale {
  margin-left: auto; font-family: var(--mono); font-size: 9.5px;
  color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ct-chev {
  flex: none; background: transparent; border: 0; cursor: pointer;
  color: var(--muted); font-family: var(--mono); font-size: 14px; line-height: 1;
  padding: 2px 4px; transform: rotate(90deg); transition: transform 0.15s;
}
.ct-chev:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ctable.collapsed .ct-chev { transform: rotate(-90deg); }
.ctable.collapsed .ct-body, .ctable.collapsed .ct-foot { display: none; }

.ct-body { display: flex; min-width: 0; max-height: 46vh; overflow-y: auto; }
.ct-labels { flex: none; width: 152px; border-right: 1px solid var(--line-2); background: var(--surface); }
.ct-label {
  display: flex; flex-direction: column; justify-content: center;
  padding: 0 9px; border-bottom: 1px solid var(--line); overflow: hidden;
}
/* Two lines in a 24-25 px row: the line-heights are pinned rather than left to
   the default 1.4, which overflowed the shorter rows and clipped the note that
   names each row's model and limit. */
.ct-lname { display: flex; align-items: baseline; gap: 5px; line-height: 1.15; }
.ct-lname b { font-size: 11px; font-weight: 600; color: var(--ink); white-space: nowrap; }
.ct-lname span { font-family: var(--mono); font-size: 9px; color: var(--muted); }
.ct-lnote {
  font-family: var(--mono); font-size: 8.5px; line-height: 1.2; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The canvas never moves; the transparent overlay above it carries the real
   scrollbar and a spacer of the table's full width, and its scrollLeft is the
   canvas's draw offset. The browser keeps its own wheel, fling and keyboard
   scrolling, and only the columns on screen are ever drawn. */
.ct-view { position: relative; flex: 1 1 auto; min-width: 0; overflow: hidden; }
.ct-view canvas { position: absolute; inset: 0; display: block; }
.ct-scroll { position: absolute; inset: 0; overflow-x: auto; overflow-y: hidden; cursor: pointer; }
.ct-spacer { height: 1px; }

.ct-foot {
  flex: none; padding: 5px 12px 7px; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 3px;
}
.ct-key { display: flex; flex-wrap: wrap; gap: 4px 14px; }
.ct-key span { font-size: 10px; color: var(--ink-2); display: inline-flex; align-items: baseline; gap: 5px; }
.ct-key i {
  font-style: normal; font-family: var(--mono); font-weight: 700; font-size: 9px;
  color: var(--ink); border: 1px solid var(--line-2); border-radius: 2px; padding: 0 3px;
}
.ct-src { font-size: 10px; color: var(--muted); line-height: 1.45; }

/* Tracks butt against each other so the playhead reads as one line down the
   stack rather than as a crosshair repeated in several separate cards. */
.trackstack > .block { margin-bottom: 0; border-radius: 0; border-top: 0; }
.trackstack > .block:last-child { border-radius: 0 0 var(--r) var(--r); }

/* --- tracks -------------------------------------------------------------
   A track is a header column plus a plot, not a card. Reading it is "what
   shape, and is it over the line"; the exact number lives in the header, which
   is why a track can be a third the height of a full chart block. */
.block.track {
  display: grid;
  /* Same variable as .rulerrow/.confrow — the header column's width is the tracks'
     left edge, and three literals that happen to agree today are three chances to
     disagree tomorrow. The fallback only matters for a track outside a stack. */
  grid-template-columns: var(--track-hdr, 150px) minmax(0, 1fr);
  align-items: stretch;
  padding: 0;
  gap: 0;
}
.block.track .trackhdr {
  padding: 6px 10px;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  min-width: 0;
}
/* The plot column: the readout strip sits above the canvas, inside the second
   grid column, so the header column's width — the thing that aligns every
   track's plot origin with the ruler's — is untouched by it. */
.block.track .plotcol { display: flex; flex-direction: column; min-width: 0; }
.block.track .creadout {
  font-size: 10.5px; line-height: 1.45;
  padding: 4px 10px 3px 10px; gap: 2px 12px;
  border-bottom: 1px solid var(--line);
}
.block.track .cr-extra { font-size: 10px; }
.block.track .bh { padding-bottom: 0; }
.block.track .bt {
  font-size: 12px; font-weight: 500; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The meter: the value under the playhead. Tabular figures so it does not
   jitter as the playhead sweeps. */
.block.track .tval {
  font-family: var(--mono); font-size: 15px; font-weight: 600;
  font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
.block.track .tval.over { color: var(--crit); }
/* Workability's meter runs the other way round: it is alarming when it is LOW, so
   it carries a band class rather than .over. Slightly smaller because it says a
   percentage AND a word ("38% · Poor") in the same 150 px column. */
.block.track .tval.wk-good { color: var(--good); }
.block.track .tval.wk-warn { color: var(--warn); }
.block.track .tval.wk-serious { color: var(--serious); }
.block.track .tval.wk-crit { color: var(--crit); }
.block.track.ribbontrack .tval { font-size: 13px; }
/* The ribbon is a band, not a plot: it needs the height of its cells and nothing
   more. The canvas insets the band itself (see chart/ribbon.ts). */
.block.track.ribbontrack .chart-wrap { height: 44px; }
/* The colour key rides in the footer where a track's source line would be — it is
   the same kind of thing, a note about how to read what is above it. */
.block.track .bsrc .ribbon-key { margin-top: 0; }
.block.track .bsub { font-size: 10px; }
.block.track .chart-wrap { height: 76px; }
/* Tracks are not all worth the same height, because they do not all carry the same
   amount of information. Wind speed and wave height each stack three or four models,
   an ensemble or envelope band, a limit line, no-go shading and now the map field's
   own trace -- at 76 px those lines land within a few pixels of each other on a calm
   day and read as one thick smudge, which is exactly when you most need to see that
   the models agree. Wave period, daylight, sea character and surface current carry
   one or two lines and say everything they have to say in 76.
   Applied via a modifier rather than to :nth-child, so reordering the stack cannot
   silently hand the extra height to the wrong track. */
.block.track.tall .chart-wrap { height: 118px; }
/* DAW-style channel stripe: the track whose quantity the MAP is currently
   painting. One accent edge, the way a mixer marks the armed channel — so the
   glance that connects "those colours on the water" to "this row of numbers"
   costs nothing. Accent, not a status colour: this is identity, not judgement. */
.block.track.mapfed { box-shadow: inset 3px 0 0 var(--accent); }
.block.track.mapfed .trackhdr { border-left: 0; }
/* The source line spans both columns, muted -- provenance matters but should not
   compete with the trace for attention. */
.block.track .bsrc {
  grid-column: 1 / -1;
  margin-top: 0; padding: 5px 10px 6px;
  font-size: 10px; line-height: 1.4;
}
.block.track.collapsed .plotcol,
.block.track.collapsed .chart-wrap,
.block.track.collapsed .bsrc { display: none; }
/* Closed, a track is ONE line: its name and the value under the playhead.
   Hiding the plot alone was not closing the track — the grid still declared two
   columns, so the vanished plot left an empty rectangle the width of the stack
   with the header's own border-right ruled down the middle of it, and the header
   went on stacking three lines inside a row meant to be small. Six collapsed
   tracks cost more vertical space than three open ones and showed nothing.
   So: one column, the header laid down in a row, and the unit line dropped —
   the meter already prints its own unit, and what a closed track owes you is
   the number, not a second description of it. */
.block.track.collapsed { grid-template-columns: minmax(0, 1fr); }
.block.track.collapsed .trackhdr {
  flex-direction: row; align-items: baseline; gap: 8px;
  padding: 5px 10px; border-right: 0;
}
.block.track.collapsed .bsub { display: none; }
/* Beside the title rather than flung to the far edge: at this height the chevron
   is the only thing that says the row reopens, and it should be next to the word
   it belongs to. */
.block.track.collapsed .bh { flex: 0 0 auto; }
.block.track.collapsed .bh .chev { margin-left: 6px; }
/* Right-aligned so a run of closed tracks reads as a column of figures. */
.block.track.collapsed .tval { margin-left: auto; font-size: 13px; }

/* --- workspace rail + panel (ui/rail.ts) ---------------------------------------
   The rail is space and mode; the transport bar is time. Icon AND label at 74 px:
   this is read on a moving vessel by a weekly-at-best user, where an unlabelled
   glyph is a guess. Every control here clears a 30 px hit target for the same
   reason. The panel is exclusive — one workspace visible — while layer toggles,
   which are additive, live inside the Observations workspace rather than in the
   rail, so a rail click never means two different things. */
.rail {
  width: var(--rail-w, 74px); height: calc(100vh - 52px);
  background: var(--surface-2); border-right: 1px solid var(--line);
  display: flex; flex-direction: column; padding: 6px 0;
  /* overflow-x hidden deliberately: the flex column reports a scrollWidth a few
     px past its client box, which raises a phantom horizontal scrollbar and
     steals 15 px of the rail's height. */
  overflow: hidden auto;
}
.rail hr { border: 0; border-top: 1px solid var(--line); margin: 6px 10px; width: calc(100% - 20px); }
.rspring { flex: 1 1 auto; }
.rbtn {
  position: relative; width: 100%; min-height: 46px; background: none; border: 0;
  /* --muted on the paper is ~2.7:1, which fails WCAG AA (4.5:1) for the 11px
     label — and this rail is the console's primary navigation, read at a glance
     across a room. --ink-2 is ~6.5:1 and re-themes with the bridge palettes,
     where the same relationship holds. */
  border-left: 2.5px solid transparent; cursor: pointer; color: var(--ink-2);
  font: inherit; font-family: var(--sans); padding: 8px 2px 7px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
}
.rbtn svg { width: 21px; height: 21px; stroke: currentColor; fill: none; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.rbtn span { font-size: 11px; line-height: 1.15; text-align: center; }
.rbtn:hover { color: var(--ink); background: var(--surface); }
.rbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.rbtn[aria-pressed="true"] { color: var(--accent); border-left-color: var(--accent); background: var(--surface); }
/* Status, not decoration: a badge always carries a title explaining it. */
/* Anchored beside the icon, not in the button's corner: at 46 px row height a
   corner dot sits nearer the row above and reads as that row's badge. */
.rbadge { position: absolute; top: 9px; right: 19px; width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid var(--surface-2); }
.rbadge.stop { background: var(--crit); }
.rbadge.warn { background: var(--warn); }

.wspanel {
  width: var(--wspanel-w, 300px); height: calc(100vh - 52px);
  border-right: 1px solid var(--line); background: var(--surface);
  display: flex; flex-direction: column; min-width: 0;
}
/* `display: flex` above would otherwise beat the UA's [hidden] rule. */
.wspanel[hidden] { display: none; }
.wshead {
  margin: 0; padding: 10px 14px 9px; border-bottom: 1px solid var(--line);
  background: var(--surface-2); color: var(--muted); font-weight: 500;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase;
}
.wsbody { overflow-y: auto; flex: 1 1 auto; font-size: 13px; color: var(--ink-2); }

.rsec { border-bottom: 1px solid var(--line); }
.rsec > summary {
  cursor: pointer; list-style: none; padding: 9px 14px; min-height: 30px;
  display: flex; align-items: center; gap: 7px; user-select: none;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
}
.rsec > summary::-webkit-details-marker { display: none; }
.rsec > summary::before { content: "▸"; font-size: 11px; }
.rsec[open] > summary::before { content: "▾"; }
.rsec > summary:hover { color: var(--ink); background: var(--surface-2); }
.rsec > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.rsbody { padding: 2px 14px 14px; }
.rsbody > p { margin: 0 0 0.7em; font-size: 12.5px; line-height: 1.55; }
.rsbody > p:last-child { margin-bottom: 0; }
/* Blocks moved out of the column drop their card chrome inside the panel: they
   already carry their own collapsible header, so wrapping them in a <details>
   section as well would give one panel two disclosure controls. The hairline
   between them does the separating instead. */
.wsbody > .block {
  background: none; border: 0; border-radius: 0;
  border-bottom: 1px solid var(--line); padding: 10px 14px 12px;
}
.wsbody > .block:last-child { border-bottom: 0; }
.wsbody > .block .bt { font-size: 12px; }
/* Narrower than the column these came from — let the controls wrap rather than
   overflow. */
.wsbody .fieldrow { flex-wrap: wrap; }
.wsbody .sel { max-width: 100%; }
.rsbody .block { background: none; border: 0; border-radius: 0; padding: 0; }

/* Narrow: the panel becomes a drawer over the map rather than squeezing it. */
@media (max-width: 1180px) {
  .appbody {
    grid-template-columns: var(--rail-w, 74px) minmax(280px, 1fr) 7px var(--col2-w, clamp(380px, 34vw, 560px));
    grid-template-rows: minmax(0, 1fr) auto;
    /* The conditions table needs its area in THIS template too, not only in the
       two above. This block sits later in the file than the ≤880px one and has
       equal specificity, so at any width from here down it is the template that
       actually wins — and a .ctable with no named area auto-places into an
       implicit zero-width column at the right edge. That is the same
       "sliver map" failure the note at the top of .appbody describes, and it is
       why every template in this file has to name every area. */
    grid-template-areas:
      "rail map    gutter col2"
      "rail ctable ctable ctable";
  }
  /* grid-area: auto — an absolutely-positioned grid child is positioned inside
     its named area, and "wspanel" no longer exists in this template; auto falls
     back to the .appbody padding box, which left/top address. */
  .wspanel { position: absolute; grid-area: auto; left: var(--rail-w, 74px); top: 0; z-index: 6; box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18); }
  /* The panel floats over the map here, so there is no seam to drag — an
     in-flow lgutter with no area would otherwise force an implicit track
     (this was the map-in-the-7px-track bug). */
  .lgutter { display: none; }
}
@media (max-width: 880px) {
  .rail { width: auto; height: auto; flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); }
  .rail hr { display: none; }
  .rspring { display: none; }
  .rbtn { width: auto; min-width: 62px; border-left: 0; border-bottom: 2.5px solid transparent; }
  .rbtn[aria-pressed="true"] { border-left-color: transparent; border-bottom-color: var(--accent); }
  /* Back in flow, so it needs its area back (the ≤1180px drawer rule above set
     grid-area: auto, and both queries apply at this width). */
  .wspanel { position: static; grid-area: wspanel; width: auto; height: auto; max-height: 46vh; border-right: 0; border-bottom: 1px solid var(--line); box-shadow: none; }
}

/* Hosted layer surface: the overlay control, moved off the map into the
   Observations workspace. It loses the floating card chrome — the panel is the
   card now — and gains room, so the sub-labels stop being cramped. */
.layerctl-hosted {
  position: static; max-width: none; border: 0; border-radius: 0;
  border-bottom: 1px solid var(--line); box-shadow: none; background: none; font-size: 12.5px;
}
.layerctl-hosted > summary {
  padding: 9px 14px; min-height: 30px; font-weight: 500;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
}
.layerctl-hosted > summary:hover { color: var(--ink); background: var(--surface-2); }
.layerctl-hosted .lc-body { padding: 4px 14px 12px; gap: 9px; }
/* The reach field's Wind/Waves toggles, hosted in that same list: they read as
   one more layer row rather than as the header of a separate control. */
.windctl-toggles-hosted { flex-wrap: wrap; gap: 6px 12px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.windctl-toggles-hosted .windctl-title { flex-basis: 100%; font-size: 10px; letter-spacing: 0.1em; color: var(--muted); }

/* Zoom-ladder segmented control in the Theater workspace. Three rungs only —
   the ladder floors where GFS-Wave stops resolving, so there is nothing below
   "Operational" to offer. */
.rungseg { display: flex; border: 1px solid var(--line-2); border-radius: var(--r); overflow: hidden; margin: 2px 0 10px; }
.rungseg-btn {
  flex: 1 1 0; min-height: 30px; padding: 5px 8px; cursor: pointer;
  background: transparent; color: var(--ink-2); border: 0; border-left: 1px solid var(--line);
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.03em;
}
.rungseg-btn:first-child { border-left: 0; }
.rungseg-btn.on { background: var(--navy); color: var(--surface); }
.rungseg-btn:hover:not(.on) { color: var(--ink); background: var(--surface-2); }
.rungseg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Mono key/value facts inside a workspace panel. */
.wsbody .kv { display: grid; grid-template-columns: auto 1fr; gap: 3px 10px; margin: 0 0 10px; font-family: var(--mono); font-size: 11.5px; }
.wsbody .kv dt { color: var(--muted); }
.wsbody .kv dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.wscheck { display: flex; align-items: center; gap: 7px; min-height: 30px; font-size: 12.5px; cursor: pointer; }
/* One unbreakable token (the theater cache key) — give it the whole row. */
.wsbody .kv dt.kvwide { grid-column: 1 / -1; margin-top: 4px; }
.wsbody .kv dd.kvwide { grid-column: 1 / -1; text-align: left; font-size: 11px; color: var(--ink-2); }

/* Confidence strip under the shared ruler. It sits in the same grid column as the
   plots and carries the charts' own gutters, so a band's left edge sits over the
   hour it describes. Colour is muted on purpose: this is a caption for the axis,
   not another data series competing with it. */
/* Taller than it needs to be for reading alone: together with the ruler above it
   this is one ~66px band you can grab without aiming, and both halves scrub. */
.trackstack .confrow { grid-template-rows: 28px; border-bottom: 1px solid var(--line); }
.confbar {
  grid-column: 2; display: flex; height: 28px; align-items: stretch; font-family: var(--mono);
  font-size: 11px; letter-spacing: 0.04em; cursor: ew-resize; touch-action: none; user-select: none; }
.confbar .cb {
  display: flex; align-items: center; justify-content: center; min-width: 0;
  overflow: hidden; white-space: nowrap; color: var(--surface);
  border-right: 1px solid var(--surface);
}
.confbar .cb:last-child { border-right: 0; }
.confbar .c0 { background: color-mix(in srgb, var(--obs) 55%, transparent); }
.confbar .c1 { background: color-mix(in srgb, var(--good) 78%, transparent); }
.confbar .c2 { background: color-mix(in srgb, var(--warn) 78%, transparent); }
/* Hatched, not a third solid colour: past 120 h the steps themselves are
   3-hourly, so the bar should read as "coarser data", not "worse weather". */
.confbar .c3 {
  color: var(--ink-2);
  background: repeating-linear-gradient(45deg, var(--line-2), var(--line-2) 4px, transparent 4px, transparent 8px);
}

/* Transport — the one place time is driven from. Heads the track stack, above
   the ruler, because it belongs to the whole timeline rather than to any track. */
.transport {
  display: flex; align-items: center; gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--line); border-bottom: 0;
  border-radius: var(--r) var(--r) 0 0;
  background: var(--surface-2);
}
.tp-btn {
  min-height: 26px; min-width: 28px; padding: 2px 8px; cursor: pointer;
  background: var(--surface); color: var(--ink-2);
  border: 1px solid var(--line-2); border-radius: var(--r);
  font-family: var(--mono); font-size: 11px; line-height: 1.2;
}
.tp-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--accent); }
.tp-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tp-btn:disabled { opacity: 0.4; cursor: default; }
.tp-btn.on { background: var(--navy); color: var(--surface); border-color: var(--navy); }
.tp-clear { margin-left: 0; }
.tp-time {
  flex: 1 1 auto; text-align: center;
  font-family: var(--mono); font-size: 11.5px; font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.tp-time.idle { color: var(--muted); }
/* The stack's first child now carries the rounded top, not the ruler. */
.trackstack .rulerrow { border-radius: 0; }
/* The field's valid-time line, when the field is muted for lead time. */
.windctl-time.windctl-faded { color: var(--muted); }
/* The field has run past the end of its grid — what is painted is its last step. */
.windctl-time.windctl-stale { color: var(--warn); }

/* Saved operating areas in the Theater workspace. */
.thsave { display: flex; gap: 6px; margin: 10px 0 8px; }
.thname {
  flex: 1 1 auto; min-width: 0; min-height: 30px; padding: 4px 8px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r);
  font-family: var(--mono); font-size: 11.5px;
}
.thname::placeholder { color: var(--muted); }
.thlist { display: flex; flex-direction: column; }
.throw { display: flex; align-items: stretch; gap: 4px; border-top: 1px solid var(--line); }
.throw:last-child { border-bottom: 1px solid var(--line); }
.throw.on { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent); }
.thopen {
  flex: 1 1 auto; min-width: 0; min-height: 30px; padding: 5px 8px; cursor: pointer;
  background: none; border: 0; text-align: left; color: inherit; font: inherit;
  display: flex; flex-direction: column; gap: 1px;
}
.thopen:hover { background: var(--surface-2); }
.thopen:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.thnm { font-size: 12.5px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thsub { font-family: var(--mono); font-size: 9.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thdel {
  flex: 0 0 auto; width: 30px; cursor: pointer; background: none; border: 0;
  color: var(--muted); font-size: 15px; line-height: 1;
}
.thdel:hover { color: var(--crit); }
.thdel:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* --- Feedback workspace ---------------------------------------------------
   The report box is a working surface, not a form field to be tabbed past, so
   it gets real height and the app's own monospace-free reading type. */
.fbbox {
  width: 100%;
  font: inherit;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  padding: 10px 11px;
  resize: vertical;
  min-height: 96px;
}
.fbbox:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.fbactions { display: flex; align-items: center; gap: 12px; margin-top: 10px; }
.fbstatus { font-size: 13px; color: var(--ink-2); }

.fblist { display: flex; flex-direction: column; gap: 10px; }
.fbrow { border-left: 2px solid var(--line-2); padding: 2px 0 4px 10px; }
.fbhead { display: flex; align-items: baseline; gap: 10px; }
.fbwhen { font-family: var(--mono); font-size: 12px; color: var(--ink-2); }
.fbwho { font-family: var(--mono); font-size: 12px; color: var(--muted); flex: 1; }
.fbdel { font-size: 12px; padding: 2px 8px; }
.fbmsg { margin: 4px 0 0; font-size: 14px; line-height: 1.5; color: var(--ink); white-space: pre-wrap; }
/* Provenance, not prose — the same monospace the source lines use, because it is
   machine-recorded fact rather than something a person wrote. */
.fbctx { margin: 4px 0 0; font-family: var(--mono); font-size: 11.5px; color: var(--muted); line-height: 1.5; }

/* --- Report page (pages/report.ts) ---------------------------------------
   A printable metocean brief. On screen the document sits on the page like a
   sheet of paper; in print the app chrome and the form disappear and the sheet
   IS the page. Editorial discipline as everywhere: navy ink on ivory, colour
   only in the data and the status chips, serif for the document voice, mono
   for machine-recorded fact. */
.reportpage { max-width: 58rem; }
/* The two doors into the form. The note beside them says which one filled it,
   so a console snapshot is never mistaken for defaults. */
.rp-entry { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 2px 0 12px; border-bottom: 1px solid var(--line); margin-bottom: 10px; }
.rp-entry .btn { padding: 6px 14px; font-size: 13px; }
.rp-modenote { font-family: var(--mono); font-size: 11px; color: var(--muted); letter-spacing: 0.02em; }
.report-form .rp-formgrid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px 18px; align-items: end; padding: 4px 0 2px;
}
.report-form label.fld select { width: 100%; }
.rp-actions { display: flex; align-items: center; gap: 12px; margin-top: 14px; }
.rp-status { font-size: 13px; color: var(--ink-2); }
.rp-status.err { color: var(--crit); }

.report-doc {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  padding: 34px 42px 30px; line-height: 1.55;
}
.rp-brandrow { display: flex; align-items: center; gap: 14px; padding-bottom: 16px; border-bottom: 2px solid var(--navy); }
.rp-mark { display: inline-flex; color: var(--navy); flex: none; }
.rp-word { font-size: 19px; font-weight: 300; letter-spacing: 0.28em; color: var(--navy); }
.rp-doctype { font-family: var(--serif); font-size: 15px; color: var(--ink-2); margin-top: 2px; }
.rp-refbox { margin-left: auto; text-align: right; font-family: var(--mono); font-size: 11px; color: var(--ink-2); line-height: 1.7; }
.rp-refbox .rp-ref { font-size: 13px; font-weight: 600; color: var(--ink); letter-spacing: 0.04em; }

.rp-partics {
  display: grid; grid-template-columns: auto 1fr; gap: 4px 18px;
  margin: 16px 0 0; font-size: 13px;
}
.rp-partics dt { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); align-self: baseline; padding-top: 2px; }
.rp-partics dd { margin: 0; color: var(--ink); font-variant-numeric: tabular-nums; }

.rp-h3 {
  font-family: var(--serif); font-size: 16px; font-weight: 400; color: var(--ink);
  margin: 26px 0 8px; padding-bottom: 5px; border-bottom: 1px solid var(--line);
}
.rp-verdict { margin-top: 22px; }
.rp-verdictrow { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
/* The verdict word rides in its band colour, but the colour never carries the
   meaning alone: the label is the word itself, and the sentence beside it says
   the number — grayscale photocopies keep the whole statement. */
.rp-verdictchip {
  display: inline-block; color: #fff; font-size: 15px; font-weight: 700;
  letter-spacing: 0.04em; padding: 3px 14px; border-radius: 2px;
}
.rp-verdictchip.wk-good { background: var(--good); }
.rp-verdictchip.wk-warn { background: var(--warn); }
.rp-verdictchip.wk-serious { background: var(--serious); }
.rp-verdictchip.wk-crit { background: var(--crit); }
.rp-verdictline { font-size: 14px; color: var(--ink); }
.rp-verdictnote { font-size: 12.5px; color: var(--ink-2); margin: 8px 0 0; }
.rp-limits { font-family: var(--mono); font-size: 12px; color: var(--ink-2); margin: 12px 0 0; }
/* Seeded-default limits are a safety fact — a bordered notice, not a footnote. */
.rp-provisional {
  border: 1.5px solid var(--warn); border-left-width: 5px; border-radius: 0 3px 3px 0;
  background: color-mix(in srgb, var(--warn) 7%, transparent);
  padding: 9px 12px; margin: 10px 0 0; font-size: 13px; color: var(--ink);
}
.rp-verdict .windows { margin-top: 4px; }
.rp-verdict .chip { font-size: 11px; }

.rp-charts { margin-top: 10px; }
.rp-chartblock { margin-top: 14px; }
.rp-chartwrap { width: 100%; height: 210px; }
.rp-chartwrap canvas { width: 100%; height: 100%; display: block; }

.rp-briefsec .brief { margin-top: 4px; }
.rp-formal { font-size: 13.5px; color: var(--ink-2); margin: 8px 0 0; }
.rp-formal.quiet { color: var(--muted); font-size: 12px; }

.rp-foot { margin-top: 26px; padding-top: 12px; border-top: 1px solid var(--line-2); }
.rp-sources { font-family: var(--mono); font-size: 10.5px; color: var(--muted); line-height: 1.6; margin: 0; }
.rp-notnav { font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em; color: var(--ink-2); margin: 10px 0 0; }
/* The per-page running footer exists only on paper. */
.rp-runfoot { display: none; }

/* --- print: browser print → a clean paginated PDF ------------------------ */
@media print {
  @page { size: A4; margin: 13mm 13mm 18mm; }
  html, body { background: #fff; }
  /* The app shell scrolls its panes; a scroll container clips everything past
     its first page height when printed, so every ancestor of the document must
     become plain flow. Scoped to the report page — other routes print as they
     always did (screen chrome minus the header). */
  #app { display: block; min-height: 0; }
  .page.reportpage { overflow: visible; height: auto; max-width: none; padding: 0; display: block; }
  .apphead, .report-form, .rp-actions { display: none !important; }
  .report-doc { border: 0; border-radius: 0; padding: 0 0 26px; }
  /* Keep the ink-on-ivory data colours — the verdict band, the status chips,
     the night shading in the charts — instead of the browser's blank default. */
  .report-doc, .report-doc * { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  /* Blocks break between, never through: a chart split across a page boundary
     is two half-charts, and half a verdict is worse. */
  .rp-head, .rp-verdict, .rp-chartblock, .rp-provisional, .rp-foot { break-inside: avoid; }
  .rp-briefsec p { break-inside: avoid; }
  .rp-h3 { break-after: avoid; }
  .rp-chartblock { border-left: 0; border-right: 0; }
  /* position:fixed repeats on every printed page in the engines that matter —
     the reference, the client and the disclaimer ride along the bottom. */
  .rp-runfoot {
    display: block; position: fixed; bottom: 0; left: 0; right: 0;
    font-family: var(--mono); font-size: 9px; letter-spacing: 0.04em;
    color: var(--muted); text-align: center;
    border-top: 1px solid var(--line); padding-top: 3px; background: #fff;
  }
}
