/* ============================================================================
   HawaiiStorm — design system, shell and layout.  Owned by: shell.
   ------------------------------------------------------------------------
   Island-first redesign (REDESIGN-SPEC.md, CONTRACTS.md §6-§7). This file owns:

     tokens      colour, type, density, radii, motion — :root, light theme,
                 :root[data-density="compact"], phone breakpoint
     primitives  the shared hw-* vocabulary every module builds tiles from
                 (hw-tile*, hw-well, hw-pill, hw-seg, hw-btn*, hw-dot--*,
                 hw-glyph--*, hw-num, hw-meta…, hw-chipbtn, hw-link, hw-sr-only)
     shell       header, footer, noscript, boot error, skip link
     layout      main.hw-layout and every mount's placement, per mode and width

   Plus the still-live component rules from the previous wave that other
   modules keep emitting (hw-map__*, hw-imagery*, hw-kv, hw-table,
   hw-src*, hw-badge*, hw-chip*, hw-age*, hw-disclosure*, hw-metric*, hw-spark*,
   hw-panel for recon.html) and the bare classes recon.html still uses
   (.app-header*, .app-footer, .btn, .chip, .status-dot). Each feature module
   styles its OWN root in its own stylesheet; none of them restate tokens.

   Contrast numbers beside tokens are measured (WCAG 2, sRGB), not estimated.
   Any new text/background pairing must be measured the same way — a pair of
   individually-measured tokens is not a measured pair.

   Contents
     00  Fonts (Manrope, self-hosted)
     01  Tokens (dark :root / light) + 01b redesign tokens and density
     02  Reset + base
     03  Utilities + legacy shell chrome (recon.html header, buttons, chips)
     04  Shell, primitives and layout (the island-first redesign)
     05  hw-panel (recon.html + legacy)
     06  hw-badge / hw-chip — the severity + status system
     07  hw-disclosure — progressive disclosure
     08  hw-kv / hw-raw / hw-table / hw-metric / hw-note / hw-empty
     10  hw-alert* — ranked NWS alerts
     11  hw-island* — per-island conditions
     12  hw-model* / hw-envelope / hw-spark — model comparison + charts
     13  hw-surge / hw-waves / hw-tides — marine
     14  hw-source* — provenance
     16  hw-imagery — satellite loop player
     17  hw-map* — Leaflet container, legend, popups
     18  Touch and anchors
     19  Reduced motion
     20  Print

   THE CHARACTERISTIC BUG OF THIS STYLESHEET: a rule that is present, readable,
   correct-looking, and INERT. It does not throw, does not warn, does not fail a
   test, and does not look wrong in the source. Three instances in one wave:

     - `max-height: 100%` on a child of an auto-height grid item resolves to
       `none` against an indefinite containing block.
     - a HIERARCHY rule at (0,4,0) silently outranked a STATE rule at (0,2,0)
       and deleted the stale-feed wash from the panels likeliest to go stale.
     - a touch `min-height: 44px` lost to a base rule that had just been
       retargeted to a more precise selector.

   What catches this class of bug is measuring the rendered page — computed
   values, real geometry, composited colour — not reading the rule. State rules
   (what is wrong with a thing) outrank emphasis rules (where it sits). Always.

   The redesign adds one more trap to that list, stated here so it is not
   re-learned: the mount defaults in §04 are written with :where() so they carry
   ZERO specificity, on purpose, so an owner may restyle its own mount by id
   (CONTRACTS §6 rule 4) without an arms race. The two rules that must NOT lose
   — mode visibility and empty-mount removal — are deliberately heavier.
   ========================================================================= */

/* ============================================================================
   00  FONTS
   ------------------------------------------------------------------------
   Manrope, variable (wght 500-800), vendored under fonts/ (OFL — fonts/OFL.txt)
   because the CSP is `font-src 'self'` and Google Fonts is a third-party
   request the reader never agreed to. Split latin / latin-ext with Google's own
   unicode-ranges, so a page of plain English downloads one file — the one
   index.html preloads. `swap`: the system face is shown until Manrope lands;
   an invisible verdict during a hurricane warning is not an acceptable FOIT.
   Glyphs Manrope lacks (◆ ▲ ● ○ ◌ ⌃ ⌄ ◉ ✈) fall through to --font-ui, which is
   why every --font-display stack ends in it.
   ========================================================================= */

@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url("../fonts/manrope-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url("../fonts/manrope-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================================
   01  TOKENS
   ========================================================================= */

:root {
  color-scheme: dark;

  /* -- surfaces, ascending elevation -- */
  --bg: #0a0e14;
  --surface: #111823;
  --surface-2: #18212e;
  --surface-3: #202b3a;
  --surface-sunken: #070a0f;

  /* -- lines -- */
  --border: #263243;          /* decorative separators */
  --border-strong: #3a4a60;   /* panel edges, emphasis */
  --border-control: #66798f;  /* interactive edges — 3.98:1 on --surface */

  /* -- ink -- */
  --text: #e9eef5;            /* 15.28:1 on --surface */
  --text-dim: #aab8c9;        /*  8.83:1 */
  --text-faint: #8fa0b5;      /*  6.67:1 */
  --text-on-accent: #06101f;

  /* -- interactive. A periwinkle blue on a hue used by NO severity or category
        token, so "you can click this" can never read as "this is dangerous". */
  --accent: #7ea6ff;          /*  7.45:1 on --surface */
  --accent-soft: color-mix(in srgb, var(--accent) 16%, var(--surface));
  --accent-line: color-mix(in srgb, var(--accent) 45%, var(--surface));

  /* -- semantic status -- */
  --ok: #5ed9a0;
  --warn: #ffc75a;
  --err: #ff5470;
  --idle: #8fa0b5;

  /* -- severity ramp (CAP: Extreme > Severe > Moderate > Minor > Unknown).
        NEVER used alone: every severity treatment also carries a rail width,
        a glyph shape supplied by panels.js, and a written label. -- */
  --sev-extreme: #ff5470;
  --sev-severe: #ff9142;
  --sev-moderate: #ffc75a;
  --sev-minor: #4fd8c0;
  --sev-unknown: #9aa9bc;
  /* Wash strength behind a severity card. THIS IS A CONTRAST KNOB, not just a
     decorative one: the wash is a third colour, so every measured ratio in this
     section — all of which are against --surface — stops applying the moment a
     card is tinted. Swept every tinted surface in the file for --text-faint,
     which is the thinnest ink that lands on one (light theme is the binding
     case throughout; dark clears 4.65 everywhere):

       hero metric tile (severe 10% on --surface-2)   4.41  FAILED -> §09
       panel--stale head (warn 8% on --surface-2)     4.55  thin   -> §05
       alert --sev-extreme / hero warning band        4.69
       models verdict wide (err 10%)                  4.63
       models verdict moderate (warn 10%)             4.79
       alert --sev-severe / --sev-minor / --moderate  4.84-4.87
       sources trust degraded (warn 10%)              4.79

     The two marked above now carry --text-dim instead. The rest pass on
     --text-faint and are left alone. Raising --sev-tint pushes ALL of these
     down together, so if it ever moves, re-run this sweep — several of them
     have well under 0.4 of headroom.

     Surfaces whose ink is a severity hue rather than --text-faint were checked
     separately and are not affected: the engaged reduce-motion toggle is --warn
     on a warn-16% wash at 5.35 light / 8.03 dark, a pressed control is --accent
     on --accent-soft at 5.66 / 5.62, and a panel error message is --err on an
     err-10% wash at 5.34 / 5.14. */
  --sev-tint: 14%;

  /* -- product kind (warning > watch > advisory > statement) -- */
  --kind-warning: var(--sev-extreme);
  --kind-watch: var(--sev-severe);
  --kind-advisory: var(--sev-moderate);
  --kind-info: var(--idle);

  /* -- ink painted OVER imagery: the satellite loop's timestamp, the map's
        floating labels. Deliberately IDENTICAL in both themes and deliberately
        not derived from --text / --surface, because what is behind them is a
        photograph or a raster tile whose luminance is arbitrary and changes
        frame to frame. A theme-flipping stamp is dark ink on a dark cloud half
        the time.

        Written as rgb(... / a), not color-mix(#000 62%, transparent), because
        color-mix computes to `color(srgb 0 0 0 / 0.62)` — and an rgb()/rgba()
        parser reads that as NO background at all. tools/audit-ux.mjs composites
        backgrounds with exactly such a parser and reported this pair as 1.22:1
        white-on-page when it actually paints 6.19:1. Same pixels either way;
        the notation is what makes the measurement checkable.

        Measured, white on the scrim over the worst case (pure white cloud top)
        and over what is behind it while the loop is still loading:
          0.62 (was) — 6.19 cloud / 7.05 light stage / 20.51 dark map well
          0.72 (now) — 9.29 cloud / 10.25 light stage / 20.64 dark map well
        0.62 already cleared AA; 0.72 buys headroom over the brightest frames
        for four percent more scrim. -- */
  --overlay-ink: #ffffff;
  --overlay-scrim: rgb(0 0 0 / 0.72);
  --overlay-halo: rgb(0 0 0 / 0.92);

  /* -- map ground truth. Land must read as quiet substrate: a step above the
        sunken map well so the islands are legible as land, but far below every
        storm and severity hue so it never competes with the cone, the track or
        a warning polygon. map.js can draw an explicit coastline layer with
        these instead of hunting for a basemap variant.
        Measured land:water separation 1.43 dark / 1.29 light — visible as
        shape, never as emphasis. Cat 1 fill reads 11.14:1 on dark land, so the
        storm layers stay dominant by a wide margin. -- */
  --map-water: #091015;
  --map-land: #213043;
  --map-land-edge: #465972;

  /* -- threat level, computed per island in data.js -- */
  --threat-none: var(--idle);
  --threat-watch: var(--sev-moderate);
  --threat-warning: var(--sev-severe);
  --threat-direct: var(--sev-extreme);

  /* -- Saffir-Simpson / classification. FILL-ONLY: this is the standard NHC
        ramp and is far too light to be text on a white page. Always paint it
        as a background under --cat-ink, or as a map stroke. -- */
  --cat-ink: #08111c;
  --cat-td: #5ebaff;
  --cat-ts: #00faf4;
  --cat-ptc: #9db4cc;
  --cat-1: #ffe9a8;
  --cat-2: #ffc94d;
  --cat-3: #ff9b2f;
  --cat-4: #ff6a2a;
  --cat-5: #ff3d5e;

  /* -- six forecast agencies. Exposed under two names because two modules read
        them differently: panels.js strokes its envelope lines with
        var(--m-<agencykey>), motion/map read the positional --hw-model-N. -- */
  --m-ecmwf: #7ea6ff;
  --m-gfs: #5ed9a0;
  --m-icon: #ffc75a;
  --m-jma: #ff8fb0;
  --m-ukmo: #c39cff;
  --m-mf: #4fd8c0;
  --hw-model-1: var(--m-ecmwf);
  --hw-model-2: var(--m-gfs);
  --hw-model-3: var(--m-icon);
  --hw-model-4: var(--m-jma);
  --hw-model-5: var(--m-ukmo);
  --hw-model-6: var(--m-mf);

  /* -- type -- */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

  --fs-3xs: 0.625rem;   /* 10 — micro labels */
  --fs-2xs: 0.6875rem;  /* 11 — mono meta */
  --fs-xs: 0.75rem;     /* 12 */
  --fs-sm: 0.8125rem;   /* 13 */
  --fs-base: 0.9375rem; /* 15 — body */
  --fs-md: 1.0625rem;   /* 17 */
  --fs-lg: clamp(1.1875rem, 0.9rem + 1vw, 1.375rem);
  --fs-xl: clamp(1.5rem, 1.1rem + 1.6vw, 1.875rem);
  --fs-2xl: clamp(1.875rem, 1.35rem + 2.4vw, 2.625rem);
  --fs-hero: clamp(2.5rem, 1.5rem + 4.6vw, 4rem);

  --lh-tight: 1.12;
  --lh-snug: 1.3;
  --lh-body: 1.55;

  /* Prose measure. Three different caps were in use before — 44ch, 62ch and
     78ch — chosen per component rather than per role, so two paragraphs of the
     same kind of sentence wrapped at different widths on the same screen.
     --measure is running prose a reader is expected to read; --measure-tight is
     a short supporting line that should stay one or two lines deep. Dense mono
     readouts are NOT capped: their line length is the data's, not ours. */
  --measure: 66ch;
  --measure-tight: 46ch;
  --tracking-caps: 0.09em;
  --tracking-tight: -0.015em;

  /* -- space (4px base) -- */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.25rem;
  --s-6: 1.5rem;
  --s-8: 2rem;
  --s-10: 2.5rem;
  --s-12: 3rem;
  --gutter: clamp(0.875rem, 0.4rem + 2vw, 2rem);
  --page-max: 1760px;

  /* -- radii + elevation -- */
  --r-xs: 4px;
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 14px;
  --r-pill: 999px;
  --shadow-1: 0 1px 2px rgb(0 0 0 / 0.5);
  --shadow-2: 0 2px 8px rgb(0 0 0 / 0.45), 0 1px 2px rgb(0 0 0 / 0.4);
  --shadow-3: 0 12px 32px -8px rgb(0 0 0 / 0.65), 0 2px 6px rgb(0 0 0 / 0.45);

  /* -- motion. Every duration routes through --motion-scale, so the global
        motion control and prefers-reduced-motion damp the whole system from
        one place. -- */
  --motion-scale: 1;
  --dur-1: calc(100ms * var(--motion-scale));
  --dur-2: calc(160ms * var(--motion-scale));
  --dur-3: calc(240ms * var(--motion-scale));
  --dur-4: calc(400ms * var(--motion-scale));
  --ease-out: cubic-bezier(0.2, 0.7, 0.3, 1);
  --ease-in-out: cubic-bezier(0.5, 0, 0.2, 1);
  --ease-emphatic: cubic-bezier(0.2, 1.06, 0.3, 1);

  /* The map is the centrepiece of a storm-tracking app; it was reading as
     smaller than the satellite thumbnail. Sized by viewport height so it stays
     proportionate rather than fixed. Kept as a STATIC height on purpose:
     map.js calls invalidateSize() once at init and has no ResizeObserver, so a
     container that grows after layout would leave Leaflet with a stale size. */
  --map-h: clamp(420px, 66vh, 760px);
}

/* Light palette — hues re-derived, not merely lightened, so severity ink and
   body text keep AA contrast on white. */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: #eef2f7;
  --surface: #ffffff;
  --surface-2: #f2f6fa;
  --surface-3: #e6edf5;
  --surface-sunken: #e2e9f1;

  --border: #cfdae6;
  --border-strong: #9fb2c7;
  --border-control: #78899e;   /* 3.31:1 on --surface */

  --text: #0d1826;             /* 17.86:1 on --surface */
  --text-dim: #46586d;         /*  7.30:1 */
  --text-faint: #586a80;       /*  5.54:1 */
  --text-on-accent: #ffffff;

  --accent: #2a54c8;           /*  6.56:1 */
  --accent-soft: color-mix(in srgb, var(--accent) 10%, var(--surface));
  --accent-line: color-mix(in srgb, var(--accent) 40%, var(--surface));

  --ok: #0a7a4a;
  --warn: #7a5400;
  --err: #c00030;
  --idle: #55677c;

  --sev-extreme: #c00030;      /* 6.39:1 */
  --sev-severe: #a04008;       /* 6.50:1 */
  --sev-moderate: #7a5400;     /* 6.78:1 */
  --sev-minor: #06695c;        /* 6.60:1 */
  --sev-unknown: #55677c;      /* 5.81:1 */
  --sev-tint: 9%;

  --map-water: #cbdcec;
  --map-land: #f8f5ed;
  --map-land-edge: #9aa9b9;

  /* Category fills are unchanged — a domain-standard ramp, always under
     --cat-ink, which is dark in both themes. */

  --m-ecmwf: #2a54c8;
  --m-gfs: #0a7a4a;
  --m-icon: #7a5400;
  --m-jma: #b0295e;
  --m-ukmo: #6b3fbf;
  --m-mf: #06695c;

  --shadow-1: 0 1px 2px rgb(13 24 38 / 0.08);
  --shadow-2: 0 2px 8px rgb(13 24 38 / 0.09), 0 1px 2px rgb(13 24 38 / 0.06);
  --shadow-3: 0 12px 32px -8px rgb(13 24 38 / 0.2), 0 2px 6px rgb(13 24 38 / 0.08);
}

/* -- 01b  REDESIGN TOKENS + DENSITY -------------------------------------- *
 * REDESIGN-SPEC §2.1-§2.3, CONTRACTS §6. The tile vocabulary is DERIVED from
 * the surfaces above rather than restated, so the light theme only has to move
 * --frame: --tile/--tile-2/--tile-track follow --surface/-2/-3, which already
 * equal the spec's light values (#fff / #f2f6fa / #e6edf5).
 *
 * Measured, every NEW text-on-background pair this layer introduces
 * (dark / light):
 *   --text       on --frame       17.29 / 14.90
 *   --text-dim   on --frame        9.99 /  6.09
 *   --text-faint on --frame        7.55 /  4.62   <- tightest: footer meta, light
 *   --accent     on --frame        8.43 /  5.47   footer links
 *   --text-faint on --tile-2       6.07 /  5.10   seg ink, pill, well labels, chevron
 *   --text-dim   on --tile-2       8.03 /  6.72   pill, status chip
 *   --accent     on --tile-2       6.78 /  6.04
 *   --warn on warn-14% / --tile-2  7.61 /  5.08   .hw-pill--warn
 *   --sev-* glyphs on --tile-2    ≥5.21 / ≥5.89   (--ok 9.19 / 4.96)
 *   --ink-on-sev on sev + ok      ≥6.10 / ≥5.39   primary button, see below
 *   --text-on-accent on --accent   7.97 /  6.56   secondary button
 * Non-text (3:1): every hw-dot colour on --tile ≥5.73 dark / ≥5.39 light.
 * Tile against frame is 1.13 / 1.20 — the tile edge is NOT a meaningful
 * boundary on its own (shadow + spacing carry it; forced-colors gets an outline
 * in §04), so never let a tile edge be the only thing separating two controls.
 */
:root {
  --font-display: "Manrope", var(--font-ui);

  --frame: #05070b;              /* page canvas behind tiles */
  --tile: var(--surface);
  --tile-2: var(--surface-2);    /* inner wells, segmented tracks, toggles */
  --tile-track: var(--surface-3);/* gauge tracks */
  --r-tile: 20px;
  --r-tile-compact: 12px;
  --r-inner: 12px;
  --shadow-tile: var(--shadow-2);

  /* Ink for text ON a severity fill (the primary CTA). The spec's literal
     rule — --cat-ink on yellow/orange, #fff on red/blue — fails in the dark
     theme, whose severity hues are light: #fff on --sev-extreme is 3.11:1 and
     on --accent 2.39:1. Measured instead, per theme:
       dark  --cat-ink on extreme 6.10 · severe 8.47 · moderate 12.25 · ok 10.75
       light #fff      on extreme 6.39 · severe 6.50 · moderate  6.78 · ok  5.39 */
  --ink-on-sev: var(--cat-ink);

  /* Density-aware. Comfy is the default and the only density Simple uses. */
  --tile-r: var(--r-tile);
  --tile-pad: 18px;
  --tile-gap: 12px;
  --well-pad: 12px;
  --fs-display: 1.75rem;   /* 28 — verdict */
  --fs-title: 0.9375rem;   /* 15 — tile title */
  --fs-num: 2.125rem;      /* 34 — big number */
  --fs-body: 0.875rem;     /* 14 */
  --fs-meta: 0.75rem;      /* 12 */
  --fs-micro: 0.6875rem;   /* 11 */
  --fs-mono: 0.71875rem;   /* 11.5 — tables, coords, timestamps */

  --page-gutter: clamp(12px, 1.4vw, 20px);
  /* --header-h is measured by js/boot.js and already INCLUDES the header's
     bottom padding (= --tile-gap), so nothing is added to it. Adding the gap
     again put the sticky map 12px below the top of the column beside it. */
  --sticky-top: var(--header-h, 84px);
}
:root[data-theme="light"] {
  --frame: #e6ebf1;
  --ink-on-sev: #ffffff;
}
:root[data-density="compact"] {
  --tile-r: var(--r-tile-compact);
  --tile-pad: 14px;
  --tile-gap: 10px;
  --well-pad: 9px;
  --fs-display: 1.5rem;
  --fs-title: 0.875rem;    /* 14 */
  --fs-num: 1.5rem;        /* 24 */
  --fs-body: 0.8125rem;    /* 13 */
}
@media (max-width: 767px) {
  :root {
    --fs-display: 1.5rem;  /* 24 on phone */
    --tile-pad: 16px;
    --page-gutter: 12px;
    /* The header is static on a phone; only switcher's chip rail sticks. */
    --sticky-top: 64px;
    /* The map is one tile among many on a phone (screen 3c), not the page:
       ~46% of an 844px viewport. Restores the phone cap the retired §18 used
       to carry; without it the map fell back to 66vh (557px at 390). */
    --map-h: clamp(300px, 46vh, 420px);
  }
  :root[data-density="compact"] { --tile-pad: 12px; }
}

/* ============================================================================
   02  RESET + BASE
   ========================================================================= */

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

:root { interpolate-size: allow-keywords; }  /* lets <details> animate to auto */

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  /* The frame, not --bg: tiles float on it (§2.2). recon.html shares this
     rule and reads the same. */
  background: var(--frame);
  color: var(--text);
  /* Manrope for body text too (spec §2.1); --font-ui is its fallback. */
  font-family: var(--font-display);
  font-size: var(--fs-body);
  font-weight: 500;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background-color var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out);
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: var(--tracking-tight);
}
p { margin: 0 0 var(--s-3); }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-underline-offset: 0.18em; }
a:hover { text-decoration-color: currentColor; }

button, input, select, textarea { font: inherit; color: inherit; }
img, svg, canvas { max-width: 100%; }
code, pre, kbd { font-family: var(--font-mono); }

/* One focus treatment for the whole app. --accent clears 3:1 on every surface
   in both themes. */
:where(a, button, summary, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }

/* ============================================================================
   03  UTILITIES + SHELL CHROME
   ========================================================================= */

.sr-only, .hw-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.hidden { display: none !important; }
.label {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
  font-weight: 500;
  line-height: 1.4;
}

.skip-link {
  position: absolute;
  top: var(--s-2);
  left: var(--s-2);
  z-index: 1000;
  padding: var(--s-2) var(--s-4);
  background: var(--accent);
  color: var(--text-on-accent);
  border-radius: var(--r-sm);
  font-weight: 600;
  font-size: var(--fs-sm);
  text-decoration: none;
  transform: translateY(-250%);
  transition: transform var(--dur-2) var(--ease-out);
}
.skip-link:focus-visible { transform: translateY(0); }

/* -- 03.1 recon.html header (legacy) ------------------------------------ *
 * NOT the dashboard header — that is .hw-header in §04.8. recon.html (owned
 * by nobody this wave) still renders .app-header*, .app-footer, .btn, .chip
 * and .status-dot, and css/recon.css does not define them, so these stay until
 * recon.html moves to the hw- vocabulary. Deleting them unstyles that page. */

.app-header {
  position: sticky;
  top: 0;
  z-index: 60;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(12px);
  -webkit-backdrop-filter: saturate(1.4) blur(12px);
  border-bottom: 1px solid var(--border);
}
.app-header__inner {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: var(--s-3) var(--gutter);
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex-wrap: wrap;
}
.app-header__brand {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  margin-right: auto;
  min-width: 0;
}
.app-header__mark { font-size: var(--fs-md); color: var(--accent); line-height: 1; }
.app-header__title {
  font-size: var(--fs-md);
  font-weight: 650;
  letter-spacing: var(--tracking-tight);
  white-space: nowrap;
}
.app-header__sub {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  white-space: nowrap;
}
.app-header__status {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
}
.app-header__clock {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  line-height: 1.25;
}
.app-header__clock time {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.app-header__controls { display: flex; align-items: center; gap: var(--s-2); }

/* The recon link sheds its word before the header sheds its single line.
   `.app-header__controls` is deliberately nowrap — the rule below it exists so
   a sticky header cannot eat a sixth of a phone screen during an emergency —
   so a new control has to earn its width rather than push the row past the
   viewport. The aria-label on the anchor carries the full name at every width,
   so losing the text costs nothing to a screen reader. */
.app-header__cta { gap: 0.35em; }
@media (max-width: 860px) {
  .app-header__cta-label { display: none; }
}

@media (max-width: 780px) {
  .app-header__inner { padding: var(--s-2) var(--gutter); gap: var(--s-2) var(--s-3); }
  .app-header__sub { display: none; }
  .app-header__status { width: 100%; order: 3; justify-content: space-between; }
}

/* A sticky header must not eat a sixth of a phone screen during an emergency.
   The global age is the one droppable item — every panel carries its own.

   The controls were 30px tall here, which saved ~14px of header and cost every
   one of them a legal touch target. Measured at 390x844: three wrapped rows,
   102px with 30px buttons, 110px with 44px ones. 8px of a 844px viewport is
   not worth a refresh button you have to aim at, so the buttons win and the
   height comes back out of the padding and the row gaps instead. */
@media (max-width: 480px) {
  .app-header__inner { padding-block: var(--s-1); gap: var(--s-1) var(--s-2); }
  .app-header__status { gap: var(--s-3); }
  #feed-age { display: none; }
  .app-header__clock { flex-direction: row; align-items: baseline; gap: var(--s-2); }
  .app-header__controls .btn { min-height: 44px; padding-inline: var(--s-3); }
}

/* -- 03.2 buttons, chips, status (shell) --------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  min-height: 32px;
  background: var(--surface-2);
  color: var(--text-dim);
  border: 1px solid var(--border-control);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.btn:hover { background: var(--surface-3); color: var(--text); border-color: var(--accent); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn--icon { padding: var(--s-2); min-width: 32px; font-size: var(--fs-sm); }

/* Shell-only chip (header feed status). Modules use .hw-chip. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border-control);
  background: var(--surface-2);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  line-height: 1.7;
  white-space: nowrap;
}

.status-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  flex: none;
  background: var(--idle);
}
.status-dot--ok { background: var(--ok); }
.status-dot--loading { background: var(--accent); animation: hw-pulse 1.6s var(--ease-in-out) infinite; }
.status-dot--stale { background: var(--warn); }
.status-dot--error { background: var(--err); }
@keyframes hw-pulse { 50% { opacity: 0.35; } }

/* Data age. Anything older than 3x its cadence renders stale — and says so in
   text, not only in color. */
.age, .hw-age {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  white-space: nowrap;
}
.age--stale, .hw-age--stale { color: var(--warn); font-weight: 600; }
.age--stale::before, .hw-age--stale::before { content: "⚠ "; }
.age--error { color: var(--err); font-weight: 600; }
.age--error::before { content: "✕ "; }

/* Per-datum provenance — "where THIS number came from, and when".
   ------------------------------------------------------------------
   A panel-level "updated 4 min ago" is a claim about the panel, and readers
   take it as a claim about every number under it. In this app that is false
   three ways: two feeds land in one block, "issued" and "fetched" are
   different times, and a value can silently fall back to an older observed
   fix. So each number carries its own answer.

   Quiet by construction. Three tiles reading "NHC advisory" in identical dim
   mono is not noise, it is the baseline that makes the fourth one legible on
   the day it differs — which is why only --fallback gets ink. */
.hw-src {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.34em;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  line-height: 1.45;
  color: var(--text-faint);
}
.hw-src__feed { color: var(--text-dim); }
/* Separator plus its value are one unwrappable unit — see srcTag(). */
.hw-src__part { display: inline-flex; align-items: baseline; gap: 0.34em; }
.hw-src__sep { color: var(--text-faint); opacity: 0.6; }
/* The tag holds .hw-age children, and `.age, .hw-age` sets BOTH size and
   --text-faint at (0,1,0). Any surface that lifts .hw-src's colour for
   contrast — the tinted first hero tile does exactly that — would set a colour
   the nested age span then quietly overrides. Measured: 4.4:1 in light theme,
   against a 4.5 floor. So the age inherits instead of re-asserting.
   :not() keeps the stale state, which is a warning and must stay --warn. */
.hw-src .hw-age { font-size: inherit; }
.hw-src .hw-age:not(.hw-age--stale) { color: inherit; }

/* A value the advisory did not carry, filled from an older observed fix.
   Marked in text and glyph, never colour alone. */
.hw-src--fallback { color: var(--text-dim); }
.hw-src--fallback .hw-src__feed {
  color: var(--text);
  text-decoration: underline dotted currentColor;
  text-underline-offset: 2px;
}
.hw-src__mark { color: var(--text-faint); }

/* A provenance table row whose value came from a fallback, not the advisory.
   Tinted AND marked with a glyph in the cell text, never tint alone. */
.hw-table__row--fallback > th,
.hw-table__row--fallback > td {
  background: color-mix(in srgb, var(--warn) 8%, transparent);
}

/* Mount points. Each keeps the tag on its own line so a long source name
   never pushes the value it describes out of shape. */
.hw-metric__src { margin-top: 3px; }
.hw-kv__src { display: block; margin-top: 2px; }

/* ============================================================================
   04  SHELL, PRIMITIVES AND LAYOUT  (island-first redesign)
   ------------------------------------------------------------------------
   04.1 mode visibility + empty mounts   04.6 dots, glyphs, numbers, type
   04.2 tile                             04.7 layer chips, links
   04.3 well, pill                       04.8 header
   04.4 segmented control                04.9 footer, noscript, boot error
   04.5 buttons                          04.10 layout
   ========================================================================= */

/* -- 04.1 mode visibility + empty mounts --------------------------------- *
 * Global and heavy on purpose (CONTRACTS §6): no module re-implements it, and
 * no module's `display` rule can resurrect the other mode's mount. */
:root[data-mode="simple"] [data-show="full"],
:root[data-mode="full"] [data-show="simple"] { display: none !important; }

/* Every layout mount stacks its tiles; modules never set outer margins. Written
   with :where() so it weighs NOTHING — an owner placing its own mount by id
   (e.g. two tiles side by side in #tiles-simple-a) wins without a fight. */
:where(#answer-panel, #tiles-simple-a, #odds-panel, #tiles-simple-b, #tiles-simple-c,
       #full-storm, #full-left, #motion-bar, #odds-full, #full-right, #imagery-panel,
       #full-right-b) {
  display: flex;
  flex-direction: column;
  gap: var(--tile-gap);
  min-width: 0;
}
/* ...and an empty mount is removed from the flow entirely, so a module that
   rendered nothing (no storm, no guidance, other mode) leaves no double gap.
   (1,1,0) — heavier than an owner's `#mount { display: grid }` — because an
   empty grid box in a gapped column is exactly the phantom band this prevents.
   Not applied to #toast: a live region must exist before its first message. */
:is(#answer-panel, #tiles-simple-a, #odds-panel, #tiles-simple-b, #tiles-simple-c,
    #full-storm, #full-left, #motion-bar, #odds-full, #full-right, #imagery-panel,
    #full-right-b, #map-head, #map-callouts, #map-legend,
    #island-rail, #first-visit):empty { display: none; }

/* -- 04.2 tile ----------------------------------------------------------- *
 * Markup: js/tile.js tile(). Head = dot · title · fact · pill · extras · toggle.
 *
 * NO transform, filter, contain, perspective or will-change on .hw-tile, ever:
 * each creates a containing block for position:fixed descendants, and map.js
 * puts the map in fullscreen with position:fixed INSIDE #map-tile, which is a
 * .hw-tile. The fullscreen map would silently shrink to the tile's box. */
.hw-tile {
  background: var(--tile);
  color: var(--text);
  border-radius: var(--tile-r);
  box-shadow: var(--shadow-tile);
  padding: var(--tile-pad);
  min-width: 0;
  /* Invisible normally; under forced-colors (Windows High Contrast) shadows
     are dropped and the tile would otherwise have no edge at all. */
  outline: 1px solid transparent;
  outline-offset: -1px;
  transition: padding var(--dur-3) var(--ease-out);
}

.hw-tile__head {
  position: relative;      /* anchors the collapsed row's whole-row hit area */
  display: flex;
  align-items: center;
  gap: 6px 10px;
  min-width: 0;
  min-height: 28px;
}
.hw-tile__title {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-title);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: 0;
  color: var(--text);
}
/* Expanded: the dot and the headline fact belong to the collapsed row only —
   the body says it at length. */
.hw-tile__head > .hw-tile__dot,
.hw-tile__head > .hw-tile__fact { display: none; }

.hw-tile__meta { flex: none; }

.hw-tile__toggle {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--tile-2);
  color: var(--text-faint);     /* 6.07 dark / 5.10 light on --tile-2 */
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.hw-tile__toggle:hover { background: var(--tile-track); color: var(--text); }
/* ⌃ and ⌄ sit at opposite ends of their em box, so each is nudged back to the
   optical centre of the circle. Measured in Chrome/macOS against the fallback
   face (Manrope has neither glyph). The alt text after "/" is empty: the
   button already carries "Collapse <title>" as its accessible name. */
.hw-tile__chev::before {
  content: "⌃";
  content: "⌃" / "";
  display: block;
  transform: translateY(0.18em);
}
.hw-tile--collapsed > .hw-tile__head .hw-tile__chev::before {
  content: "⌄";
  content: "⌄" / "";
  transform: translateY(-0.18em);
}

/* Collapsed row: 12px block padding · dot · title 14/700 · fact · chevron. */
.hw-tile--collapsed { padding-block: 12px; }
.hw-tile--collapsed > .hw-tile__head > .hw-tile__dot { display: inline-block; }
.hw-tile--collapsed > .hw-tile__head > .hw-tile__title {
  flex: 0 1 auto;
  font-size: 0.875rem;
}
.hw-tile--collapsed > .hw-tile__head > .hw-tile__fact {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-align: end;
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--text-faint);     /* 6.67 dark / 5.54 light on --tile */
}
/* A tile with NO toggle (collapsible: false) that still carries a fact is a
   permanent one-line row — e.g. "Will it hit Kauai? · model tracks
   unavailable without the edge proxy" (§4.5 no-guidance state). Its fact is
   the content, so it shows expanded, with the same ellipsis. Requested by
   odds, lead-approved. Note it applies to ANY toggle-less tile that passes a
   `fact`: the verdict and the map tile simply should not pass one. */
.hw-tile:not(:has(> .hw-tile__head > .hw-tile__toggle)) > .hw-tile__head > .hw-tile__fact {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-align: end;
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--text-faint);
}
.hw-tile:not(:has(> .hw-tile__head > .hw-tile__toggle)) > .hw-tile__head > .hw-tile__title { flex: 0 1 auto; }
.hw-tile--collapsed > .hw-tile__head > .hw-tile__meta { display: none; }
/* The whole head row is the toggle's hit area: an overlay hung off the
   toggle but positioned against the head (the toggle itself is NOT positioned),
   stretched over the tile's own padding so the row edge-to-edge is live.
   Real controls inside the head are lifted above it.
   Item 7 (panels-ux wave): this used to be scoped to `.hw-tile--collapsed`
   only, so an EXPANDED tile could only be collapsed by hitting the 28px
   chevron — clicking the title itself did nothing (measured). The `:has()`
   guard keeps it off toggle-less tiles (`collapsible: false`, e.g. the
   verdict tile), which have no `.hw-tile__toggle` to click at all. */
.hw-tile__head:has(> .hw-tile__toggle) { cursor: pointer; }
.hw-tile__head > .hw-tile__toggle::after {
  content: "";
  position: absolute;
  inset: -12px calc(-1 * var(--tile-pad));
}
/* Item 7: descendant, not just direct child — a headHtml control like
   panels' storm chips (js/panels.js stormChips()) sits inside its own
   wrapper div, one level deeper than this used to reach. Measured: with the
   `>` combinator, tapping a storm chip fell through to the overlay behind it
   and collapsed the tile instead of switching storms. */
.hw-tile__head :is(a, button, details, input, select, label):not(.hw-tile__toggle) {
  position: relative;
  z-index: 1;
}
.hw-tile__head:has(> .hw-tile__toggle):hover > .hw-tile__toggle { background: var(--tile-track); color: var(--text); }

/* Body collapse: grid 1fr -> 0fr animates to and from the content's real
   height with no JS measurement. visibility flips AFTER the close transition
   (and immediately on open), so collapsed content leaves the tab order and the
   accessibility tree rather than merely going transparent.

   Overflow: the inner must clip while the row is shorter than its content, but
   a permanent clip would also shear focus rings and any popover that hangs out
   of a tile. So the clip is permanent only while collapsed, and on EXPAND it
   is held for exactly the transition by a keyframe (overflow is discrete, so an
   animation can pin it). overflow-clip-margin leaves room for a 2px ring at a
   2px offset. */
.hw-tile__body {
  display: grid;
  grid-template-rows: 1fr;
  margin-top: 12px;
  opacity: 1;
  visibility: visible;
  transition:
    grid-template-rows var(--dur-3) var(--ease-out),
    margin-top var(--dur-3) var(--ease-out),
    opacity var(--dur-3) var(--ease-out),
    visibility 0s linear 0s;
}
.hw-tile__inner {
  min-height: 0;
  min-width: 0;
  animation: hw-tile-clip var(--dur-3) linear;
}
@keyframes hw-tile-clip { from, to { overflow: clip; overflow-clip-margin: 6px; } }
.hw-tile--collapsed > .hw-tile__body {
  grid-template-rows: 0fr;
  margin-top: 0;
  opacity: 0;
  visibility: hidden;
  transition:
    grid-template-rows var(--dur-3) var(--ease-out),
    margin-top var(--dur-3) var(--ease-out),
    opacity var(--dur-3) var(--ease-out),
    visibility 0s linear var(--dur-3);
}
.hw-tile--collapsed > .hw-tile__body > .hw-tile__inner {
  overflow: clip;
  animation: none;
}
/* A tile with no head (the map) or no body content needs no body gap. */
.hw-tile__body:has(> .hw-tile__inner:empty) { margin-top: 0; }

/* The collapse gate (CONTRACTS §0): no transition at all — not a shortened one
   — when the reader or the OS has asked for less motion. §19 also scales every
   duration, but a scaled 240ms is still motion. */
:root[data-motion="off"] :is(.hw-tile, .hw-tile__body, .hw-tile__inner) {
  transition: none;
  animation: none;
}
@media (prefers-reduced-motion: reduce) {
  :is(.hw-tile, .hw-tile__body, .hw-tile__inner) { transition: none; animation: none; }
}

/* -- 04.3 well, pill ----------------------------------------------------- */

.hw-well {
  background: var(--tile-2);
  border-radius: var(--r-inner);
  padding: var(--well-pad);
  min-width: 0;
}

.hw-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  background: var(--tile-2);
  color: var(--text-dim);        /* 8.03 dark / 6.72 light */
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 700;
  line-height: 1.5;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* "18 of 31 forecasts · partial". Tinted AND worded — never tint alone.
   --warn on its own 14% wash: 7.61 dark / 5.08 light. */
.hw-pill--warn {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 14%, var(--tile-2));
}

/* -- 04.4 segmented control ---------------------------------------------- *
 * Simple | Full data, units, density. Selected = [aria-pressed="true"]. */
.hw-seg {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 4px;
  border-radius: var(--r-pill);
  background: var(--tile-2);
  flex: none;
}
.hw-seg__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 7px 14px;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text-faint);      /* 6.07 dark / 5.10 light on --tile-2 */
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.hw-seg__btn:hover { color: var(--text); }
.hw-seg__btn[aria-pressed="true"] {
  background: var(--tile);
  color: var(--text);            /* 15.28 dark / 17.86 light */
  box-shadow: var(--shadow-1);
}
.hw-seg__btn:disabled { opacity: 0.5; cursor: not-allowed; }
.hw-seg__btn:focus-visible { outline-offset: -2px; border-radius: var(--r-pill); }

/* -- 04.5 buttons -------------------------------------------------------- *
 * Primary = the island's severity (P3: one of the three places severity colour
 * is spent). Secondary = accent. Same geometry, min 44px tall at every width —
 * these are the page's calls to action, not chrome. */
.hw-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
  min-height: 44px;
  padding: 12px 18px;
  border: 0;
  border-radius: 12px;
  background: var(--tile-2);
  color: var(--text);
  font-family: var(--font-display);
  font-size: 0.875rem;
  font-weight: 800;
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: box-shadow var(--dur-1) var(--ease-out);
}
/* Hover lightens/darkens by an overlay in the ink's own colour, so it works on
   every fill in both themes without a per-level hover token. */
.hw-btn:hover { box-shadow: inset 0 0 0 999px color-mix(in srgb, currentColor 10%, transparent); }
.hw-btn:focus-visible { outline-offset: 3px; border-radius: 12px; }
.hw-btn--primary { background: var(--accent); color: var(--text-on-accent); }
.hw-btn--secondary { background: var(--accent); color: var(--text-on-accent); }   /* 7.97 / 6.56 */
.hw-btn--extreme  { background: var(--sev-extreme);  color: var(--ink-on-sev); }  /* 6.10 / 6.39 */
.hw-btn--severe   { background: var(--sev-severe);   color: var(--ink-on-sev); }  /* 8.47 / 6.50 */
.hw-btn--moderate { background: var(--sev-moderate); color: var(--ink-on-sev); }  /* 12.25 / 6.78 */
/* No alert (§5 "Prepare for the season"): no severity colour spent at all. */
.hw-btn--none     { background: var(--tile-2);       color: var(--text); }        /* 13.90 / 16.45 */

/* -- 04.6 dots, glyphs, numbers, type ----------------------------------- */

/* The 8px status dot. Decorative (aria-hidden): always paired with words or a
   glyph. `none` = no alert posted = --ok (CONTRACTS §0); `unknown` = the alert
   feed has not answered, which must NOT look like all-clear, hence hollow. */
.hw-dot {
  display: inline-block;
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--idle);
}
.hw-dot--extreme  { background: var(--sev-extreme); }
.hw-dot--severe   { background: var(--sev-severe); }
.hw-dot--moderate { background: var(--sev-moderate); }
.hw-dot--none,
.hw-dot--ok       { background: var(--ok); }
.hw-dot--warn     { background: var(--warn); }
.hw-dot--err      { background: var(--err); }
.hw-dot--unknown  { background: transparent; box-shadow: inset 0 0 0 1.5px var(--sev-unknown); }
.hw-dot--loading  { background: var(--accent); animation: hw-pulse 1.6s var(--ease-in-out) infinite; }
:root[data-motion="off"] .hw-dot--loading { animation: none; }

/* Glyph ink by level. The glyph SHAPE (◆ ▲ ● ○ ◌) is the module's text; this
   only colours it. Calm (none/unknown) spends no colour at all (§4.1). */
.hw-glyph { font-style: normal; line-height: 1; font-family: var(--font-ui); }
.hw-glyph--extreme  { color: var(--sev-extreme); }
.hw-glyph--severe   { color: var(--sev-severe); }
.hw-glyph--moderate { color: var(--sev-moderate); }
.hw-glyph--fringe   { color: var(--sev-moderate); }
.hw-glyph--none,
.hw-glyph--unknown  { color: var(--text-faint); }

/* Big number + ~45% unit (§2.1). */
.hw-num {
  font-family: var(--font-display);
  font-size: var(--fs-num);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.hw-num__unit {
  margin-left: 4px;
  font-size: 0.45em;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--text-faint);
}

.hw-meta  { font-family: var(--font-display); font-size: var(--fs-meta); font-weight: 700; line-height: 1.4; color: var(--text-faint); }
.hw-micro { font-family: var(--font-display); font-size: var(--fs-micro); font-weight: 700; line-height: 1.35; color: var(--text-faint); }
.hw-mono  { font-family: var(--font-mono); font-size: var(--fs-mono); font-weight: 500; font-variant-numeric: tabular-nums; }
.hw-body  { font-family: var(--font-display); font-size: var(--fs-body); font-weight: 500; line-height: 1.5; }
.hw-dim   { color: var(--text-dim); }
.hw-faint { color: var(--text-faint); }

/* -- 04.7 layer chips, links -------------------------------------------- */

/* Map layer chips (§2.4). On = [aria-pressed="true"] or --on. */
.hw-chipbtn {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 4px 10px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--tile-2);
  color: var(--text-faint);      /* 6.07 / 5.10 */
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 700;
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
}
.hw-chipbtn:hover { color: var(--text); }
.hw-chipbtn--on,
.hw-chipbtn[aria-pressed="true"] { background: var(--accent); color: var(--text-on-accent); }
.hw-chipbtn:focus-visible { border-radius: var(--r-pill); }

/* Inline and standalone links ("weather.gov ↗", "Why these bands? ⌄"). Size is
   inherited so the same class works in a sentence and in a tile footer. */
.hw-link {
  color: var(--accent);
  font-family: inherit;
  font-weight: 700;
  text-decoration: none;
}
.hw-link:hover { text-decoration: underline; text-underline-offset: 0.18em; }

/* -- 04.8 header (§3.1) -------------------------------------------------- *
 * The header is itself a tile. One row; below 1100px it wraps into exactly two
 * rows — identity + status on top, controls underneath — by `order`, so the
 * DOM (and the tab order) stays brand, switcher, mode, status, full, theme.
 * Sticky from 768px; static on phones, where switcher's chip rail sticks
 * instead. js/boot.js measures the header into --header-h. */
.hw-header {
  padding: var(--page-gutter) var(--page-gutter) var(--tile-gap);
  background: var(--frame);
}
@media (min-width: 768px) {
  .hw-header { position: sticky; top: 0; z-index: 60; }
}
.hw-header__inner {
  max-width: var(--page-max);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding: 10px 12px 10px 18px;
  background: var(--tile);
  border-radius: var(--r-tile);
  box-shadow: var(--shadow-tile);
  outline: 1px solid transparent;
  outline-offset: -1px;
  min-width: 0;
}
.hw-header__brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: none;
  color: var(--text);
  text-decoration: none;
  border-radius: var(--r-pill);
}
.hw-header__mark {
  display: inline-grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--text-on-accent);
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1;
}
.hw-header__title {
  font-family: var(--font-display);
  font-size: 1.0625rem;          /* 17 */
  font-weight: 800;
  letter-spacing: -0.02em;
  white-space: nowrap;
}
.hw-header__switcher { min-width: 0; flex: 0 1 auto; }
.hw-header__mode { margin-inline-start: auto; }
.hw-header__full {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.hw-header__link {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 6px 4px;
  color: var(--accent);          /* 7.45 / 6.56 on --tile */
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}
.hw-header__link:hover { text-decoration: underline; }
/* boot.js hides "hunters 3D" when no edge proxy is configured (recon needs it). */
.hw-header__link[hidden] { display: none; }

/* Status chip: dot · "live" · clock · "· all times HST". The HST suffix is the
   ONE place the page states its timezone (P7), so it survives every width;
   the chip may wrap between its words, never drop them. */
.hw-status {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 6px;
  padding: 6px 12px;
  border-radius: var(--r-pill);
  background: var(--tile-2);
  color: var(--text-dim);        /* 8.03 / 6.72 on --tile-2 */
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  line-height: 1.35;
  min-width: 0;
}
.hw-status > .hw-dot { margin-inline-end: 2px; }
.hw-status__text { color: var(--text); white-space: nowrap; }
.hw-status__sep { color: var(--text-faint); }   /* 6.07 / 5.10 on --tile-2 */
.hw-status__clock { font-variant-numeric: tabular-nums; white-space: nowrap; }
.hw-status__tz { white-space: nowrap; }

.hw-iconbtn {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--tile-2);
  color: var(--text-dim);
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.hw-iconbtn:hover { color: var(--text); background: var(--tile-track); }
.hw-iconbtn:focus-visible { border-radius: 50%; }

/* Two rows: [brand mode … status theme] / [switcher full-controls].
   The row-2 items are the two WIDE ones — the six-island switcher (~540px)
   and, in Full, units + density + hunters (~330px) — so they get a row to
   themselves. Measured before this split, with mode on row 2 instead: 1024
   Full wrapped to three rows and a 175px STICKY header (19% of a 900px
   viewport); 1440 Full wrapped unplanned and stranded the theme button alone
   at the left of row 2. One row only fits Full from ~1640px, hence the second
   block. The ::after is a zero-height forced line break; row-gap is 0 and the
   row-2 items carry the 8px so the break adds no phantom gap. */
@media (max-width: 1099px) {
  .hw-header__inner { row-gap: 0; }
  .hw-header__brand { order: 1; }
  .hw-header__mode { order: 2; margin-inline-start: 0; }
  .hw-status { order: 3; margin-inline-start: auto; }
  .hw-header__theme { order: 4; }
  .hw-header__inner::after { content: ""; order: 5; flex-basis: 100%; height: 0; }
  .hw-header__switcher { order: 6; }
  .hw-header__full { order: 7; }
  .hw-header__switcher, .hw-header__full { margin-top: 8px; }
  /* An empty switcher (phones use the rail) must not add a row of margin. */
  .hw-header__switcher:empty { display: none; }
}
@media (min-width: 1100px) and (max-width: 1679px) {
  :root[data-mode="full"] .hw-header__inner { row-gap: 0; }
  :root[data-mode="full"] .hw-header__brand { order: 1; }
  :root[data-mode="full"] .hw-header__mode { order: 2; margin-inline-start: 0; }
  :root[data-mode="full"] .hw-status { order: 3; margin-inline-start: auto; }
  :root[data-mode="full"] .hw-header__theme { order: 4; }
  :root[data-mode="full"] .hw-header__inner::after { content: ""; order: 5; flex-basis: 100%; height: 0; }
  :root[data-mode="full"] .hw-header__switcher { order: 6; margin-top: 8px; }
  :root[data-mode="full"] .hw-header__full { order: 7; margin-top: 8px; margin-inline-start: auto; }
}
/* 768-1023 Full: still two rows. Measured with switcher's compact pill
   (446px), row 2 has ~714px and switcher + units + density + hunters needed
   ~772px even with the segs tightened and hunters reduced to its icon, so
   compaction alone left a 171px three-row STICKY header over the content.
   What fits is rebalancing: the wordmark goes visually (the ◉ mark stays
   visible; "HawaiiStorm" stays the link's accessible name via the
   sr-only technique) and density moves up to row 1.
     row 1  ◉ · Simple|Full data · Comfy|Compact · … status · theme   ~650px
     row 2  island switcher · °F|°C · ✈                              ~636px
   .hw-header__full becomes display:contents so its three children order
   independently — no markup change. Hunters sheds its words with font-size:0
   on the link (text nodes stay in the accessibility tree; only the aria-hidden
   ✈ span gets a size back), so its name is still "hunters 3D (opens in a new
   tab)". Headroom ~60px on each row, fine or coarse, which also absorbs a
   longer status string ("12/12 source error"). */
@media (min-width: 768px) and (max-width: 1023px) {
  :root[data-mode="full"] .hw-header__title {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  :root[data-mode="full"] .hw-header__full { display: contents; }
  :root[data-mode="full"] .hw-header__brand { order: 1; }
  :root[data-mode="full"] .hw-header__mode { order: 2; }
  :root[data-mode="full"] .hw-header__full > .hw-seg:has(#density-comfy) { order: 3; }
  :root[data-mode="full"] .hw-status { order: 4; margin-inline-start: auto; }
  :root[data-mode="full"] .hw-header__theme { order: 5; }
  :root[data-mode="full"] .hw-header__inner::after { order: 6; }
  :root[data-mode="full"] .hw-header__switcher { order: 7; margin-top: 8px; }
  :root[data-mode="full"] .hw-header__full > .hw-seg:has(#units-imperial) { order: 8; margin-top: 8px; }
  :root[data-mode="full"] .hw-header__full > .hw-header__link {
    order: 9;
    margin-top: 8px;
    font-size: 0;
    min-width: 28px;
    justify-content: center;
  }
  :root[data-mode="full"] .hw-header__full > .hw-header__link > [aria-hidden="true"] { font-size: 15px; }
}
@media (min-width: 768px) and (max-width: 1023px) and (pointer: coarse) {
  :root[data-mode="full"] .hw-header__full > .hw-header__link { min-width: 44px; }
}
@media (max-width: 767px) {
  .hw-header__inner { padding: 8px 8px 8px 12px; border-radius: var(--r-tile); column-gap: 8px; }
  /* Row 1 is the brand and the theme button; the status chip takes the full
     second row so "· all times HST" never has to fight the brand for width
     (brand + mode + status + theme measure ~560px against 366 available). */
  .hw-header__theme { order: 2; margin-inline-start: auto; }
  .hw-header__inner::after { order: 3; }
  .hw-status { order: 4; flex: 1 1 100%; margin-inline-start: 0; margin-top: 8px; justify-content: flex-start; }
  .hw-header__mode { order: 5; margin-top: 8px; }
  .hw-header__switcher { order: 6; }
  .hw-header__full { order: 7; }
}

/* -- 04.9 footer, noscript, boot error, skip link ------------------------- */

.hw-footer {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 24px var(--page-gutter) 32px;
  color: var(--text-faint);      /* 7.55 / 4.62 on --frame */
  font-size: var(--fs-meta);
  line-height: 1.55;
}
.hw-footer__disclaimer {
  max-width: var(--measure);
  color: var(--text-dim);        /* 9.99 / 6.09 on --frame */
  font-size: 0.8125rem;
}
.hw-footer__disclaimer strong { color: var(--text); }
.hw-footer__meta { max-width: var(--measure); margin-top: 8px; }
.hw-footer a { color: var(--accent); }   /* 8.43 / 5.47 on --frame */

.hw-noscript,
.hw-boot-error {
  max-width: var(--page-max);
  margin: 0 auto var(--tile-gap);
  padding: var(--tile-pad);
  border-radius: var(--tile-r);
  background: var(--tile);
  color: var(--text-dim);
  font-size: var(--fs-body);
  box-shadow: var(--shadow-tile);
}
.hw-noscript { border-inline-start: 6px solid var(--warn); }
.hw-boot-error { border-inline-start: 6px solid var(--err); }
.hw-noscript strong, .hw-boot-error strong { color: var(--text); }
.hw-boot-error code { display: block; margin-top: 6px; color: var(--text-faint); font-size: var(--fs-mono); word-break: break-word; }
/* Outside .hw-layout, so they need the page gutter themselves. */
body > noscript > .hw-noscript,
body > .hw-boot-error { margin-inline: var(--page-gutter); }
@media (min-width: 1800px) {
  body > noscript > .hw-noscript,
  body > .hw-boot-error { margin-inline: auto; }
}

/* -- 04.10 layout (CONTRACTS §7) ----------------------------------------- *
 *
 *   ≥1280  Simple  520 | map (sticky, ≥720 tall)
 *          Full    392 | 1fr | 392   (right column is its own 2-col grid)
 *   768-   Simple  one column: verdict, MAP 480, a, odds, b, c
 *   1279   Full    left | center, right stacked under center
 *   <768   Simple  verdict, a, odds, b, MAP, c  (+ action bar reserve)
 *          Full    storm, map, timeline, odds, right, left, goes, right-b
 *
 * Where a one-column ORDER differs from DOM order, the columns become
 * display:contents and the mounts are ordered directly. Where there really are
 * columns, the columns are flex stacks so each runs to its own height — a
 * shared row grid would make a tall left mount open a hole in the center.
 * Every track is minmax(0, …): a bare 1fr is minmax(auto, 1fr) and one nowrap
 * table would drag the track past the viewport (measured 905px at 390, last
 * wave). */
.hw-layout {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 0 var(--page-gutter);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--tile-gap);
  align-items: start;
}
.hw-layout:focus { outline: none; }   /* skip-link target, tabindex=-1 */
.hw-col {
  display: flex;
  flex-direction: column;
  gap: var(--tile-gap);
  min-width: 0;
}
/* The map's sticky offset and anchor targets both clear the sticky header. */
.hw-layout [id] { scroll-margin-top: var(--sticky-top); }

/* ≥1280 --------------------------------------------------------------- */
@media (min-width: 1280px) {
  :root[data-mode="simple"] .hw-layout { grid-template-columns: 520px minmax(0, 1fr); align-items: stretch; }
  /* The column stretches to the row; the map sticks inside it and fills the
     viewport under the header, never less than 720px (§2.5).

     Sized through --map-h (the height the legacy `#map` rule in §17 reads)
     plus a min-height, NOT a fixed tile height. A fixed height with a child
     that sizes itself was measured overflowing the tile by ~110px at 1024
     before map.css existed; this way the tile can never be shorter than its
     map. map.css may instead make the stage flex:1 inside the tile — then
     this --map-h is simply unused. Static per breakpoint on purpose: Leaflet
     needs invalidateSize() after any container resize. */
  :root[data-mode="simple"] {
    --map-h: max(calc(720px - 2 * var(--tile-pad)),
                 calc(100dvh - var(--sticky-top) - var(--tile-gap) - 2 * var(--tile-pad)));
  }
  :root[data-mode="simple"] #map-tile {
    position: sticky;
    top: var(--sticky-top);
    min-height: 720px;
  }

  :root[data-mode="full"] .hw-layout {
    grid-template-columns: 392px minmax(0, 1fr) 392px;
  }
}

/* Full data, right column: a 2-col grid so Sea & tides and GOES-18 sit side
   by side (screen 3b) while every other tile spans both. The three mounts are
   display:contents so their tiles become the grid's items. Motion renders the
   GOES tile straight into #imagery-panel; if that ever gains a wrapper, the
   span rule below misses and GOES goes full width — visible, not broken. */
@media (min-width: 1024px) {
  :root[data-mode="full"] .hw-col--right {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--tile-gap);
    align-content: start;
  }
  :root[data-mode="full"] :is(#full-right, #full-right-b, #imagery-panel) { display: contents; }
  :root[data-mode="full"] .hw-col--right > * > * { grid-column: 1 / -1; min-width: 0; }
  :root[data-mode="full"] .hw-col--right #tile-full-tides,
  :root[data-mode="full"] .hw-col--right #tile-full-goes { grid-column: auto; }
}

/* 768-1279 ------------------------------------------------------------ */
@media (min-width: 768px) and (max-width: 1279px) {
  :root[data-mode="simple"] .hw-col { display: contents; }
  :root[data-mode="simple"] #answer-panel   { order: 1; }
  /* "map 480px" (§7) is the MAP; the tile adds its own chrome around it. */
  :root[data-mode="simple"] { --map-h: 480px; }
  :root[data-mode="simple"] #map-tile       { order: 2; }
  :root[data-mode="simple"] #tiles-simple-a { order: 3; }
  :root[data-mode="simple"] #odds-panel     { order: 4; }
  :root[data-mode="simple"] #tiles-simple-b { order: 5; }
  :root[data-mode="simple"] #tiles-simple-c { order: 6; }

  /* Left spans both rows; center on row 1, right under it on row 2. The spare
     height of a tall left column goes to row 2 (1fr), under the right column,
     never between the map and the tiles beneath it. */
  :root[data-mode="full"] .hw-layout {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    grid-template-rows: auto 1fr;
  }
  :root[data-mode="full"] .hw-col--left   { grid-column: 1; grid-row: 1 / span 2; }
  :root[data-mode="full"] .hw-col--center { grid-column: 2; grid-row: 1; }
  :root[data-mode="full"] .hw-col--right  { grid-column: 2; grid-row: 2; }
}

/* <768 ---------------------------------------------------------------- */
@media (max-width: 767px) {
  .hw-col { display: contents; }

  :root[data-mode="simple"] #answer-panel   { order: 1; }
  :root[data-mode="simple"] #tiles-simple-a { order: 2; }
  :root[data-mode="simple"] #odds-panel     { order: 3; }
  :root[data-mode="simple"] #tiles-simple-b { order: 4; }
  :root[data-mode="simple"] #map-tile       { order: 5; }
  :root[data-mode="simple"] #tiles-simple-c { order: 6; }

  :root[data-mode="full"] #full-storm    { order: 1; }
  :root[data-mode="full"] #map-tile      { order: 2; }
  :root[data-mode="full"] #motion-bar    { order: 3; }
  :root[data-mode="full"] #odds-full     { order: 4; }
  :root[data-mode="full"] #full-right    { order: 5; }
  :root[data-mode="full"] #full-left     { order: 6; }
  :root[data-mode="full"] #imagery-panel { order: 7; }
  :root[data-mode="full"] #full-right-b  { order: 8; }
}

/* ============================================================================
   05  PANEL  (.hw-panel — panels.js renders one inside each mount)
   ========================================================================= */

.hw-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  overflow: clip;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.hw-panel__head {
  display: flex;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  flex-wrap: wrap;
  padding: var(--s-3) var(--s-4);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  min-height: 48px;
  /* Never shrink: inside the height-capped alerts column the body is the
     flexible item, and a shrunk header collapses onto the body's first row. */
  flex: none;
}

.hw-panel__title {
  font-size: var(--fs-xs);
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text);
  margin: 0;
}

.hw-panel__count {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
  margin-left: 0.5em;
}

.hw-panel__meta {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
  min-width: 0;
}
.hw-panel__meta a { color: var(--text-dim); text-decoration: none; }
.hw-panel__meta a:hover { color: var(--accent); text-decoration: underline; }

.hw-panel__body {
  padding: var(--s-4);
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

/* Panel states. --stale = the backing source is past 3x its cadence; the age
   readout in .hw-panel__meta says so in words, this is the peripheral cue. */
/* DO NOT COPY THIS SHORTHAND INTO A COMPONENT WITH A RAIL.

   `border-color` sets all four sides. That is correct here and only here,
   because .hw-panel's four borders are all --border, so tinting all four is
   what "this panel's source is stale" should look like. It is NOT safe in any
   component that carries a severity rail on one side — and five in this file do
   (.hw-hero--active/--calm, .hw-alert--sev-*, .hw-surge, .hw-models__verdict).
   Pasted there, the shorthand silently repaints the rail.

   This is not hypothetical: `answer` copied this exact line into .hw-answer,
   which has `border-inline-start: 6px solid var(--tone)`. Measured, a Tropical
   Storm Warning panel's orange rail went grey-gold the moment the alert feed
   went stale — severity visually downgraded by a data-freshness condition.

   In a railed component, tint only the three sides that are not the rail:
     border-block-color: color-mix(in srgb, var(--warn) 50%, var(--border));
     border-inline-end-color: color-mix(in srgb, var(--warn) 50%, var(--border));

   The principle, which is a sibling of the state-vs-emphasis one in the file
   header rather than the same rule (answer's formulation, and it is the better
   one): the RAIL says what the weather is doing; the BORDER says how much to
   trust what you are reading. A stale feed is a reason to distrust the number.
   It is never a reason to downgrade the warning that number is reporting. */
.hw-panel--stale { border-color: color-mix(in srgb, var(--warn) 50%, var(--border)); }
.hw-panel--stale > .hw-panel__head {
  background: color-mix(in srgb, var(--warn) 8%, var(--surface-2));
  box-shadow: inset 3px 0 0 var(--warn);
}
/* Same fix, same reason, as the tinted hero metric in section 09. The head's
   meta line is --text-faint, and on this wash it measures 4.55:1 in light
   theme — a pass, by 0.05. That is not a margin, it is the next regression,
   and it only appears when a feed has gone stale, which is precisely when the
   age readout it contains has to be readable. --text-dim takes it to 5.99
   light / 6.78 dark. */
.hw-panel--stale > .hw-panel__head .hw-panel__meta { color: var(--text-dim); }
/* A panel that threw during render. The page keeps living around it. */
.hw-panel--error { border-color: color-mix(in srgb, var(--err) 50%, var(--border)); }
.hw-panel__error { color: var(--text-dim); font-size: var(--fs-sm); margin: 0; }
.hw-panel__error-msg {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--err);
  background: color-mix(in srgb, var(--err) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--err) 35%, transparent);
  border-radius: var(--r-sm);
  padding: var(--s-2);
  word-break: break-word;
  margin: 0;
}

/* ============================================================================
   06  BADGE + CHIP — the severity, kind, threat and status system
   ------------------------------------------------------------------------
   Rank is encoded FOUR ways at once, so removing color loses nothing:
     1. hue            (--sev-* / --kind-* / --threat-*)
     2. rail width     on the card that carries the badge (§10)
     3. glyph shape    ◆ ▲ ● ■ ○ △ — supplied by panels.js in .hw-badge__glyph
     4. text weight    750 → 400, plus the always-present written label
   ========================================================================= */

.hw-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 1px var(--s-2);
  border-radius: var(--r-xs);
  border: 1px solid currentColor;
  background: color-mix(in srgb, currentColor 12%, var(--surface));
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  line-height: 1.7;
  white-space: nowrap;
  vertical-align: baseline;
  color: var(--idle);
}
.hw-badge__glyph { font-size: 1.05em; line-height: 1; }
.hw-badge__text { letter-spacing: var(--tracking-caps); }

/* severity */
.hw-badge--sev-extreme  { color: var(--sev-extreme);  font-weight: 750; border-width: 2px; }
.hw-badge--sev-severe   { color: var(--sev-severe);   font-weight: 700; }
.hw-badge--sev-moderate { color: var(--sev-moderate); font-weight: 650; }
.hw-badge--sev-minor    { color: var(--sev-minor);    font-weight: 600; }
.hw-badge--sev-unknown  { color: var(--sev-unknown);  font-weight: 400; border-style: dashed; }

/* product kind */
.hw-badge--warning  { color: var(--kind-warning);  font-weight: 750; }
.hw-badge--watch    { color: var(--kind-watch);    font-weight: 700; }
.hw-badge--advisory { color: var(--kind-advisory); font-weight: 600; }
.hw-badge--info     { color: var(--kind-info);     font-weight: 500; }

/* threat */
.hw-badge--threat-direct  { color: var(--threat-direct);  font-weight: 750; border-width: 2px; }
.hw-badge--threat-warning { color: var(--threat-warning); font-weight: 700; }
.hw-badge--threat-watch   { color: var(--threat-watch);   font-weight: 650; }
.hw-badge--threat-none    { color: var(--threat-none);    font-weight: 500; }

/* storm / calm */
.hw-badge--storm { color: var(--sev-severe); font-weight: 700; }
.hw-badge--calm  { color: var(--ok); font-weight: 600; }

/* storm surge */
.hw-badge--surge-high     { color: var(--sev-extreme);  font-weight: 750; border-width: 2px; }
.hw-badge--surge-elevated { color: var(--sev-moderate); font-weight: 650; }
.hw-badge--surge-normal,
.hw-badge--surge-none     { color: var(--ok); font-weight: 500; }

/* model agreement — panels.js has used both "tight" and "close" for the
   in-agreement case; style both so a rename can't silently unstyle it. */
.hw-badge--spread-wide     { color: var(--err);  font-weight: 700; }
.hw-badge--spread-moderate { color: var(--warn); font-weight: 650; }
.hw-badge--spread-tight,
.hw-badge--spread-close    { color: var(--ok);   font-weight: 600; }
.hw-badge--spread-unknown  { color: var(--idle); font-weight: 500; border-style: dashed; }

/* source status */
.hw-badge--src-ok      { color: var(--ok); font-weight: 500; }
.hw-badge--src-loading { color: var(--accent); font-weight: 500; }
.hw-badge--src-stale   { color: var(--warn); font-weight: 650; }
.hw-badge--src-error   { color: var(--err); font-weight: 700; }

/* -- Saffir-Simpson / classification chip -------------------------------- */
/*
   FILL-ONLY tokens under --cat-ink. This is the standard NHC ramp; Cat 1 is
   #ffe9a8, which is 1.20:1 as text on white. Painted as a background with dark
   ink it is >=5.49:1 on every step, in both themes.

   <span class="hw-cat-chip hw-cat-chip--1" role="img" aria-label="Saffir-Simpson category 1">1</span>
*/
.hw-cat-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.15em 0.5em;
  min-width: 2.2em;
  border-radius: var(--r-sm);
  border: 1px solid rgb(0 0 0 / 0.35);
  background: var(--cat-ptc);
  color: var(--cat-ink);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.4;
  white-space: nowrap;
}
.hw-cat-chip--td  { background: var(--cat-td); }
.hw-cat-chip--ts  { background: var(--cat-ts); }
.hw-cat-chip--ptc { background: var(--cat-ptc); }
.hw-cat-chip--1   { background: var(--cat-1); }
.hw-cat-chip--2   { background: var(--cat-2); }
.hw-cat-chip--3   { background: var(--cat-3); }
.hw-cat-chip--4   { background: var(--cat-4); }
.hw-cat-chip--5   { background: var(--cat-5); }
/* Hero scale. */
.hw-cat-chip--xl {
  font-size: var(--fs-lg);
  padding: 0.25em 0.55em;
  border-radius: var(--r-md);
  min-width: 2.6em;
  box-shadow: var(--shadow-2);
}

/* -- chips: counts and tags, quieter than a badge ------------------------ */

.hw-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35em;
  padding: 2px var(--s-2);
  border-radius: var(--r-pill);
  border: 1px solid var(--border-control);
  background: var(--surface-2);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  line-height: 1.6;
  white-space: nowrap;
}
.hw-chip__glyph { font-size: 0.95em; }
.hw-chip__n { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
.hw-chip__sep { color: var(--border-strong); margin: 0 var(--s-1); }

.hw-chip--warning  { border-color: color-mix(in srgb, var(--kind-warning) 60%, transparent);  color: var(--kind-warning); }
.hw-chip--watch    { border-color: color-mix(in srgb, var(--kind-watch) 60%, transparent);    color: var(--kind-watch); }
.hw-chip--advisory { border-color: color-mix(in srgb, var(--kind-advisory) 60%, transparent); color: var(--kind-advisory); }
.hw-chip--info     { color: var(--text-faint); }
.hw-chip--warning .hw-chip__n,
.hw-chip--watch .hw-chip__n,
.hw-chip--advisory .hw-chip__n { color: inherit; }

.hw-chip--sev-extreme  { border-color: color-mix(in srgb, var(--sev-extreme) 60%, transparent);  color: var(--sev-extreme); }
.hw-chip--sev-severe   { border-color: color-mix(in srgb, var(--sev-severe) 60%, transparent);   color: var(--sev-severe); }
.hw-chip--sev-moderate { border-color: color-mix(in srgb, var(--sev-moderate) 60%, transparent); color: var(--sev-moderate); }
.hw-chip--sev-minor    { border-color: color-mix(in srgb, var(--sev-minor) 60%, transparent);    color: var(--sev-minor); }
.hw-chip--sev-unknown  { color: var(--text-faint); border-style: dashed; }
.hw-chip[class*="hw-chip--sev-"] .hw-chip__n { color: inherit; }

.hw-chip--island { border-color: var(--border); color: var(--text-dim); background: var(--surface-3); }

.hw-chip--src-ok      { border-color: color-mix(in srgb, var(--ok) 55%, transparent); color: var(--ok); }
.hw-chip--src-loading { border-color: var(--accent-line); color: var(--accent); }
.hw-chip--src-stale   { border-color: color-mix(in srgb, var(--warn) 60%, transparent); color: var(--warn); }
.hw-chip--src-error   { border-color: color-mix(in srgb, var(--err) 60%, transparent); color: var(--err); }
.hw-chip[class*="hw-chip--src-"] .hw-chip__n { color: inherit; }

/* ============================================================================
   07  PROGRESSIVE DISCLOSURE  (.hw-disclosure, levels 1-3)
   ------------------------------------------------------------------------
   L1/L2 = supporting detail.  L3 = raw values, model spread, provenance,
   timestamps — deliberately recessive so tier three never competes with two.
   ========================================================================= */

.hw-disclosure {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  overflow: clip;
}
.hw-disclosure + .hw-disclosure { margin-top: var(--s-2); }

.hw-disclosure__summary {
  display: flex;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  flex-wrap: wrap;
  padding: var(--s-3);
  cursor: pointer;
  list-style: none;
  min-width: 0;
  transition: background-color var(--dur-1) var(--ease-out);
}
.hw-disclosure__summary::-webkit-details-marker { display: none; }
.hw-disclosure__summary:hover { background: var(--surface-3); }

/* "more" / "raw" affordance + chevron. panels.js writes the word; CSS adds the
   rotating chevron so the motion reads as "this opened". */
.hw-disclosure__hint {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
  flex: none;
  /* Without this the flex parent shrinks the hint 2px below its content and
     clips the chevron's right edge. */
  min-width: max-content;
}
.hw-disclosure__hint::after {
  content: "";
  width: 6px; height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform var(--dur-2) var(--ease-emphatic);
}
.hw-disclosure[open] > .hw-disclosure__summary > .hw-disclosure__hint { color: var(--accent); }
.hw-disclosure[open] > .hw-disclosure__summary > .hw-disclosure__hint::after {
  transform: rotate(-135deg) translate(-1px, -1px);
}

.hw-disclosure__body {
  padding: var(--s-3);
  border-top: 1px solid var(--border);
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.hw-disclosure__label { font-size: var(--fs-xs); color: var(--text-dim); font-weight: 600; }
.hw-disclosure__sub {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
  font-weight: 400;
}

/* --bare — a disclosure that already sits inside a framed, tinted container,
   so it drops its own card rather than drawing a second box inside the first.
   Requested by panels for the hero's top-warning band, where the band's frame
   and the disclosure's card nested into a visible box-inside-a-box.

   The request estimated ~50px of height recovered; measured after shipping it
   is TWO — warning block 174 -> 172 at 390, 85 -> 83 at 1440. Almost nothing
   was recoverable because the summary's block padding is load-bearing for the
   44px tap target (§18.9) and correctly stays. Recorded because the estimate is
   the sort of thing that gets quoted later: the justification for --bare is the
   nested-card artifact, not height. If you are reaching for it to shorten
   something, it will not.

   This is the only place the card is allowed to go. Everywhere else the card IS
   the affordance — it is what makes a disclosure read as "there is more here"
   against a flat panel body — so --bare is for the case where a frame already
   exists, never for tidying up a busy panel. The summary keeps its full block
   padding: the tap target is 44px on touch (§18.9) and dropping the card must
   not quietly drop that with it. */
.hw-disclosure--bare {
  background: none;
  border: 0;
  border-radius: 0;
  margin: 0;
}
.hw-disclosure--bare > .hw-disclosure__summary { padding-inline: 0; }
.hw-disclosure--bare > .hw-disclosure__body {
  background: none;
  padding-inline: 0;
}

/* Level 3 — raw. Sunken, dashed, mono, quiet. */
.hw-disclosure--l3 { background: var(--surface-sunken); border-style: dashed; }
.hw-disclosure--l3 > .hw-disclosure__summary { padding: var(--s-2) var(--s-3); }
.hw-disclosure--l3 .hw-disclosure__label {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}
.hw-disclosure--l3 > .hw-disclosure__body {
  background: var(--surface-sunken);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-dim);
}

/* Smooth height animation where ::details-content exists; a hard open/close
   everywhere else, which is still perfectly usable. */
@supports selector(details::details-content) {
  .hw-disclosure::details-content {
    block-size: 0;
    overflow: hidden;
    opacity: 0;
    transition: content-visibility var(--dur-3) allow-discrete,
                block-size var(--dur-3) var(--ease-out),
                opacity var(--dur-2) var(--ease-out);
  }
  .hw-disclosure[open]::details-content { block-size: auto; opacity: 1; }
}

/* ============================================================================
   08  DATA PRIMITIVES  (kv, raw, table, metric, note, empty, scroll)
   ========================================================================= */

.hw-kv { margin: 0; display: flex; flex-direction: column; gap: 1px; }
.hw-kv__row {
  display: grid;
  grid-template-columns: minmax(6.5rem, 34%) 1fr;
  gap: var(--s-3);
  padding: var(--s-1) 0;
  align-items: baseline;
}
/* A row carrying a provenance tag is two lines tall, so its value no longer
   sits on the key's baseline — align to the top and give the tag room to
   breathe before the next key. Without this the source line of one row reads
   as belonging to the row beneath it. */
.hw-kv__row--sourced { align-items: start; padding-bottom: var(--s-2); }

.hw-kv__k {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}
.hw-kv__v {
  margin: 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  color: var(--text);
  word-break: break-word;
}

.hw-raw {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}
.hw-raw th, .hw-raw td {
  padding: 3px var(--s-2);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
}
.hw-raw th {
  color: var(--text-faint);
  font-weight: 500;
  white-space: nowrap;
  width: 10rem;
}
.hw-raw td { color: var(--text-dim); word-break: break-word; }
.hw-raw tr:last-child th, .hw-raw tr:last-child td { border-bottom: 0; }

.hw-scroll { overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; }

.hw-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  white-space: nowrap;
}
.hw-table th, .hw-table td {
  padding: var(--s-2) var(--s-3);
  text-align: right;
  border-bottom: 1px solid var(--border);
}
.hw-table th:first-child, .hw-table td:first-child,
.hw-table th[scope="row"] { text-align: left; }
.hw-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-2);
  color: var(--text-faint);
  font-weight: 500;
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  white-space: normal;
}
.hw-table td { color: var(--text-dim); }
.hw-table tbody th, .hw-table tbody td:first-child { color: var(--text); font-weight: 600; }
.hw-table tbody tr:last-child td { border-bottom: 0; }
.hw-table tbody tr:hover td { background: var(--surface-2); }
.hw-table--dense th, .hw-table--dense td { padding: var(--s-1) var(--s-2); font-size: var(--fs-2xs); }

/* The one number that matters, at three sizes. */
.hw-metric { min-width: 0; }
.hw-metric__label {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
  margin-bottom: 2px;
}
.hw-metric__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-lg);
  font-weight: 650;
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}
.hw-metric__value strong { font-weight: 700; }
.hw-metric__unit { font-size: 0.58em; font-weight: 500; color: var(--text-dim); margin-left: 0.1em; }
.hw-metric__note {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
  margin-top: 2px;
  line-height: 1.4;
}

.hw-note {
  margin: 0;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: var(--fs-xs);
  line-height: var(--lh-snug);
  color: var(--text-dim);
}
.hw-note--demo {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  color: var(--text);
}
.hw-note code { font-size: var(--fs-2xs); color: var(--accent); }

.hw-empty {
  margin: 0;
  padding: var(--s-5) var(--s-3);
  text-align: center;
  color: var(--text-faint);
  font-size: var(--fs-sm);
}

/* ============================================================================
   10  ALERTS  (#alerts-panel)
   ========================================================================= */

.hw-alerts__summary-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}

.hw-alerts__filter {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.hw-alerts__filter-label {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}
.hw-alerts__filter-select {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-control);
  border-radius: var(--r-xs);
  padding: var(--s-1) var(--s-2);
  font-size: var(--fs-xs);
  min-height: 30px;
  max-width: 100%;
}
.hw-alerts__filter-note {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
}

.hw-alerts__tier { display: flex; flex-direction: column; gap: var(--s-2); }
.hw-alerts__tier-title {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  font-size: var(--fs-xs);
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text);
  padding-bottom: var(--s-1);
  border-bottom: 1px solid var(--border);
}
.hw-alerts__tier--rest .hw-alerts__tier-title {
  color: var(--text-faint);
  font-weight: 500;
  margin-top: var(--s-2);
}
/* The tropical tier outranks everything on the page, so its heading is the
   only one that carries the extreme hue plus a rule of its own. */
.hw-alerts__tier--top > .hw-alerts__tier-title {
  color: var(--sev-extreme);
  font-weight: 750;
  border-bottom-color: color-mix(in srgb, var(--sev-extreme) 45%, var(--border));
}
.hw-alerts__tier--top > .hw-alerts__tier-title::before { content: "◆ "; }

/* Overflow drawer: "N more products". Capped so a 50-alert day can't push the
   rest of the panel off the screen. */
.hw-alert__more {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  max-height: 60vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

/* Compact zone list inside a grouped card — many short names, so it reads as a
   block of metadata rather than prose. */
.hw-alert__zones {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  line-height: 1.65;
  color: var(--text-dim);
  word-break: break-word;
}

/* -- alert card: a .hw-disclosure carrying severity + kind ---------------- */

.hw-alert {
  border-inline-start-width: 2px;
  border-inline-start-style: dashed;
  border-inline-start-color: var(--sev-unknown);
  background: var(--surface-2);
}

.hw-alert__head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  min-width: 0;
  flex: 1 1 100%;
}
.hw-alert__event {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text);
  line-height: var(--lh-snug);
}
.hw-alert__areas {
  flex: 1 1 100%;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
  line-height: 1.45;
}
.hw-alert__until {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
  white-space: nowrap;
}
.hw-alert__until--soon { color: var(--warn); font-weight: 650; }
.hw-alert__until--soon::before { content: "⏳ "; }

.hw-alert__headline {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--text);
}
.hw-alert__islands { display: flex; flex-wrap: wrap; gap: var(--s-1); }

/* "What to do" — never dimmed, never behind another disclosure. */
.hw-alert__instruction {
  padding: var(--s-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
}
.hw-alert__subhead {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
  font-weight: 500;
  margin-bottom: var(--s-1);
}
.hw-alert__text {
  margin: 0;
  font-size: var(--fs-xs);
  line-height: var(--lh-body);
  color: var(--text);
  white-space: pre-line;
}
.hw-alert__raw {
  margin: 0;
  font-size: var(--fs-2xs);
  line-height: 1.5;
  color: var(--text-dim);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 24rem;
  overflow: auto;
  scrollbar-width: thin;
}

/* Product kind is a SECOND axis, independent of CAP severity: a Hurricane
   Warning and a High Surf Advisory can both be "Severe". Severity owns the rail
   and the wash (below); kind owns only the small badge tint on the summary row,
   so the two never fight for the same channel. */
.hw-alert--warning  > .hw-disclosure__summary { box-shadow: inset 0 2px 0 color-mix(in srgb, var(--kind-warning) 55%, transparent); }
.hw-alert--watch    > .hw-disclosure__summary { box-shadow: inset 0 2px 0 color-mix(in srgb, var(--kind-watch) 55%, transparent); }
.hw-alert--advisory > .hw-disclosure__summary { box-shadow: inset 0 2px 0 color-mix(in srgb, var(--kind-advisory) 45%, transparent); }
.hw-alert--info     > .hw-disclosure__summary { box-shadow: none; }

/* severity: hue + rail width + wash + weight, all at once */
/* Unknown restates the base treatment rather than inheriting it silently.
   panels.js emits hw-alert--sev-unknown for real, so leaving it ruleless makes
   it read as a gap to every audit and to the next person in this file. The
   dashed 2px rail IS the intended treatment for "severity not stated". */
.hw-alert--sev-unknown  { border-inline-start: 2px dashed var(--sev-unknown); }
.hw-alert--sev-minor    { border-inline-start: 3px solid var(--sev-minor); }
.hw-alert--sev-moderate { border-inline-start: 4px solid var(--sev-moderate); }
.hw-alert--sev-severe   { border-inline-start: 5px solid var(--sev-severe); }
.hw-alert--sev-extreme  { border-inline-start: 6px solid var(--sev-extreme); }

.hw-alert--sev-minor    { background: color-mix(in srgb, var(--sev-minor) var(--sev-tint), var(--surface)); }
.hw-alert--sev-moderate { background: color-mix(in srgb, var(--sev-moderate) var(--sev-tint), var(--surface)); }
.hw-alert--sev-severe   { background: color-mix(in srgb, var(--sev-severe) var(--sev-tint), var(--surface)); }
.hw-alert--sev-extreme  { background: color-mix(in srgb, var(--sev-extreme) var(--sev-tint), var(--surface)); }

.hw-alert--sev-moderate .hw-alert__event { font-weight: 600; }
.hw-alert--sev-severe   .hw-alert__event { font-weight: 700; }
.hw-alert--sev-extreme  .hw-alert__event { font-weight: 750; font-size: var(--fs-base); }

/* Tropical warnings outrank everything regardless of the CAP severity field. */
.hw-alert--prominent {
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--sev-extreme) 45%, transparent), var(--shadow-1);
}
/* A hazard texture on the rail, so top-tier survives greyscale and print. */
.hw-alert--prominent.hw-alert--sev-extreme {
  border-inline-start: 0;
  padding-inline-start: 6px;
  background-image: linear-gradient(90deg,
      var(--sev-extreme) 0 3px,
      color-mix(in srgb, var(--sev-extreme) 45%, #000) 3px 6px,
      transparent 6px);
  background-repeat: repeat-y;
  background-size: 6px 8px;
}

/* -- grouped "other products" ------------------------------------------- */

.hw-alert-group { background: var(--surface-2); }
.hw-alert-group__head { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; min-width: 0; }
.hw-alert-group__event { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.hw-alert-group__count {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: var(--surface-3);
  border-radius: var(--r-pill);
  padding: 0 var(--s-2);
}
.hw-alert-group__areas,
.hw-alert-group__expiry {
  flex: 1 1 100%;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
}

/* ============================================================================
   11  ISLAND CARDS  (#islands-panel)
   ========================================================================= */

.hw-islands__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 268px), 1fr));
  gap: var(--s-3);
}
.hw-islands__grid > .hw-disclosure { margin-top: 0; }

.hw-island__head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  flex: 1 1 100%;
  min-width: 0;
}
.hw-island__name {
  font-size: var(--fs-sm);
  font-weight: 650;
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}
.hw-island__alert { display: inline-flex; align-items: center; gap: var(--s-1); margin-left: auto; }
.hw-island__alert-text { font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-faint); }
.hw-island__alert--none {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}

.hw-island__now {
  display: flex;
  align-items: baseline;
  gap: var(--s-2) var(--s-3);
  flex-wrap: wrap;
  flex: 1 1 100%;
}
.hw-island__temp {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xl);
  line-height: 1;
  color: var(--text);
  font-weight: 500;
}
.hw-island__temp strong { font-weight: 700; }
.hw-island__wind {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  color: var(--text-dim);
}
.hw-island__gust { color: var(--warn); }
.hw-island__sky { font-size: var(--fs-xs); color: var(--text-faint); }

.hw-island__subhead {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
  font-weight: 500;
  margin-bottom: var(--s-1);
}
.hw-island__sparks { display: flex; flex-direction: column; gap: var(--s-3); }
.hw-island__alert-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  font-size: var(--fs-xs);
}
.hw-island__alert-area { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-faint); }

/* ============================================================================
   12  MODEL COMPARISON + CHARTS  (#models-panel)
   ------------------------------------------------------------------------
   The point of this panel is DISAGREEMENT: the verdict is the loudest element
   and the per-agency rows read as the evidence behind it.
   ========================================================================= */

/* -- sparkline (islands) ------------------------------------------------- */

.hw-spark { display: block; width: 100%; height: auto; overflow: visible; }
.hw-spark--empty { color: var(--text-faint); font-family: var(--font-mono); font-size: var(--fs-2xs); }
.hw-spark__band { fill: color-mix(in srgb, var(--accent) 22%, transparent); stroke: none; }
.hw-spark__line { stroke: var(--accent); stroke-width: 1.5; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.hw-spark__dot { fill: var(--accent); }
.hw-spark__row { display: flex; flex-direction: column; gap: var(--s-1); }
.hw-spark__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}
.hw-spark__range { text-transform: none; letter-spacing: 0; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.hw-spark__caption { margin: 0; font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-faint); }

/* -- envelope chart (six agencies) --------------------------------------- */

.hw-envelope { display: block; width: 100%; height: auto; overflow: visible; }
.hw-envelope__band { fill: color-mix(in srgb, var(--text-faint) 18%, transparent); stroke: none; }
.hw-envelope__grid { stroke: var(--border); stroke-width: 1; }
.hw-envelope__mean { stroke: var(--text); stroke-width: 1.5; stroke-dasharray: 4 3; fill: none; }
.hw-envelope__mark { stroke: var(--err); stroke-width: 1.5; stroke-dasharray: 2 2; }
.hw-envelope__ylabel,
.hw-envelope__xlabel {
  fill: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 9px;
}
.hw-model-line {
  stroke: var(--hw-model-color, currentColor);
  stroke-width: 1.75;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* -- variable switcher --------------------------------------------------- */

.hw-models__vars { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.hw-models__var {
  padding: var(--s-1) var(--s-3);
  min-height: 30px;
  background: var(--surface-2);
  color: var(--text-dim);
  border: 1px solid var(--border-control);
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.hw-models__var:hover { color: var(--text); border-color: var(--accent); }
.hw-models__var--on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

/* -- verdict ------------------------------------------------------------- */

.hw-models__verdict {
  padding: var(--s-3);
  border: 1px solid var(--border);
  border-inline-start: 4px solid var(--idle);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.hw-models__verdict--wide     { border-inline-start-color: var(--err);  background: color-mix(in srgb, var(--err) 10%, var(--surface)); }
.hw-models__verdict--moderate { border-inline-start-color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, var(--surface)); }
.hw-models__verdict--tight,
.hw-models__verdict--close    { border-inline-start-color: var(--ok);   background: color-mix(in srgb, var(--ok) 10%, var(--surface)); }
.hw-models__verdict--unknown  { border-inline-start-style: dashed; }
.hw-models__verdict-text {
  margin: var(--s-2) 0 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--text);
  max-width: 70ch;
}

/* -- chart block --------------------------------------------------------- */

.hw-models__chart { display: flex; flex-direction: column; gap: var(--s-2); }
.hw-models__chart-title {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}
.hw-models__chart-unit { color: var(--text-dim); }
.hw-models__chart-key { margin: 0; font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-faint); }

/* -- legend + table ------------------------------------------------------ */

.hw-model-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
  gap: var(--s-1) var(--s-3);
}
.hw-model-legend__item {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  font-size: var(--fs-xs);
  min-width: 0;
}
.hw-model-legend__swatch {
  width: 14px;
  height: 3px;
  border-radius: 2px;
  flex: none;
  align-self: center;
  background: var(--hw-model-color, var(--idle));
}
.hw-model-legend__name { color: var(--text); min-width: 0; }
.hw-model-legend__country {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}
.hw-model-legend__val {
  margin-left: auto;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 650;
  color: var(--text);
  white-space: nowrap;
}

.hw-model-table__country {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
  margin-left: 0.4em;
  font-weight: 400;
}
.hw-model-table__peak {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--err);
  font-weight: 700;
}
.hw-model-table__mark { color: var(--err); font-weight: 700; }
.hw-model-table__mean td,
.hw-model-table__mean th { border-top: 1px solid var(--border-strong); color: var(--text); font-weight: 650; }
.hw-model-table__spread td,
.hw-model-table__spread th { color: var(--text); font-weight: 700; }
.hw-model-table__row th[scope="row"] { white-space: nowrap; }

/* SVG line colors flow from the same six tokens the legend swatches use. */
.hw-model-line--1 { --hw-model-color: var(--hw-model-1); }
.hw-model-line--2 { --hw-model-color: var(--hw-model-2); }
.hw-model-line--3 { --hw-model-color: var(--hw-model-3); }
.hw-model-line--4 { --hw-model-color: var(--hw-model-4); }
.hw-model-line--5 { --hw-model-color: var(--hw-model-5); }
.hw-model-line--6 { --hw-model-color: var(--hw-model-6); }

/* ============================================================================
   13  MARINE — waves, tides, computed storm surge  (#marine-panel)
   ========================================================================= */

.hw-waves { display: flex; flex-direction: column; gap: var(--s-3); }
.hw-waves__lead { margin: 0; font-size: var(--fs-sm); color: var(--text-dim); max-width: 68ch; }

/* Observed minus predicted — the number that matters during a hurricane. */
.hw-surge {
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border);
  border-inline-start: 4px solid var(--ok);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.hw-surge__head {
  display: flex;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  flex-wrap: wrap;
  margin-bottom: var(--s-2);
}
.hw-surge__station {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
}
.hw-surge__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-2xl);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}
.hw-surge__unit { font-size: 0.42em; font-weight: 500; color: var(--text-dim); margin-left: 0.15em; }
.hw-surge__words { margin: var(--s-2) 0 0; font-size: var(--fs-sm); line-height: var(--lh-body); color: var(--text); max-width: 68ch; }
.hw-surge__formula {
  margin: var(--s-2) 0 0;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
}
.hw-surge__inline { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 650; }

.hw-surge--elevated {
  border-inline-start-color: var(--sev-moderate);
  background: color-mix(in srgb, var(--sev-moderate) var(--sev-tint), var(--surface));
}
.hw-surge--elevated .hw-surge__value { color: var(--sev-moderate); }
.hw-surge--high {
  border-inline-start-color: var(--sev-extreme);
  border-inline-start-width: 6px;
  background: color-mix(in srgb, var(--sev-extreme) var(--sev-tint), var(--surface));
}
.hw-surge--high .hw-surge__value { color: var(--sev-extreme); }
.hw-surge--normal { border-inline-start-color: var(--ok); }
.hw-surge--none { border-inline-start-color: var(--border-strong); border-inline-start-style: dashed; }

.hw-tides__surge { font-weight: 700; color: var(--text); }

/* -- 13b. observed instruments (#marine-panel) ---------------------------
   The only *measured* data in the app — every other number on the page is model
   output. That distinction is worth a real signal, so this reads as a band
   rather than another card: full-bleed inside the panel body, its own surface,
   and a heavy top rule.

   Structural, not chromatic, on purpose. A sixth semantic hue would have to
   compete with the severity ramp for attention, and "this is trustworthy" must
   never out-shout "this is dangerous".

   <div class="hw-observed">
     <h3 class="hw-island__subhead">…</h3>
     <p class="hw-note">…</p>
     <div class="hw-observed__metrics">  .hw-metric x3  </div>
     <p class="hw-observed__lead">…</p>
     <details class="hw-disclosure">…</details> x2
   </div>
*/
.hw-observed {
  /* cancel .hw-panel__body's padding so the band runs edge to edge */
  margin-inline: calc(var(--s-4) * -1);
  padding: var(--s-4);
  background: var(--surface-2);
  border-top: 2px solid var(--border-strong);
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
/* Last child of the body: drop the bottom rule, the panel's own edge does it. */
.hw-panel__body > .hw-observed:last-child {
  margin-bottom: calc(var(--s-4) * -1);
  border-bottom: 0;
}

/* Three peer headline numbers. Deliberately NOT .hw-hero__metrics: that grid
   promotes its first child to hero scale, and these three are equals. */
.hw-observed__metrics {
  display: grid;
  /* Explicit 3, not auto-fit: the marine panel sits in a ~411px grid column, so
     auto-fit resolves to 2 and orphans the third metric on its own row. Three
     peers read as three peers, or as a single stack — never 2 + 1. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-2);
}
@media (max-width: 560px) {
  .hw-observed__metrics { grid-template-columns: 1fr; }
}
.hw-observed__metrics > .hw-metric {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-3);
}

/* The finding, in words — the most important sentence in the panel. Measure is
   capped for readability; the <strong> figures get tabular numerals so numbers
   inside running prose line up with the metric boxes above. */
.hw-observed__lead {
  margin: 0;
  max-width: 62ch;
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  color: var(--text);
}
.hw-observed__lead strong {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

@media (max-width: 520px) {
  .hw-observed { padding-inline: var(--s-3); }
  .hw-observed__lead { font-size: var(--fs-base); }
}

/* ============================================================================
   14  SOURCES / PROVENANCE  (#sources-panel)
   ========================================================================= */

.hw-sources__strip { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; }

.hw-source__head {
  display: flex;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  flex-wrap: wrap;
  min-width: 0;
  flex: 1 1 auto;
}
.hw-source__label { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.hw-source__age {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  white-space: nowrap;
}
.hw-source__err { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--err); }
.hw-source__error-block {
  border: 1px solid color-mix(in srgb, var(--err) 35%, transparent);
  background: color-mix(in srgb, var(--err) 8%, var(--surface));
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
}
.hw-source__subhead {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-faint);
  font-weight: 500;
  margin-bottom: var(--s-1);
}
.hw-source__error-pre {
  margin: 0;
  font-size: var(--fs-2xs);
  color: var(--text-dim);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 12rem;
  overflow: auto;
}
.hw-sources__trust {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--text-dim);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
  max-width: 78ch;
}
.hw-sources__trust--degraded {
  border-color: color-mix(in srgb, var(--warn) 50%, var(--border));
  background: color-mix(in srgb, var(--warn) 10%, var(--surface));
  color: var(--text);
}

/* ============================================================================
   16  IMAGERY PLAYER  (#imagery-panel → .hw-imagery)
   ========================================================================= */

.hw-imagery { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }

.hw-imagery__bands { display: flex; gap: var(--s-1); flex-wrap: wrap; }
.hw-imagery__band,
.hw-imagery__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-3);
  min-height: 30px;
  background: var(--surface-2);
  color: var(--text-dim);
  border: 1px solid var(--border-control);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.hw-imagery__band:hover,
.hw-imagery__btn:hover { background: var(--surface-3); color: var(--text); border-color: var(--accent); }
.hw-imagery__band[aria-pressed="true"],
.hw-imagery__btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.hw-imagery__btn--play { min-width: 68px; background: var(--accent); color: var(--text-on-accent); border-color: var(--accent); }
.hw-imagery__btn--play:hover { background: color-mix(in srgb, var(--accent) 86%, var(--text)); color: var(--text-on-accent); }
.hw-imagery__btn--rate { min-width: 42px; }

.hw-imagery__stage {
  position: relative;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface-sunken);
  overflow: hidden;
  display: grid;
  place-items: center;
  min-height: 220px;
}
.hw-imagery__canvas { display: block; width: 100%; height: auto; }

.hw-imagery__loading,
.hw-imagery__error {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  gap: var(--s-2);
  justify-items: center;
  padding: var(--s-4);
  text-align: center;
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--text-faint);
}
.hw-imagery__error { color: var(--text-dim); }
.hw-imagery__error-text { margin: 0; font-size: var(--fs-xs); max-width: 44ch; }
.hw-imagery__error[hidden],
.hw-imagery__loading[hidden] { display: none; }

.hw-imagery__stamp {
  position: absolute;
  left: var(--s-2);
  bottom: var(--s-2);
  padding: 2px var(--s-2);
  border-radius: var(--r-sm);
  background: var(--overlay-scrim);
  color: var(--overlay-ink);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.hw-imagery__controls {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
}
.hw-imagery__scrub { flex: 1 1 140px; min-width: 100px; }
.hw-imagery__rate { display: inline-flex; gap: var(--s-1); }

.hw-imagery__progress {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  white-space: nowrap;
}
.hw-imagery__progress-track {
  flex: 1 1 80px;
  height: 4px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  overflow: hidden;
}
.hw-imagery__bar { height: 100%; background: var(--accent); transition: width var(--dur-2) var(--ease-out); }

.hw-imagery__meta { margin: 0; font-family: var(--font-mono); font-size: var(--fs-3xs); color: var(--text-faint); }

/* -- the loop scrubber's range input -------------------------------------- *
 * Was shared with the old timeline's input.hw-motion__scrub; that class is no
 * longer emitted (motion.js renders hw-scrub, styled in css/motion.css). */

.hw-imagery__scrub {
  appearance: none;
  height: 22px;
  background: transparent;
  cursor: pointer;
}
.hw-imagery__scrub::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  border: 1px solid var(--border);
}
.hw-imagery__scrub::-moz-range-track {
  height: 5px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  border: 1px solid var(--border);
}
.hw-imagery__scrub::-webkit-slider-thumb {
  appearance: none;
  width: 15px; height: 15px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
  box-shadow: var(--shadow-1);
}
.hw-imagery__scrub::-moz-range-thumb {
  width: 15px; height: 15px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
}

/* ============================================================================
   17  MAP  (#map — map.js; new map rules live in css/map.css)
   ------------------------------------------------------------------------
   Pre-redesign hw-map__* rules still emitted by map.js. The #map-controls
   block and .map-shell were retired with their markup (the layer controls are
   now .hw-chipbtn chips in #map-head, §04.7). #map-tile's placement is §04.10;
   its internals belong to map.css, which loads after this file.
   ========================================================================= */

#map {
  height: var(--map-h);
  width: 100%;
  background: var(--surface-sunken);
  z-index: 0;               /* keeps Leaflet panes under the sticky header */
  color: var(--text);
}
#map.hw-map__root { min-height: 0; background: var(--surface-sunken); }

/* Static fallback: present in the HTML, so it shows even if map.js never runs
   at all. map.js clears #map on boot and renders its own richer message on a
   Leaflet failure. */
.map-boot-fallback {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  align-items: flex-start;
  justify-content: center;
  height: 100%;
  padding: var(--s-6) var(--s-5);
  font-size: var(--fs-sm);
  color: var(--text-dim);
}
#map .hw-map__fallback {
  height: 100%;
  padding: var(--s-6) var(--s-5);
  font-size: var(--fs-sm);
  color: var(--text-dim);
}
#map .hw-map__fallback-title { font-size: var(--fs-md); font-weight: 650; color: var(--text); }
#map .hw-map__fallback-detail { color: var(--text-faint); max-width: 62ch; }

/* The animated wind-particle canvas (a BlanketOverlay subclass). It covers the
   full map surface, so it MUST NOT eat pointer events or the map stops being
   draggable — map.js sets pointer-events inline too; this is the belt to that
   pair of braces. Sits above tiles/overlays but below markers and popups.
   Opacity keeps it a texture behind the storm geometry, never competing with
   the cone or the track. */
.hw-map__windfield {
  position: absolute;
  inset: 0;
  /* No z-index here on purpose: BlanketOverlay puts this canvas in a Leaflet
     pane and Leaflet owns pane stacking. A z-index of ours would just lose to
     the pane's, and a rule that silently loses is worse than no rule. */
  pointer-events: none !important;
  opacity: 0.85;
  mix-blend-mode: plus-lighter;
}
:root[data-theme="light"] .hw-map__windfield { opacity: 0.7; mix-blend-mode: multiply; }
@media (prefers-reduced-motion: reduce) { .hw-map__windfield { opacity: 0.5; } }

/* Light basemap: no tile dimming, and the map surface matches the panel it
   sits in rather than the dark sunken well. */
.hw-map__root--light { background: var(--surface-2); }

/* Buoy / shore-station markers. A DivIcon, so unlike the cone and the track
   this IS a real DOM element and a real CSS target — the glyph inside is
   inline-styled SVG from map.js, but the box around it is ours.

   Leaflet's DivIcon default paints a white square with a border; the passed
   className replaces `leaflet-div-icon`, but resetting is cheap insurance. */
.hw-map__buoy {
  background: none;
  border: 0;
}
/* map.js sets `keyboard: true`, so these markers are tabbable. Without a
   visible focus ring a keyboard user cannot tell which station they are on —
   Leaflet draws none of its own for DivIcon markers. */
/* map.js grew these DivIcons to a padded transparent touch box around an
   unchanged glyph, so this outline now traces the TARGET, not the mark. That is
   the honest thing to show a keyboard user — but `border-radius: 50%` on a 44px
   box around a 16px station drew a 50px accent CIRCLE on a storm map.

   That is not merely ambiguous, it is a collision: map.js already emits
   .hw-map__island-ring, a dashed --accent circle around the island selected in
   uistate, on the same basemap two panes below. A circular accent focus ring
   would have been pixel-for-pixel the same object as a real map annotation. A
   rounded rectangle says "focused control" and cannot be read as cartography.
   Confirmed with map before changing it — do not put the 50% back. */
.hw-map__buoy:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-md);
}
/* Wave buoy vs shore station: the shape difference is drawn in the SVG, these
   are hooks for querying and for anything the glyph can't carry. */
.hw-map__buoy--wave, .hw-map__buoy--cman { }

/* One-line observed readout on hover/focus. Inherits the themed .leaflet-tooltip
   surface; this only tightens the type, since it is pure instrument data. */
.hw-map__buoy-tip {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs) !important;
  font-variant-numeric: tabular-nums;
  padding: 2px var(--s-2);
  white-space: nowrap;
}

/* Floating labels drawn on the map itself. Same reasoning as .hw-imagery__stamp:
   these sit on raster tiles, not on a surface, so they do not follow the theme.
   The halo is the scrim here — there is no box to fill behind a floating label. */
.hw-map__marker-label {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  color: var(--overlay-ink);
  text-shadow: 0 0 3px var(--overlay-halo), 0 0 6px var(--overlay-halo);
  white-space: nowrap;
}
/* --- Tropical weather outlook (GTWO) ------------------------------------
   Areas NHC is watching for possible development. Styled to be unmistakably
   NOT a forecast: the only dotted shapes on the map, the lightest fill, and
   the bottom-most hazard pane. See renderOutlook() in js/map.js. Stroke and
   fill colours are set from --sev-* tokens as SVG attributes by map.js, so
   they flip with the theme; only what cannot be expressed there lives here. */
.hw-map__outlook { pointer-events: fill; }
.hw-map__outlook-icon { background: none; border: 0; }
/* The keyboard-reachable ring gets the same focus treatment as every other
   map target — it is a real tab stop, not decoration. */
.hw-map__outlook-icon:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 50%;
}
/* "(nearest edge of the area)" — the qualifier on the distance. Recessive
   because the number is the message, present because the number is measured
   from somewhere the reader would not otherwise guess. */
.hw-map__outlook-note { color: var(--text-faint); white-space: nowrap; }

/* --- Model track guidance ("spaghetti") ---------------------------------
   The 31 ensemble members are drawn as one recessive hairline colour on
   purpose: their WIDTH is the message, not their identity. Only the named
   models get ink, a casing and an end-of-track chip. See renderGuidance()
   in js/map.js for why. Stroke colour comes from the design tokens via
   GUIDANCE_STYLE, so it flips with the theme like everything else. */
.hw-map__guidance { pointer-events: stroke; }
.hw-map__guidance--ensemble { pointer-events: none; }
/* Hover/keyboard focus on a track. The stroke colour and width are already set
   from tokens as SVG attributes by map.js, so this adds glow and nothing else —
   the state is fully legible without it, which is why it is a filter and not a
   colour change. --accent is not a severity or category hue, so "this is the
   one under your pointer" cannot read as "this one is worse". */
.hw-map__guidance--hot {
  filter: drop-shadow(0 0 3px color-mix(in srgb, var(--accent) 55%, transparent));
}

/* The "showing one forecast only" bar. NOT in #map-controls — map.js mounts it
   as a Leaflet Control at the map's top right, so it lives inside #map and
   inherits .leaflet-control. Its children are .hw-chip and .btn, which already
   carry themed surfaces, so it deliberately has no background of its own.
   The chip is clamped in vw because the track label is arbitrary-length model
   text ("GEFS member 14") and a long one would otherwise push the clear button
   off the map on a phone. */
#map .hw-map__isolate {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
#map .hw-map__isolate .hw-chip {
  max-width: 46vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
#map .hw-map__isolate .btn { min-height: 28px; padding-inline: var(--s-3); }
/* On touch the clear button is the only way out of an isolated track, so it
   takes the 44px floor even though it sits on top of the map. */
@media (max-width: 719px), (pointer: coarse) {
  #map .hw-map__isolate .btn { min-height: 44px; }
}
.hw-map__guidance-casing { pointer-events: none; }

/* Wrapper is a zero-size anchor at the end of the track; the chip hangs off
   it, so Leaflet never has to know how wide a model name is. */
.hw-map__guidance-chip-wrap { overflow: visible; background: none; border: 0; }

/* The chip is the click target for its track. The label pane is
   pointer-events:none so buoy labels never swallow map drags — the chip opts
   itself back in, and only itself. */
.hw-map__guidance-chip {
  pointer-events: auto;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  padding: 1px 5px;
  border: 1px solid currentColor;
  border-radius: 3px;
  /* Opaque, not translucent: the chip's whole job is to stay readable on top
     of the densest part of the plot, which is exactly where tracks converge. */
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.45;
  letter-spacing: .02em;
  white-space: nowrap;
  box-shadow: 0 0 0 1px var(--surface), 0 1px 3px rgb(0 0 0 / .45);
}
/* The interpolated marker. A tilde rather than a word, because the popup
   spells it out and the chip has no room to. */
.hw-map__guidance-chip i { font-style: normal; opacity: .65; margin-left: 2px; }

/* Hover and keyboard focus both have to read, because the chip is a control.
   The border is already the model's colour, so the affordance is weight and a
   ring rather than a hue that would collide with the track it names. */
.hw-map__guidance-chip:hover { border-width: 2px; padding: 0 4px; }

/* A fingertip is ~9 mm across and the chip is 19 px tall at a 20 px pitch, so
   one touch covered two or three names with no way to tell which would open.
   32 px is the target; js/map.js widens the stacking pitch to match (see
   CHIP_ROW_PX_COARSE) and pours whatever no longer fits the frame into one
   "+n more" chip, because taller chips mean fewer rows. */
@media (pointer: coarse) {
  .hw-map__guidance-chip { min-block-size: 32px; padding-inline: 8px; font-size: 11px; }
  .hw-map__guidance-chip:hover { padding-inline: 7px; }
}

/* The overflow chip's list. Quieter than a model's own chip — it names no
   single forecast — and its rows are the shared chipbtn, so selecting one
   isolates that track exactly as its own chip would have. */
.hw-map__guidance-chip--more { font-family: var(--font-ui); letter-spacing: 0; }
.hw-map__guidance-more {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 0;
  padding: 0;
  list-style: none;
}
.hw-map__guidance-more__item { display: inline-flex; align-items: center; gap: 4px; }
.hw-map__guidance-more__kind { font-size: var(--fs-2xs); color: var(--text-faint); }
@media (pointer: coarse) {
  .hw-map__guidance-more .hw-chipbtn { min-block-size: 44px; }
}
.hw-map__guidance-chip-wrap:focus-visible { outline: none; }
.hw-map__guidance-chip-wrap:focus-visible .hw-map__guidance-chip {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* When there is no room above the tag, the popup flips below it — and the tip,
   which points down out of the popup's bottom edge, would then point at empty
   ocean instead of at the thing it describes. */
.hw-map__popup-wrap--flipped .leaflet-popup-tip-container { display: none; }

/* Leader back to the point on the track the chip belongs to. Models that agree
   end in the same place, so their chips get pushed apart to stay legible — and
   without this the name and the line it names are no longer connected. */
.hw-map__guidance-leader { opacity: .55; pointer-events: none; }

.hw-map__popup { font-size: var(--fs-xs); line-height: var(--lh-snug); min-width: 12rem; }
.hw-map__popup-badge {
  display: inline-block;
  padding: 1px var(--s-2);
  border-radius: var(--r-xs);
  background: var(--surface-3);
  border: 1px solid var(--border-control);
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-dim);
  margin-bottom: var(--s-1);
}

/* -- Leaflet chrome, themed --------------------------------------------- */

.leaflet-container { font-family: var(--font-ui); background: var(--surface-sunken); }
.leaflet-control-attribution {
  background: color-mix(in srgb, var(--surface) 85%, transparent) !important;
  color: var(--text-faint) !important;
  font-size: 10px !important;
}
.leaflet-control-attribution a { color: var(--text-dim) !important; }
.leaflet-bar, .leaflet-control-zoom a, .leaflet-control-layers {
  background: var(--surface-2) !important;
  color: var(--text) !important;
  border-color: var(--border) !important;
}
.leaflet-control-zoom a:hover { background: var(--surface-3) !important; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
  background: var(--surface) !important;
  color: var(--text) !important;
  box-shadow: var(--shadow-3) !important;
  border: 1px solid var(--border);
}
.leaflet-popup-content { font-size: var(--fs-xs); margin: var(--s-3); }
.leaflet-popup-close-button { color: var(--text-faint) !important; }
.leaflet-tooltip {
  background: var(--surface) !important;
  color: var(--text) !important;
  border: 1px solid var(--border-strong) !important;
  box-shadow: var(--shadow-2) !important;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs) !important;
}
/* REMOVED: `.hw-map__root--dark .leaflet-tile-pane { filter: brightness(.78)… }`.
   It was tuned for the CARTO light raster, which the redesign replaced with
   Esri's theme-matched Dark Gray / Light Gray bases (CONTRACTS §0) — a dark
   base needs no dimming, and dimming it again sank the islands. Any basemap
   treatment now belongs to map.css. */

/* ============================================================================
   18  TOUCH AND ANCHORS
   ------------------------------------------------------------------------
   The previous wave's hierarchy ladder (tiers 0-4 over .region-*, .map-shell,
   #map-controls) is retired with the markup it described: the tile system in
   §04 carries one elevation for every tile, and the three severity spends
   (dot, verdict rail, primary CTA) carry the emphasis. What survives here is
   the part that was never about that layout: touch targets, and the
   selected-storm ring on the map.
   ========================================================================= */

html { scroll-behavior: smooth; }

/* -- 18.1 the storm currently driving the dashboard --------------------- *
 * Emitted by map.js on the selected system's marker. --accent is deliberately
 * no severity or category hue, so "you are looking at this one" can never be
 * misread as "this one is worse". */
.hw-map__marker--current {
  filter: drop-shadow(0 0 0 var(--accent)) drop-shadow(0 0 6px var(--accent));
}
.hw-map__marker--current:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* -- 18.2 touch ---------------------------------------------------------- *
 * A Hawaii resident opens this standing up, one-handed, possibly while
 * packing: every control is at least 44px on its short axis under a coarse
 * pointer. Width, not just height — a 44px-tall 28px-wide button is a miss.
 *
 * The redesign's controls keep their DRAWN size (a 28px chevron circle, a
 * 36px theme button, a slim segmented pill — the spec's geometry) and grow an
 * invisible hit area around it: transparent borders plus
 * background-clip:padding-box, and a negative margin so the row they sit in
 * does not grow. What the eye sees is the design; what the thumb hits is 44.
 *
 * Inline links inside running prose are deliberately NOT enlarged — WCAG
 * exempts them, and padding them out fractures the sentence's line box.
 *
 * HOW TO VERIFY THE COARSE BRANCH: `Emulation.setTouchEmulationEnabled
 * ({enabled: true, maxTouchPoints: 5})`. setDeviceMetricsOverride({mobile})
 * does NOT make (pointer: coarse) match, and setEmulatedMedia silently ignores
 * `pointer`; both leave the desktop rules in force while the report says
 * "phone". Assert matchMedia('(pointer: coarse)').matches in the page. */
@media (pointer: coarse) {
  .hw-tile__toggle {
    width: 44px;
    height: 44px;
    margin: -8px;
    border: 8px solid transparent;
    background-clip: padding-box;
  }
  /* margin-block only: an inline negative margin here overrode the header's
     `margin-inline-start: auto` and dragged the theme button off the right
     edge at 390 (measured). The extra 8px of width is paid by the row. */
  .hw-iconbtn {
    width: 44px;
    height: 44px;
    margin-block: -4px;
    border: 4px solid transparent;
    background-clip: padding-box;
  }
  /* The track loses its padding and each button carries its own 4px of
     transparent border instead: same drawn pill, 44px targets. The selected
     segment's shadow is dropped because it would draw at the 44px edge. */
  .hw-seg { padding: 0; }
  .hw-seg__btn {
    min-height: 44px;
    border: 4px solid transparent;
    background-clip: padding-box;
  }
  .hw-seg__btn[aria-pressed="true"] { box-shadow: none; }
  /* min-width too: audit-ux measured "radii" at 43.x wide — a short word
     leaves the chip under the floor on its long axis. */
  .hw-chipbtn {
    min-height: 44px;
    min-width: 44px;
    justify-content: center;
    border-block: 10px solid transparent;
    background-clip: padding-box;
  }
  .hw-header__link { min-height: 44px; }
  /* min-width for 768-1023 Full, where the wordmark is visually hidden and
     only the 26px mark gives the link width (measured 26x44). */
  .hw-header__brand { min-height: 44px; min-width: 44px; justify-content: center; }

  /* The disclaimer's three links are the life-safety exits on every view, so
     they get real targets even though WCAG exempts links in prose. Measured
     before: 18px-tall inline boxes (13px text). Vertical padding on an INLINE
     box grows the hit area without touching the line box, so nothing reflows:
     13px each side lands the box at 44px. The line pitch rises to 2.2 (~29px)
     so neighbouring lines' boxes overlap by ~15px, split evenly between the
     two lines, rather than one line's link swallowing the next line's text.
     clone gives every wrapped fragment its own padding. */
  .hw-footer__disclaimer { line-height: 2.2; }
  .hw-footer__disclaimer a {
    padding-block: 13px;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }
}

@media (max-width: 719px), (pointer: coarse) {
  .skip-link { display: inline-flex; align-items: center; min-height: 44px; }

  /* recon.html's shell buttons */
  .btn { min-height: 44px; }
  .btn--icon { min-width: 44px; min-height: 44px; }

  .hw-models__var { min-height: 44px; }
  /* min-width too: the loop's prev/next are a single chevron each. */
  .hw-imagery__band, .hw-imagery__btn { min-height: 44px; min-width: 44px; }
  .hw-imagery__btn--rate { min-width: 48px; }
  .hw-alerts__filter-select { min-height: 44px; }

  /* The scrubber is a drag target; a 30px rail on a phone means you grab the
     page instead of the handle. (The old .hw-motion__* targets are retired
     with their markup; motion.css owns the new scrubber's.) */
  .hw-imagery__scrub { height: 44px; }
  .hw-imagery__scrub::-webkit-slider-thumb { width: 20px; height: 20px; margin-top: -8px; }
  .hw-imagery__scrub::-moz-range-thumb { width: 20px; height: 20px; }

  .hw-disclosure__summary { min-height: 44px; }

  /* A list of targets (a panel head's source link), not prose. */
  .hw-panel__meta a { display: inline-flex; align-items: center; min-height: 44px; }
  /* Pays for the above out of the head's padding rather than the page. */
  .hw-panel__head { padding-block: var(--s-1); }

  /* Leaflet's zoom control ships at 30x30; on touch, zoom is the primary
     interaction after pan. !important because leaflet.css loads alongside. */
  .leaflet-touch .leaflet-bar a,
  .leaflet-control-zoom a {
    width: 44px !important;
    height: 44px !important;
    line-height: 44px !important;
    font-size: 22px !important;
  }
}

/* ============================================================================
   19  REDUCED MOTION
   ------------------------------------------------------------------------
   Honored on FIRST LOAD, not only after a toggle: js/preboot.js stamps
   data-motion="off" before paint, and motion.js reads the same query to start
   every player paused. The tile collapse has its own hard gate in §04.2.
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  :root { --motion-scale: 0.01; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .status-dot--loading, .hw-dot--loading { animation: none; opacity: 1; }
}

:root[data-motion="off"] { --motion-scale: 0.35; }
:root[data-motion="off"] .status-dot--loading { animation: none; }

/* ============================================================================
   20  PRINT
   ------------------------------------------------------------------------
   Someone may print this while deciding whether to evacuate. Black on white,
   every tile expanded, severity legible without colour, link targets shown.
   ========================================================================= */

@media print {
  /* The light palette lives on :root[data-theme="light"] (0,2,0), so a bare
     :root here would lose. Match both. */
  :root,
  :root[data-theme] {
    color-scheme: light;
    --bg: #fff; --surface: #fff; --surface-2: #fff; --surface-3: #fff; --surface-sunken: #fff;
    --frame: #fff; --ink-on-sev: #fff;
    --border: #999; --border-strong: #333; --border-control: #333;
    --text: #000; --text-dim: #222; --text-faint: #444;
    --accent: #000; --accent-soft: #fff; --accent-line: #666; --text-on-accent: #fff;
    --ok: #000; --warn: #000; --err: #000; --idle: #000;
    --sev-extreme: #000; --sev-severe: #000; --sev-moderate: #000;
    --sev-minor: #000; --sev-unknown: #000; --sev-tint: 0%;
    --kind-warning: #000; --kind-watch: #000; --kind-advisory: #000; --kind-info: #000;
    --threat-none: #000; --threat-watch: #000; --threat-warning: #000; --threat-direct: #000;
    --shadow-1: none; --shadow-2: none; --shadow-3: none; --shadow-tile: none;
    --motion-scale: 0.01;
    /* Six agencies on a mono printer: separated by value. Every chart is also
       captioned in text, so the lines never carry the meaning alone. */
    --m-ecmwf: #000; --m-gfs: #555; --m-icon: #999;
    --m-jma: #222; --m-ukmo: #777; --m-mf: #bbb;
  }

  @page { margin: 0.5in; }

  body { background: var(--tile); color: var(--text); font-size: 10pt; padding: 0; }

  /* Interactive chrome carries no information on paper. */
  .skip-link,
  .hw-header__switcher,
  .hw-header__mode,
  .hw-header__full,
  .hw-iconbtn,
  .hw-tile__toggle,
  #island-rail,
  #first-visit,
  #toast,
  #motion-bar,
  .hw-imagery__controls,
  .hw-imagery__bands,
  .hw-models__vars,
  .hw-alerts__filter,
  .hw-disclosure__hint,
  .hw-alerts__filter-select,
  .app-header__controls,
  .btn { display: none !important; }

  .hw-header, .app-header { position: static; padding: 0; background: var(--tile); }
  .hw-header__inner { box-shadow: none; padding: 0 0 6pt; border-bottom: 2pt solid var(--text); border-radius: 0; }

  .hw-layout { display: block; max-width: none; padding: 0; }
  .hw-col { display: block; }
  .hw-layout :is(#answer-panel, #tiles-simple-a, #odds-panel, #tiles-simple-b, #tiles-simple-c,
    #full-storm, #full-left, #odds-full, #full-right, #imagery-panel, #full-right-b) { display: block; }
  #map-tile { position: static !important; height: auto !important; min-height: 0 !important; }

  .hw-tile, .hw-panel {
    border: 1px solid var(--border-strong);
    box-shadow: none;
    background: var(--tile);
    margin-bottom: 8pt;
  }
  /* Expand every tile — the whole point of printing is the detail. */
  .hw-tile--collapsed > .hw-tile__body {
    grid-template-rows: 1fr;
    margin-top: 8pt;
    opacity: 1;
    visibility: visible;
  }
  .hw-tile--collapsed > .hw-tile__head > .hw-tile__fact { display: none; }
  .hw-alert, .hw-surge, .hw-models__verdict, .hw-metric, .hw-well,
  .hw-model-legend__item, .hw-model-table__row, .hw-spark__row, tr {
    break-inside: avoid;
    page-break-inside: avoid;
  }
  .hw-tile__head, .hw-panel__head {
    break-after: avoid;
    page-break-after: avoid;
  }
  h2, h3, h4 { break-after: avoid; page-break-after: avoid; }
  .hw-observed { background: var(--tile); border-top: 2pt solid var(--text); break-inside: avoid; }
  .hw-observed__metrics { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; }

  /* Severity survives greyscale on rail weight + glyph + written label. */
  .hw-alert--sev-extreme  { border-inline-start: 6pt solid var(--text) !important; }
  .hw-alert--sev-severe   { border-inline-start: 4pt solid var(--text) !important; }
  .hw-alert--sev-moderate { border-inline-start: 3pt solid var(--text) !important; }
  .hw-alert--sev-minor    { border-inline-start: 2pt solid var(--text) !important; }
  .hw-alert--sev-unknown  { border-inline-start: 1pt dashed var(--text) !important; }
  .hw-alert--prominent.hw-alert--sev-extreme { background-image: none; padding-inline-start: 0; }
  .hw-badge, .hw-chip, .hw-pill, .hw-btn { border: 1px solid var(--text); background: var(--tile); color: var(--text); }
  .hw-cat-chip { background: var(--tile) !important; color: var(--text); border: 1.5pt solid var(--text); }

  details::details-content {
    content-visibility: visible !important;
    block-size: auto !important;
    opacity: 1 !important;
    overflow: visible !important;
  }
  .hw-disclosure__hint::after { display: none; }
  .hw-alert__raw, .hw-source__error-pre { max-height: none; overflow: visible; }

  .hw-panel__body--scroll, .hw-alert__more, .hw-alert__raw,
  .hw-scroll { overflow: visible !important; max-height: none !important; }
  .hw-table, .hw-raw { white-space: normal; }
  .hw-islands__grid, .hw-model-legend { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }

  #map { height: 4in; }
  .hw-imagery__stage { max-height: 3.5in; }

  /* Show link targets — a printout has no hover. */
  .hw-footer a::after,
  .hw-panel__meta a::after,
  .hw-source__head a::after {
    content: " (" attr(href) ")";
    font-size: 7pt;
    color: var(--text-faint);
    word-break: break-all;
  }
}
