/* Maverick Wx — the application shell.

   Everything three sections share: the global masthead, the contextual title bar under
   it, the two layout variants, the side-panel component, the drawer, and the control,
   badge and metadata primitives. Loaded on every page, before the page-specific sheet.

   The problem it solves: Tropical Weather, Radar and the NWS Explorer each grew their
   own sidebar (`.rail`, `.radar-panel`, `.sidebar`), their own title placement (an <h1>
   in the pane, an <h1> inside the layer panel, an <h1> inside the scrolling content) and
   their own responsive collapse (static stacking, a bottom sheet, a left drawer). They
   read as three sites. One panel component with three modifiers and one title bar is
   what makes them read as one.

   Markup contract — <body data-layout="dashboard|workspace"> selects the variant:

     <a class="skip-link">
     <header class="masthead">      global brand + section nav, identical everywhere
       <div class="masthead-tools">   status pill + theme toggle, wrapping as one
     <section class="pagehead">     contextual: where am I, what is this, how fresh —
                                    a labelled region, because it is page content and
                                    sits outside <main> in every layout
     <div class="databanner">       optional, sticky, outage/stale state
     <div class="shell">            layout root
       <aside class="panel …">      the side panel, one component
       <div class="stage|pane">     the work surface
*/

* { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: var(--fs-remarks) / var(--lh-remarks) var(--font-body);
  -webkit-font-smoothing: antialiased;
}

/* The surfaces the browser paints, drawn from the committed colour rather than left on the
   OS default: the network's enamel extends to the selection wash, the caret and the
   scrollbar. */
::selection { background: color-mix(in srgb, var(--enamel) 26%, transparent); }
html { scrollbar-color: var(--enamel) transparent; }
input, textarea { caret-color: var(--enamel); }

/* A heading is the sans's own voice, larger — not a second family. Headings were set in the
   mono for a spell, on the argument that a heading in a bulletin is the same teletype that
   printed the values under it; but a monospace headline reads as a terminal, not a masthead,
   and it clashed with the sans prose beneath it. Headings are the interface sans now, and the
   mono is kept for the values it aligns. `text-wrap: balance` stops a two-word orphan hanging
   off a wrapped page title, which at --fs-title is the most visible ragging on the site. */
h1, h2, h3, h4 {
  font-family: var(--font-body);
  font-weight: 700;
  line-height: var(--lh-lead);
  letter-spacing: var(--track-title);
  text-wrap: balance;
}
/* Every heading level takes a step by name. Left unsized, h3 and h4 fell through to the
   user agent's 1.17em and 1em — 18.72px and 16px, two sizes off the scale that no rule in
   this repository asked for and that a browser sweep is the only way to see. Below h2 the
   scale has run out of heading steps, which is the Short Scale Rule working: a third level
   of heading is the bulletin's own body weight, and the hierarchy is carried by the rule
   above it and by the weight, not by a fifth size. */
h1 { font-size: var(--fs-title); line-height: var(--lh-title); }
h2 { font-size: var(--fs-lead); }
h3, h4 { font-size: var(--fs-bulletin); }

/* Colour used to be what told a reader this word was a link. With the interface reduced to
   ink there is nothing left to carry it but the underline, so the underline stops being a
   hover affordance and becomes the link's resting state — wherever a link sits inside
   running prose, which is where a reader could otherwise not tell it apart from the sentence
   around it (WCAG 1.4.1).

   Grouped links do not need it. In a nav bar, a footer column or a list of destinations
   every item is a link and there is no prose to confuse them with, so position does the work
   an underline would otherwise do and twelve underlines of noise are avoided. That is the
   whole rule: underline in a sentence, not in a list. */
a {
  color: var(--link);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
a:hover { text-decoration-thickness: 2px; }

/* Anything that is a control rather than a link in a sentence opts back out. It has a
   ground, a border or a bar of its own, which is a stronger affordance than an underline
   and does not need one under it. Everything not listed here keeps the underline, including
   the footer columns: a muted link with neither colour nor underline is hard to tell from
   the heading above it, and that heading is the only other text in the column. */
.brand,
.skip-link,
.sitestatus,
.masthead nav a,
.panel-item,
.btn,
.choice,
.drawer-close { text-decoration: none; }

/* These containers used to need a darker step of the accent ramp: --link (#2563EB) measured
 * 4.43:1 on the amber tint, 4.38:1 on coral and 4.49:1 on green — all just under AA, and all
 * invisible to a reading of the stylesheet because every token passed on its own surface.
 * axe found it on the NWS Guide's safety callout, the worst possible place for a link a
 * reader might not see.
 *
 * The class of bug is now unreachable rather than fixed: a link is ink, ink is the same ink
 * as the sentence it sits in, and a tint that could carry the sentence can carry the link.
 * The rule stays for the underline, which these prose blocks need and which the selector
 * above does not always reach — a link directly inside `.callout` is in no paragraph. */
.callout a,
.notice a,
.databanner a,
.freshness--attached a,
.product-note a,
.about-safety a,
.empty a {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }

code, pre, .mono { font-family: var(--font-mono); }
code { background: var(--code-bg); padding: 1px 5px;  }
pre {
  background: var(--code-bg); padding: var(--sp-3) var(--sp-4); overflow-x: auto;
  border: 1px solid var(--divider);
}
pre code { background: none; padding: 0; }

.muted { color: var(--muted); }

/* `hidden` means hidden, whatever a component rule says about display.
 *
 * The attribute's `display: none` comes from the UA stylesheet, which any author rule
 * outranks — so `.btn[hidden]` was visible, because `.btn` sets `display: inline-flex`.
 * That is a silent bug generator: every component that sets `display` quietly opts out of
 * the platform's own way of hiding things, and the failure shows up as a control that is
 * on screen when the script that owns it believes it is not. Four rules in this codebase
 * were already patching it one selector at a time. */
[hidden] { display: none !important; }

/* Standfirst prose, under a heading or as a loading placeholder. Shared: it was defined
   twice, at two sizes and two colours, and appeared on both sides of /nws. */
.lede { color: var(--muted); font-family: var(--font-body); font-size: var(--fs-remarks); max-width: 64ch; margin: 0 0 var(--sp-6); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--ink); color: var(--paper); padding: 10px var(--sp-4); font-weight: 600;
}
.skip-link:focus { left: 0; }

/* Reading measure. Clear Sky asks for comfortable line lengths on the editorial pages,
   and 1120px of Public Sans at 16px is roughly 153 characters — well past readable. Prose
   opts into the narrower measure; tables, code and figures inside it opt back out. */
.prose { max-width: var(--wrap-prose); }
.prose > .table-scroll,
.prose > pre,
.prose > figure,
.prose > .codeblock { max-width: none; }

/* Every measured *value* reads as instrument data: monospace with tabular figures, so
   digits line up down a column and two readings can be compared by eye.

   Two classes left this list in the redesign, and both were carrying sentences:

     `.updated` is the standing attribution and disclaimer line under a graphic — "Static
     graphics generated by Maverick Wx. Not an official forecast." Three sentences of
     monospaced prose, on nearly every view. It is the clearest case of the old system
     using mono as a *character* rather than as an affordance, which is exactly what the
     brief rules out.

     `.panel-label` is a heading. It is redefined below as the display face; leaving it here only
     meant two rules disagreeing about the same element.

   What stays is what the face is for: a stat's value, its sub-line, and anything explicitly
   marked `.mono` or `.data`. `.item-meta` was in this list for the storm rail's measured
   line ("115 kt · 948 hPa"); the rail is gone and the readout's cells take the face from
   `.systems-table` itself. */
.data, .mono, .stat-value, .stat-sub {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.updated { font-variant-numeric: tabular-nums; }

/* ============================================================================
   Containers
   ==========================================================================*/

/* The shell container tracks the page's layout variant, so the masthead and the title
   bar sit on the same left edge as the content beneath them in both variants. A
   constrained bar over a full-bleed map is the specific thing that made Radar look like
   a different site. */
.shell-inner {
  max-width: var(--wrap-wide);
  margin: 0 auto;
  padding: 0 var(--gutter);
  width: 100%;
}
body[data-layout="workspace"] .shell-inner { max-width: none; }
/* A reading page (the API reference, the 404) keeps the prose measure, so its bars line
   up with its text instead of overhanging it by 300px on a wide monitor. */
body[data-layout="page"] .shell-inner { max-width: var(--wrap); }

/* Prose measure, for the API reference. */
.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 var(--gutter); }
.wrap--wide { max-width: var(--wrap-wide); }

/* ============================================================================
   Masthead — global, identical on every page
   ==========================================================================*/

/* The masthead is the product's header block, not a brand bar. Two systems ago it was a
   navy strip that ran across the top of every map on the site and competed with it; Clear
   Sky moved it onto a surface 1.08:1 from the canvas, and The Advisory takes the last step:
   it is the same paper as everything below it and a single hairline says where it ends.
   That is what a printed product does, and "keep maps and primary graphics visually
   dominant" is mostly a statement about what the chrome above them is allowed to weigh. */
.masthead {
  background: var(--header);
  color: var(--header-ink);
  /* The station header sits ON the paper, its own edge printed underneath — the boundary of
     a coloured field, not a divider between two paper blocks. */
  border-bottom: 1px solid var(--enamel-strong);
  box-shadow: 0 1px 0 color-mix(in srgb, var(--enamel-strong) 60%, transparent);
  position: sticky;
  top: 0;
  z-index: 30;

  /* The masthead is an enamel field, so every child rule that reaches for ink, a grey, a
     rule or a wash is now reaching for it against cobalt. Rather than rewrite each rule, the
     role tokens are re-pointed to their on-enamel siblings for the length of this block: the
     brand, the nav, the status pill and the theme toggle all re-resolve correctly and keep
     their behaviour. `--paper` and `--ink` swap so the reversed `critical` pill lands as a
     bright block on the dark band — the strongest escalation the header can carry. */
  --ink: var(--on-enamel);
  --ink-2: var(--on-enamel);
  --muted: var(--on-enamel-muted);
  --paper: var(--enamel);
  --line: color-mix(in srgb, var(--on-enamel) 22%, transparent);
  --line-strong: var(--on-enamel-muted);
  --edge: var(--on-enamel-muted);
  --wash: color-mix(in srgb, var(--on-enamel) 8%, transparent);
  --wash-strong: color-mix(in srgb, var(--on-enamel) 16%, transparent);
}
.masthead .shell-inner {
  display: flex;
  align-items: center;
  gap: var(--sp-3) var(--sp-4);
  padding-block: var(--sp-3);
  flex-wrap: wrap;
}

.brand {
  font-family: var(--font-body); font-weight: 700; letter-spacing: -0.02em;
  font-size: var(--fs-lead); color: var(--header-ink); display: inline-flex;
  align-items: center; gap: var(--sp-2);
}
.brand:hover { text-decoration: none; }
.brand .mark { width: 24px; height: 24px; }
/* The wordmark reads "MaverickWX" as a compact product mark in the bar, and "Maverick
   Weather" in the footer, where the line has room and a first-time visitor most needs the
   full name. `.wx` is the second half of the compact form; the footer just sets the long
   name as text, since it needs no second colour. */
.brand .wx {
  font-weight: 700; color: var(--muted); margin-left: -2px;
}

/* The interface sans, not the mono. An all-caps monospaced nav is the single loudest "this
   is a terminal" signal a page can send; set in the sans it is a label, not a readout. The
   caps and tracking stay — see the register note below — but the face is the voice's, so the
   bar sits in the same family as the title under it. */
/* Five destinations wrap; they do not scroll and they do not collapse behind a button.
   The links measure 445px together, so a row narrower than about 520px cannot hold them
   and the nav takes a second line — which is all WCAG 1.4.10 asks for and all five items
   need. `flex-wrap` is stated once, unconditionally, rather than at a breakpoint: above
   520px the row always fits and wrapping is inert, and a value that is correct at every
   width cannot be wrong at the one width nobody sampled (a longer label, a bigger user
   font, 400% zoom). What it replaces is a `pointer: coarse` overflow-x scroller, which
   put "Archive" and "Developers" behind an invisible scrollbar on a phone and — because
   the gate did not match a desktop, which is exactly the 1.4.10 test condition — let the
   DOCUMENT overflow by 145px at 320px instead. */
.masthead nav {
  margin-left: auto;
  display: flex; flex-wrap: wrap; gap: var(--sp-1); align-items: center;
}
/* Navigation is Micro, and Micro is uppercase and tracked — the three go together, and
   splitting them is what makes an 11px nav read as a footnote instead of a header block.
   The bulletin sets its own header line the same way: `WTNT33 KNHC 261459` is not small
   because it is unimportant, it is small because it is a register. Sentence case at 11px
   would be the mistake; caps at 11px with 0.04em between them is a legible label at a size
   that keeps five destinations subordinate to the page title, which is the point. */
.masthead nav a {
  color: var(--header-nav); font-size: var(--fs-micro); letter-spacing: var(--track-micro);
  font-family: var(--font-body); font-weight: 700; text-transform: uppercase;
  padding: var(--sp-2) var(--sp-3);
  position: relative; white-space: nowrap;
}
.masthead nav a:hover {
  color: var(--ink); background: var(--wash-strong); text-decoration: none;
}
/* Current section: NOT colour alone. Three signals — a weight change, a tinted ground and
   an underline rule — so it survives greyscale, a colour-vision difference and a forced
   -colours mode, which a `color:` swap on its own does not. */
.masthead nav a[aria-current="page"] {
  color: var(--ink);
  background: var(--wash-strong);
  font-weight: 700;
}
.masthead nav a[aria-current="page"]::after {
  content: "";
  position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: 3px;
  height: 2px; background: var(--ink);
}

/* --- The global status indicator -------------------------------------------------
   One concise reading of whether the site is publishing, in the masthead, on every page.
   It renders as a plain link to /status before JS and stays one if the probe fails, so a
   reader always has a route to the health page — the indicator is an enhancement on top
   of a link, never a replacement for one.

   Never colour alone, and never at any width: the dot carries the state, a sentence beside
   it names the same state in words, and below 1080px — where the sentence does not fit —
   a single word takes its place. `aria-label` carries the sentence throughout. */
/* The pill and the theme control wrap as a pair, because they are a pair: site state and
   site preference, in the bar's right-hand corner. As two loose flex items the bar's own
   wrap split them — at 768px the 32px toggle went to a second row on its own, under the
   wordmark, buying a whole row of sticky chrome for one button. Grouping them means the
   row breaks between the nav and the pair rather than through it. */
.masthead-tools {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  margin-left: var(--sp-2);
}

.sitestatus {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--line);
  color: var(--muted); font-size: var(--fs-bulletin); font-weight: 600;
  white-space: nowrap;
}
.sitestatus:hover { text-decoration: none; border-color: var(--line-strong); color: var(--ink); }
/* The same marker ladder the freshness component uses, so the two places this site reports
   a state say it the same way — which matters more here than anywhere, because this pill and
   the storm rows below it answer *different* questions (pipeline health vs this advisory's
   age) in the same visual language, and a green pill over amber rows is the one place the
   interface can read as less true than the copy. It no longer claims a colour the rows
   cannot contradict; `critical` reverses, which nothing else in the masthead does. */
.sitestatus-dot {
  flex: none;
  min-width: 1.1em; text-align: left;
  font-family: var(--font-mono); font-size: var(--fs-micro);
  letter-spacing: 0.02em; color: var(--muted);
}
.sitestatus-dot::before { content: "·"; }
.sitestatus[data-state="warn"] .sitestatus-dot::before { content: "!"; }
.sitestatus[data-state="critical"] .sitestatus-dot::before { content: "!!"; }
.sitestatus[data-state="warn"] { color: var(--ink); border-color: var(--edge); font-weight: 700; }
.sitestatus[data-state="warn"] .sitestatus-dot { color: var(--ink); font-weight: 700; }
.sitestatus[data-state="critical"] {
  color: var(--paper); background: var(--ink); border-color: var(--ink); font-weight: 700;
}
.sitestatus[data-state="critical"] .sitestatus-dot { color: var(--paper); font-weight: 700; }
.sitestatus[data-state="critical"]:hover { color: var(--paper); border-color: var(--ink); }

/* Below 1080px the sentence does not fit beside five nav items, and it used to be the
   only thing that was not a colour — so what a narrow screen actually rendered was a bare
   coloured dot: state by colour alone (WCAG 1.4.1), and in a forced-colours mode, which
   discards the tint entirely, no state at all. The short word takes its place. It is
   `aria-hidden` because the sentence is still in the accessibility tree above and in
   `aria-label` after shell.js paints, and hearing both would be hearing it twice. */
.sitestatus-short { display: none; }
@media (max-width: 1080px) {
  .sitestatus-text { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
  /* Sized for the longest word it can hold ("Behind"), so the bar does not resize under
     the reader when the probe answers: the pill ships reading "Health" and paints "OK",
     "Behind" or "Down" a moment later, and a pill that changed width would shove the whole
     nav sideways after first paint. */
  .sitestatus-short { display: inline-block; min-width: 3.6em; text-align: center; }
  .sitestatus { padding-inline: var(--sp-2); }
}

/* --- Theme control ---------------------------------------------------------------
   Clear Sky is a light system and the site no longer follows the OS into dark, so this
   is the only way a reader who wants dark can ask for it. That makes it chrome, not a
   preference buried in a settings page — but quiet chrome: an icon beside the status
   pill, at the same 32px rhythm, never competing with the map below it.

   `hidden` until shell.js reveals it; see the markup comment in each page head. The 44px
   coarse-pointer target is added with the rest of the bar further down. */
.themetoggle {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none;
  width: 32px; height: 32px;
  padding: 0;
  border: 1px solid var(--line);
  background: none; color: var(--muted);
  cursor: pointer;
}
.themetoggle svg { width: 17px; height: 17px; display: block; }
.themetoggle:hover { border-color: var(--line-strong); color: var(--ink); }

/* --- The two-row bar, and touch targets on a coarse pointer ----------------------
   The nav links are real padded controls rather than bare text, so most of the hit area
   comes from the component itself; what is left is guaranteeing the 44px height WCAG asks
   for. Fitting five sections plus a status pill into a 390px viewport is no longer this
   block's problem — the bar wraps, at every pointer. */
/* Below the point where the bar wraps, the status pill and the theme toggle ride with the
   brand instead of claiming a row of their own, and the nav takes the second row whole.
   Measured at 375px: brand / nav / pill on three rows is ~150px of sticky chrome above a
   map that has 780px to work with.

   The threshold is 820px rather than the 720px it was, because that is where the one-row
   form actually stops fitting: 156px of wordmark + 445px of nav + a status pill that now
   carries a word (M-13) + gutters needs about 814px. Between 721px and 813px the bar was
   therefore wrapping *anyway* — but by flexbox's rule, which sheds the last item, so what
   went to the second row was the tools pair alone while the nav stayed squeezed beside the
   brand. Two rows either way; this is the one that was designed. */
@media (max-width: 820px) {
  .masthead .shell-inner { gap: var(--sp-2) var(--sp-3); }
  .masthead-tools { order: 1; margin-left: auto; }
  .masthead nav { order: 3; width: 100%; margin-left: 0; }
}

/* One row of five, on a phone.
 *
 * Wrapping is still the mechanism and still correct — it is what WCAG 1.4.10 asks for and
 * it is why the scroller that used to live here is gone. But wrapping five items when the
 * fifth is the only one that does not fit spends 48px of a 390px screen on a row holding
 * `DEVELOPERS` alone, and it does it above the headline, before anything the page is for.
 *
 * The words are not negotiable. `explorer/src/render/chrome.ts` records why each is the
 * word it is — `Developers` in particular exists so no nav item reads `API` on a site that
 * documents two of them, and `tests/web/test_naming.py` pins it. So what gives is the
 * padding, which is density rather than meaning: 445px of links is 309px of text inside
 * 136px of inset, and the inset is the part a 350px row cannot afford.
 *
 * IBM Plex Sans is a normal-width face, wider than the condensed mono the nav used to be set
 * in, so the phone row needs more than a padding trim to stay one line: 5px of inset each
 * side (down from 12px), 1px between items (down from 4px), no tracking (down from 0.04em)
 * and a 12px masthead gutter (down from 20px). Together that lands five uppercase labels at
 * ~312px, inside the ~336px a 360px screen leaves after the gutter — ~24px of slack, so a few
 * pixels of font-rendering difference between machines cannot tip it into a second row (a
 * tighter margin passed locally and wrapped in CI). The target keeps its 44px height under a
 * coarse pointer (the rule below is untouched) and every label stays far wider than 2.5.8's
 * 24px floor — the narrowest, `RADAR`, measures ~47px. Below ~312px of row the fifth item
 * wraps, which is the honest outcome: a 300px screen cannot hold five of these words at a
 * legible size, and wrapping is what this nav does when it cannot. */
@media (max-width: 430px) {
  .masthead .shell-inner { padding-inline: var(--sp-3); }
  .masthead nav { gap: 1px; }
  .masthead nav a { padding-inline: 5px; letter-spacing: 0; }
}

/* What is left of the old coarse-pointer block: the 44px targets, which are a real
   requirement. The scroller that used to live here is gone — see `.masthead nav` above.
   Nothing about hit-target size may be conditional on the *document* not overflowing. */
@media (pointer: coarse) {
  .masthead nav a { flex: none; display: inline-flex; align-items: center; min-height: 44px; }
  .brand { min-height: 44px; }
  .sitestatus { min-height: 44px; }
  .themetoggle { min-height: 44px; min-width: 44px; }
}

/* ============================================================================
   Pagehead — the contextual title system
   ==========================================================================*/

/* One title treatment for all three sections. Before this, the title was in a different
   place and a different size in each: inside the pane on Tropical Weather, inside the
   layer panel on Radar, inside the scrolling content on /nws — so scrolling could carry
   the page's own name off screen while the brand bar stayed. It sits here now, between
   the global bar and the work surface, and it is where a section says what it is, what
   it is showing, and how fresh that is. */
.pagehead {
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}
.pagehead .shell-inner {
  display: flex;
  align-items: flex-end;
  /* The drawer button aligns to the title, not to the bottom of a two-line sub. */
}
.pagehead .shell-inner > .panel-toggle { align-self: center; }
.pagehead .shell-inner {
  gap: var(--sp-4) var(--sp-5);
  padding-block: var(--sp-4);
  flex-wrap: wrap;
}

/* `1 1 260px` rather than `1 1 auto`: with an auto basis the titles' content width wins
   and the drawer button wraps to a row of its own, which on a phone costs the map ~44px
   of the little height it has. */
.pagehead-titles { min-width: 0; flex: 1 1 260px; }
/* Was an uppercase mono category label — "REFERENCE", "LIVE OBSERVATIONS" — which the
   brief names as the specific thing to stop doing. What survives is the *useful* half of
   that slot: a way back up the hierarchy. It is sentence case, it is a link where there is
   somewhere to go, and where there is nowhere to go it is simply absent rather than
   restating the section name the nav already marks. */
.pagehead-kicker {
  font-size: var(--fs-bulletin); color: var(--muted); margin: 0 0 var(--sp-1);
  font-weight: 600;
}
.pagehead-kicker a { color: var(--muted); }
.pagehead-kicker a:hover { color: var(--ink); }
/* Title row: a status badge may sit beside the title (the storm classification chip),
   which is why the H1 is not the flex parent itself. */
.pagehead-line { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.pagehead-title { font-size: var(--fs-title); line-height: var(--lh-title); margin: 0; }
.pagehead-badge[hidden] { display: none; }
.pagehead-sub {
  color: var(--ink-2); font-family: var(--font-body); font-size: var(--fs-remarks);
  margin: var(--sp-2) 0 0; max-width: var(--wrap-prose); line-height: var(--lh-remarks);
}

/* Right-hand slot: search, mode toggles, freshness stamps.
   `flex: 0 0 auto` and `nowrap`, not `0 1 auto` and `wrap`: the titles block carries the
   only grow factor on the row, so with a shrinkable aux it took every pixel of free space
   and pushed the last control — the theme toggle — onto a second line at full desktop
   width. The controls hold their size; the title wraps instead, which is what a title is
   for. Below the breakpoint the slot wraps under the title as a whole. */
.pagehead-aux {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-left: auto; flex: 0 0 auto; min-width: 0; flex-wrap: nowrap;
}
/* Inside the strip the controls hold their shape and the search absorbs the slack.
   Without this the aux row is exactly wide enough to squeeze the segmented control below
   its content width, and it breaks its own cells onto two lines. The no-wrap half of that
   rule now lives on the component itself (see `.choicebar, .seg` above — it was never
   specific to this strip); what is still needed here is that the control may not *shrink*, or the
   titles block takes its width and it scrolls when it had room not to. */
.pagehead-aux > * { flex: none; }
.pagehead-aux .seg, .pagehead-aux .choicebar { flex: none; }
.pagehead-aux .topsearch { flex: 1 1 200px; min-width: 132px; }
/* The freshness stamp is the one aux item that is a *sentence*, so it is the one that must
   be allowed to shrink and wrap. With `flex: none` it sized to max-content: at 375px the
   attached form ("Iselle's advisory is 11 hours old. We're showing the last valid update.")
   came out 546px wide and pushed the whole page into a horizontal scroll. It may shrink but
   never grow, so on a desktop it still sits right where it always did. */
.pagehead-aux .pagehead-meta { flex: 0 1 auto; min-width: 0; }

.pagehead-meta {
  font-size: var(--fs-bulletin); color: var(--muted);
  text-align: right; line-height: 1.45; margin: 0;
}
/* The numbers inside a freshness stamp are still measured values and still line up in
   mono; the words around them are not. This is the whole typographic rule of the redesign
   in one selector. */
.pagehead-meta .mono, .pagehead-meta time { font-variant-numeric: tabular-nums; }

/* The issuance a storm page is showing, named once: "Advisory 4 · 27 Aug 00:00Z".

   It sits above the condition sentence because identity comes before condition — what am I
   looking at, then is it current — and it is the only *time* in the strip, which is the
   whole point of it (audit M-02/M-25). The number is the identity, so it takes the ink; the
   instant beside it is a measured value and is set in mono like every other measured value
   on the site. */
.pagehead-ident {
  display: block; margin-bottom: 2px;
  font-size: var(--fs-bulletin); font-weight: 600; color: var(--ink-2);
}
.pagehead-ident time {
  margin-left: 2px; font-family: var(--font-mono); 
  font-weight: 500; color: var(--muted);
}
/* "(final)" is a qualifier on the number, not part of it. */
.ident-final { font-weight: 500; color: var(--muted); }

/* ============================================================================
   Layout variants
   ==========================================================================*/

/* Dashboard — Tropical Weather. A constrained monitoring surface: the page scrolls, the
   rail sticks under the bars, the measure stays readable. */
body[data-layout="dashboard"] .shell {
  display: grid;
  grid-template-columns: var(--panel-w) minmax(0, 1fr);
  gap: var(--sp-6);
  align-items: start;
  max-width: var(--wrap-wide);
  margin: 0 auto;
  padding: var(--sp-6) var(--gutter) var(--sp-8);
}

/* Workspace — Radar and the NWS Explorer. The tool fills the viewport under the bars and
   scrolls internally, so the map or the reference never scrolls its own controls away.
   The offsets compose from measured heights (shell.js): hardcoding them tucked the panel
   under the bars on exactly the viewports where both wrapped to two lines. */
body[data-layout="workspace"] .shell {
  display: grid;
  grid-template-columns: var(--panel-w) minmax(0, 1fr);
  height: calc(100vh - var(--masthead-h) - var(--pagehead-h) - var(--banner-h));
  min-height: 460px;
  overflow: hidden;
}

/* The work surface in each variant. */
.pane { min-width: 0; }
.stage { position: relative; min-width: 0; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
.stage--scroll { overflow-y: auto; }

/* ============================================================================
   Panel — ONE side-panel component, three modifiers
   ==========================================================================*/

.panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: var(--sp-1);
}

/* Dashboard rail: no chrome of its own, sticks under the bars, sits on the page ground. */
.panel--rail {
  position: sticky;
  /* Only the bars that are themselves sticky. The title strip scrolls away, so counting
     it here would pin the rail its full height below the masthead and leave a gap under
     the bar for the whole scroll. */
  top: calc(var(--masthead-h) + var(--banner-h) + var(--sp-2));
  max-height: calc(100vh - var(--masthead-h) - var(--banner-h) - var(--sp-4));
  overflow-y: auto;
  gap: var(--sp-2);
}

/* Workspace panels: a bordered column beside the stage, scrolling on their own. */
/* The horizontal padding is the page gutter, so a panel's first column of text sits on
   the same left edge as the page title above it. --sp-4 was 4px short of that and the
   misalignment was visible at every width. */
.panel--controls,
.panel--nav {
  border-right: 1px solid var(--line);
  background: var(--paper);
  overflow-y: auto;
  padding: var(--sp-4) var(--gutter) var(--sp-8);
}
/* Zero horizontal padding so a current nav row's inset accent bar reaches the panel edge;
   the blocks that are not full-bleed rows carry the gutter themselves. */
.panel--nav { padding: var(--sp-4) 0; }
/* The nav rail's own `.panel-foot` rule lived here and is gone with the block it padded:
   the NWS Guide's disclaimer and source links moved into the page foot at the end of the
   content (M-49), and `.panel--nav` is the only rail that ever had a foot. */

/* Ruled section label — the legend caption that divides a panel into groups. Identical
   in all three sections; it was three near-identical rules before. */
/* A group heading in a panel. It was an uppercase mono legend caption — the "REFERENCE" /
   "LIVE OBSERVATIONS" voice the brief asks to replace with short, direct headings. It is
   now sentence-case Public Sans at a small size: still clearly subordinate to the content it
   labels, no longer shouting. */
.panel-label,
.panel-section {
  font-family: var(--font-body); font-size: var(--fs-bulletin); font-weight: 700;
  letter-spacing: 0; text-transform: none; color: var(--ink-2);
  margin: var(--sp-2) 0 var(--sp-2);
}
.panel-section {
  margin-top: var(--sp-5); padding-top: var(--sp-3); border-top: 1px solid var(--line);
}
.panel-section:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

.panel-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); }

/* The shared row. All three sections had already invented this independently — a
   hairline box that takes the accent on hover and an inset accent bar when current
   (`.storm-item.active`, `.radar-row.is-on`, `.nav-link[aria-current]`, `.facet--active`).
   Defining it once is most of what makes the sections feel like one product. */
.panel-item {
  display: block; width: 100%; text-align: left; font: inherit; color: var(--ink);
  background: var(--paper); border: 1px solid var(--line);
  padding: 10px var(--sp-3); cursor: pointer;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.panel-item:hover { border-color: var(--ink); text-decoration: none; }
/* Selection carried by two signals and no colour: the boundary goes to ink and the ground
   steps up.

   The 3px inset accent bar this replaces was the one device in the shared row that could not
   survive the palette: an inset rule that thick reads as a coloured left border, which is a
   pattern the craft floor refuses on a list item, and an ink one would have read heavier
   still. Weight is not used here — these rows carry radar layer names and section titles,
   and bolding the selected one changes how its name reads against the others.

   `[aria-pressed="true"]` stood in this group for the storm rail's rows, which were toggle
   buttons. The rail is gone and no `.panel-item` is a toggle now; the pressed state is the
   basin chips', and `.choice` paints it. */
.panel-item.is-active,
.panel-item[aria-current="page"],
.panel-item[aria-current="true"] {
  border-color: var(--ink);
  background: var(--wash-strong);
}
/* No marker here. `.panel-item` is a grid, so a `::before` becomes a grid item and the
   character lands on a row of its own under the label rather than in front of it — which is
   what it did on the first attempt. The ink boundary and the stepped ground are two signals
   and neither is colour, which is what the rule actually requires. */

/* Borderless variant, for dense navigation lists where a box per row is too much ink. */
.panel-item--flush {
  border-color: transparent; background: transparent;
  display: flex; align-items: center; gap: var(--sp-3);
  font-family: var(--font-body); font-weight: 600; font-size: var(--fs-bulletin);
  color: color-mix(in srgb, var(--ink) 80%, transparent);
}
.panel-item--flush:hover {
  border-color: transparent; background: color-mix(in srgb, var(--ink) 5%, transparent);
  color: var(--ink);
}
.panel-item--flush.is-active,
.panel-item--flush[aria-current="page"] {
  border-color: transparent; color: var(--ink); background: var(--wash-strong);
}

.panel-item-name { font-weight: 700; font-size: var(--fs-bulletin); line-height: 1.2; }
.panel-item-meta {
  color: var(--muted); font-size: var(--fs-bulletin); font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.panel-count {
  margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums; color: var(--muted); flex: none;
}
/* In a section label the count is not a row's trailing badge, it is part of the heading —
   so it needs a gap and its own weight, or "ACTIVE LAYERS 2" reads as one string. */
.panel-section .panel-count { margin-left: 0; color: var(--muted); }
.panel-item[aria-current="page"] .panel-count { color: var(--ink); }

.panel-foot {
  margin-top: auto; padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
  color: var(--muted); font-size: var(--fs-micro); line-height: 1.5;
}
.panel-empty { color: var(--muted); font-size: var(--fs-remarks); font-family: var(--font-body); padding: var(--sp-2) 2px; }

/* ============================================================================
   Drawer — one collapse behaviour for both workspace panels
   ==========================================================================*/

/* Radar slid a bottom sheet up; /nws slid a left drawer in. Same job, two gestures, two
   scrims, two scripts. One left drawer now, driven by shell.js. */
.panel-toggle {
  display: none;
  align-items: center; justify-content: center; gap: var(--sp-2);
  font: inherit; font-family: var(--font-body); font-size: var(--fs-micro);
  letter-spacing: var(--track-micro);
  font-weight: 600; letter-spacing: 0; text-transform: none;
  min-height: 44px; padding: 0 var(--sp-3);
  color: var(--ink); background: var(--paper);
  border: 1px solid var(--edge); cursor: pointer;
}
.panel-toggle:hover { border-color: var(--ink); }
/* What is behind the button. "Layers" alone does not say whether the map is showing one
   feed or six, and "Storms" does not say whether the season is quiet — which is the
   whole question the Tropical page exists to answer. The count is part of the label. */
.panel-toggle-count {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--fs-bulletin);
  color: var(--ink);
  background: var(--wash); border: 1px solid var(--edge); padding: 1px 8px; line-height: 1.5;
}
.panel-toggle-count[hidden] { display: none; }

.drawer-scrim { position: fixed; inset: 0; background: color-mix(in srgb, #000 45%, transparent); z-index: 40; }
.drawer-scrim[hidden] { display: none; }

/* The drawer's own header. It only exists in drawer mode: on a desktop the panel is a
   column of the page under a title strip that already names it, and a header would be
   the section's title said twice. On a phone it is the only thing that says what the
   sheet is, how much is in it, and how to get rid of it — the previous drawer relied on
   a ~55px strip of exposed map as its dismissal affordance, which nobody finds on
   purpose. Sticky, so scrolling a long catalogue never scrolls the way out away. */
.drawer-head { display: none; }
.drawer-title {
  font-family: var(--font-body); font-size: var(--fs-remarks); font-weight: 700;
  letter-spacing: var(--track-title); text-transform: none; color: var(--ink); margin: 0;
  display: flex; align-items: center; gap: var(--sp-2);
}
.drawer-count {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--fs-bulletin); font-weight: 400; color: var(--muted); letter-spacing: 0;
}
/* "STORMS 11 SYSTEMS" is two labels jammed together; the separator makes it a title and
   a readout, which is what it is. */
.drawer-count::before { content: "· "; color: var(--muted); }
.drawer-count:empty { display: none; }
.drawer-close {
  margin-left: auto; flex: none;
  display: grid; place-items: center; width: 40px; height: 40px;
  font: inherit; font-size: var(--fs-lead); line-height: 1;
  color: var(--muted); background: transparent;
  border: 1px solid var(--edge); cursor: pointer;
}
.drawer-close:hover { color: var(--ink); border-color: var(--ink); }

@media (max-width: 900px) {
  /* `min-width` holds the count's room before the count exists.
   *
   * The button reads "Storms" in the markup and "Storms 2" once `active.json` lands, and the
   * ~40px it gains is enough to push the title block onto a flex line of its own — so the
   * whole strip, and `<main>` under it, moved on every load. 7.5rem is the loaded width with
   * a two-digit count, which is the widest it goes; reserving it makes the first paint the
   * layout the page settles into rather than one it has to leave. */
  .panel-toggle { display: inline-flex; min-width: 7.5rem; }
  .drawer-head {
    display: flex; align-items: center; gap: var(--sp-3);
    position: sticky; top: 0; z-index: 1;
    padding: var(--sp-2) var(--gutter);
    background: var(--paper);
    border-bottom: 1px solid var(--line);
  }
  /* Full-bleed across the panel's own gutter, whichever variant it is. `--panel-pad` is
     the panel's horizontal padding, declared by each variant below. */
  .panel[data-drawer] > .drawer-head { margin-inline: calc(var(--panel-pad, 0px) * -1); }

  /* The sub-line is NOT clamped, at any width. It used to be — two lines here, one below
     640px — on the reasoning that it is context and that a disclaimer is repeated in the
     panel foot. Both halves were wrong. On an invest page the sub-line *is* the
     disclaimer: "It is not a tracked storm and has no forecast track" was line four of
     four, and at 360px a reader saw only line one, which names the agency watching the
     area and reads as a forecast product. Measured there: scrollHeight 96, clientHeight
     24. And a repetition somewhere else is not a substitute for the sentence a reader is
     actually looking at.

     The rule this leaves behind: safety, source and status copy is never truncated —
     not by `-webkit-line-clamp`, not by `text-overflow`. Height is the cheaper thing to
     spend. Everything else in the strip still gets tighter on a narrow screen. */

  /* Below the masthead, not over it. A sheet that covers the global bar reads as a
     different application taking the screen; one that starts under it reads as a panel
     of the page you are on, and the section nav stays reachable while it is open. */
  .panel[data-drawer] {
    position: fixed;
    top: calc(var(--masthead-h) + var(--banner-h));
    left: 0; bottom: 0;
    width: min(86vw, var(--drawer-w));
    max-height: none;
    z-index: 50;
    transform: translateX(-100%);
    transition: transform 0.2s var(--ease);
    border-right: 1px solid var(--edge);
    background: var(--paper);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0 var(--panel-pad) var(--sp-8);
  }
  .panel[data-drawer][data-open] { transform: translateX(0); }

  /* The scrim starts under the masthead too, so the bar it leaves uncovered is still
     usable rather than merely visible. */
  .drawer-scrim { top: calc(var(--masthead-h) + var(--banner-h)); }

  .panel--controls[data-drawer],
  .panel--rail[data-drawer] { --panel-pad: var(--gutter); }
  .panel--nav[data-drawer] { --panel-pad: 0px; }
  .panel--nav[data-drawer] > .drawer-head { padding-inline: var(--gutter); }

  body[data-layout="workspace"] .shell {
    grid-template-columns: minmax(0, 1fr);
    height: calc(100vh - var(--masthead-h) - var(--pagehead-h) - var(--banner-h));
  }

  /* A dashboard collapses the same way a workspace does. It used to reflow its rail into
     a row of tiles *above* the pane, which on a phone put the entire storm catalogue in
     front of the overview map — the one thing the page exists to show. The rail is a
     drawer here now, so the work surface is what follows the title strip in both
     layouts. */
  body[data-layout="dashboard"] .shell {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-5);
    padding-top: var(--sp-5);
  }
  /* The rail's desktop `position: sticky`, `top` and `max-height` are all outranked by
     `.panel[data-drawer]` above, which is deliberate — the rail needs no drawer rules of
     its own beyond the gutter it shares with the radar controls. */

  /* The slot now wraps as a whole, under the title. */
  .pagehead-aux { flex-wrap: wrap; }

  /* Title-strip height is traded for work surface here. The kicker is the first thing to
     go: the masthead already marks which section is current, so on a narrow screen it is
     the one line in the strip that says nothing new.

     The title itself is NOT stepped down here any more. It used to drop to --fs-xl, which
     after the short-scale pass resolves to the same 19px a section heading takes — so from
     320px to 720px the page title and the headings under it were one size, and the strip
     stopped being a title strip. `clamp(26px, 3.2vw, 34px)` already does this job: it floors
     at 26px, which is where the old override was aiming. */
  .pagehead-kicker { display: none; }
}

@media (max-width: 640px) {
  /* Search is the widest control and the one that benefits from the room, so it takes a
     row of its own below the compact ones rather than squeezing all three onto one. */
  .pagehead .shell-inner { gap: var(--sp-2) var(--sp-3); }
  .pagehead-aux { width: 100%; margin-left: 0; }
  /* Hold the freshness row's space before there is anything in it.
   *
   * The strip's contents are written by app.js once `active.json` lands: the heading becomes
   * a sentence about what is active, the storm count appears on the drawer button, and this
   * slot — empty in the markup — gains the freshness line. Below 640px that slot is a
   * full-width row of its own, so it appearing pushes `<main>` and everything in it down.
   * Measured at 390px: 53 px of travel, 0.078 of Cumulative Layout Shift, which is the whole
   * budget spent before the page has drawn anything.
   *
   * Two lines, because that is what the line wraps to at these widths and reserving one
   * would trade a large shift for half of one. It is the *slot* that is held, not the
   * paragraph, so a page whose line is shorter still gets a stable strip. */
  .pagehead-aux { min-height: calc(var(--fs-bulletin) * 1.5 * 2); }
  /* Right-aligned metadata under a left-aligned title reads as a stray fragment once the
     slot is a full-width row of its own. */
  .pagehead-meta { text-align: left; }
  .pagehead-meta .freshness { justify-content: flex-start; }
  .topsearch { order: 5; flex: 1 1 100%; max-width: none; }
  .choice, .seg-opt > span { padding-inline: var(--sp-3); }
}

@media (min-width: 901px) {
  .drawer-scrim { display: none !important; }
}

/* ============================================================================
   Controls
   ==========================================================================*/

/* Two components with one vocabulary — the same face, size and hairline, so a chip and a
   segmented cell read as the same family. They differ in exactly one thing, and it is the
   thing the audit turned on: what each does when it does not fit.

     `.seg`       one instrument, cells divided by 1px rules inside a single frame. Scrolls.
     `.choicebar` a group of chips, each with its own frame. Wraps. Used for the basin strip
                  and the radar basemap picker, which were previously `.seg`-shaped and paid
                  for it by hiding options off the edge of a scroller.

   `.choicebar`/`.choice` were `.tabs`/`.tab` until the August 2026 audit (M-08). Nothing
   here is a tab: there is no tablist, no tabpanel and no arrow-key model, and the name was
   the reason the markup kept drifting towards half a tab pattern — `role="tablist"` with no
   `role="tab"` under it, then `aria-current` on one strip and nothing at all on another. A
   choice in a group of choices is a toggle button, `aria-pressed` says which one is on, and
   the class now says the same thing the ARIA does. */
.choicebar, .seg { display: inline-flex; max-width: 100%; }
.choice, .seg-opt { flex: none; }

/* `.seg` is one instrument, and an instrument never wraps.
 *
 * Wrapping does not degrade a segmented control gracefully, it breaks it: the frame is one
 * border round the whole thing while the rows inside are ragged, so the space left at the
 * end of a row reads as empty cells. It scrolls instead. Two reading levels always fit, so
 * this is a guard rather than a behaviour a reader ever meets. */
.seg {
  gap: 0;
  border: 1px solid var(--edge);
  width: fit-content;
  flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
}

/* `.choicebar` is a group of CHIPS, and a chip group wraps.
 *
 * It used to be the same joined instrument, scrolling, and this is what the scroll bought:
 * seven basins measure 680px, so every viewport under 900px showed four of them and hid
 * three past an edge with the scrollbar suppressed — and at 1024px the dashboard's rail
 * takes the pane back under 680px, hiding them again on a desktop. A choice a reader
 * cannot see is a choice they do not have.
 *
 * Wrapping it means giving up the single frame, which was the entire argument for the
 * scroller. Each chip carries its own closed hairline frame instead, so a short last
 * row reads as a short row rather than as phantom cells inside a broken box — and there is
 * no width at which a basin is unreachable. */
.choicebar { flex-wrap: wrap; gap: var(--sp-1); }
.choice, .seg-opt > span {
  display: block; cursor: pointer; font: inherit;
  font-family: var(--font-body); font-size: var(--fs-bulletin); font-weight: 600;
  letter-spacing: 0; text-transform: none; white-space: nowrap;
  color: var(--muted); background: var(--paper);
  border: 0; border-right: 1px solid var(--edge);
  padding: 8px var(--sp-4);
}
/* The segmented instrument used to be rounded on its ends only, so its cells read as one
   control rather than a row of separate pills. Square, that reading comes from the shared
   frame and the single internal hairline instead — which is how a ruled table does it. */
.seg-opt:last-child > span { border-right: 0; }
/* A chip is a whole control, not a cell of one: its own closed frame on four sides, in
   --edge because a control boundary owes 3:1 and, with one plane under it, the frame is
   now the only thing separating it from the page. This is what lets the group wrap without
   looking broken. */
.choicebar .choice { border: 1px solid var(--edge); }

/* What is in the region this chip opens.

   Seven regions were offered at identical weight while five of them held nothing, so the
   only way to find out which was worth a tap was to tap it — three rows of controls on a
   phone, ahead of the first storm number, none of them saying anything. The number is
   storms; an area of possible formation is not a storm, and it is named in the chip's
   accessible label rather than folded into the count (the same rule the rail's own count
   follows). A region with nothing in it keeps its place and says so. */
.choice .choice-n {
  margin-left: var(--sp-2); font-family: var(--font-mono); font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums; color: var(--muted);
}
/* The count rides the reversed ground, so it takes the reversed ink rather than the
   status token it borrowed while that ground was the accent. */
.choice[aria-pressed="true"] .choice-n { color: var(--paper); }
/* The chip that holds nothing is the one a reader can skip, and saying so was left to a rule
   that styled a child the empty chip never renders: `.choice-n` is emitted only when the count
   is above zero, so `.choice--empty .choice-n` matched nothing and all seven basins arrived at
   identical weight — the "muted" half of the intent stated beside `choicebar` in app.js never
   landed. Recede the chip itself, and do it without touching its contrast: the label keeps
   `--muted`, which is documented AA on the canvas as well as on the card, and loses only the
   surface and the weight that made an empty basin look like somewhere worth going. The
   selected chip is unaffected either way — `.choice[aria-pressed="true"]` carries an attribute
   and outranks this. */
.choice--empty { background: transparent; font-weight: 500; }
.choice:hover, .seg-opt > span:hover { color: var(--ink); }
/* One selector, and it is the one a screen reader reads. The state used to be carried by
   an `.active` class *and* an `aria-current`, which is two sources of truth for one fact
   and exactly how the radar basemap strip ended up painted-but-silent: it had the class
   and none of the ARIA. Paint from `aria-pressed` and that cannot happen again. */
/* A pressed segment reverses. That is the system's one emphasis device, and it is the same
   one the primary button uses, so "chosen" and "do this" read as the same weight of claim. */
.choice[aria-pressed="true"] { background: var(--ink); color: var(--paper); }
/* The selected chip's own hairline goes with its ground, or the frame stays grey around a
   filled cell and reads as a control that is both on and off. */
.choicebar .choice[aria-pressed="true"] { border-color: var(--ink); }

.seg-opt { position: relative; display: block; }
.seg-opt + .seg-opt > span { border-left: 0; }
.seg-opt input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.seg-opt input:focus-visible + span { outline: var(--focus-ring); outline-offset: -2px; }

/* Micro, uppercase, tracked — the same three-part register the navigation takes, and for
   the same reason. Padding and the 44px targets are unchanged: the label got smaller, the
   thing a finger lands on did not. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-micro);
  letter-spacing: var(--track-micro); text-transform: uppercase;
  padding: 9px var(--sp-4); cursor: pointer;
  border: 1px solid var(--edge); background: var(--paper); color: var(--ink);
  text-decoration: none; transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.btn:hover { border-color: var(--ink); color: var(--ink); background: var(--wash-strong); text-decoration: none; }
/* The primary action reverses: ink ground, canvas text. It cannot fail contrast in one
   appearance and pass in the other, because it is the body pair swapped — which is exactly
   the bug the retired `--on-accent` token existed to patch, when a hardcoded `color: #fff`
   on this rule measured 5.17:1 on the light accent and 2.20:1 on the dark one. */
/* The primary action carries the committed enamel — the one place the interface spends its
   colour on "do this". `--on-enamel` is measured against the enamel in both appearances
   (14.17:1 light, 10.36:1 dark), so it cannot pass in one and fail in the other, and the
   enamel is never a ramp colour, so a filled action can never be read as a category. */
.btn--primary { background: var(--enamel); border-color: var(--enamel); color: var(--on-enamel); }
.btn--primary:hover { background: var(--enamel-strong); border-color: var(--enamel-strong); color: var(--on-enamel); }
.btn--small { padding: 5px 10px; font-size: var(--fs-bulletin); }
/* The one action a page wants read before anything else beside it. */

.input, .topsearch-input {
  width: 100%; min-height: 42px; padding: 0 var(--sp-3); font: inherit; font-size: var(--fs-bulletin);
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--edge);
}
.input::placeholder, .topsearch-input::placeholder { color: var(--muted); }
.input:focus, .topsearch-input:focus {
  border-color: var(--ink); border-width: 2px; outline: var(--focus-ring); outline-offset: var(--focus-offset);
}

/* Checkboxes, ranges and selects follow the accent so a control never reverts to the
   browser's own blue mid-panel. */
/* The browser's own controls follow the palette rather than the OS. */
input[type="checkbox"], input[type="range"] { accent-color: var(--enamel); }

/* ============================================================================
   Badges, tags, metadata
   ==========================================================================*/

.tag {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fs-bulletin); font-weight: 600; letter-spacing: 0; text-transform: none;
  padding: 2px var(--sp-2); line-height: 1.6;
  background: var(--neutral-100); color: var(--neutral-800);
  border: 1px solid transparent;
}
.tag--outline, .tag--quiet {
  background: transparent; border-color: var(--divider);
  color: var(--muted); text-transform: none; letter-spacing: 0.02em;
}
.tag--accent, .tag--alert { background: var(--wash-strong); color: var(--ink); border: 1px solid var(--line-strong); }
.tag--ok { background: var(--wash-strong); color: var(--ink); border: 1px solid var(--line-strong); }
.tag--warn { background: var(--wash); color: var(--ink); border: 1px solid var(--ink); font-weight: 700; }
.tag--code {
  font-family: var(--font-mono); text-transform: none; letter-spacing: 0;
  background: var(--code-bg); color: var(--muted);
}

/* Status badge — the shape the radar feed health, the /nws severity chips and the
   freshness marks all share. */
/* A badge was 9.5px uppercase mono — under the smallest size this site should set text
   at, in the voice the redesign is removing. It is a small sentence-case label now, and it
   is 11px, which is the floor. */
.badge {
  display: inline-block; font-size: var(--fs-micro); font-weight: 600;
  letter-spacing: 0; text-transform: none; white-space: nowrap;
  padding: 2px 7px; border: 1px solid var(--line); color: var(--muted);
}
.badge--ok { color: var(--ink); border-color: var(--line-strong); }
.badge--warn { color: var(--ink); border-color: var(--edge); font-weight: 700; }
.badge--critical { color: var(--paper); background: var(--ink); border-color: var(--ink); }

/* Measured readouts. */
.stat { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
/* The label names a quantity; the value *is* one. So the label is Public Sans and the value
   is mono — the two halves of a stat tile deliberately do not share a face. */
.stat-label {
  font-family: var(--font-body); font-size: var(--fs-bulletin); font-weight: 600;
  letter-spacing: 0; text-transform: none; color: var(--muted);
}
.stat-value { font-size: var(--fs-bulletin); font-weight: 650; color: var(--ink); }
.stat-value .u { font-size: var(--fs-micro); font-weight: 400; color: var(--muted); margin-left: 2px; }
.stat-sub { font-size: var(--fs-micro); color: var(--muted); }

/* Corner registration marks — the viewfinder brackets on a framed map. Decorative. */
.corner { position: absolute; width: 11px; height: 11px; z-index: 2; pointer-events: none; }
/* Toned from a 2px accent bracket to a 1px hairline. They were the loudest piece of the
   old instrument character — four blue crosshairs on every framed graphic — and Clear Sky
   wants the graphic to dominate, not its frame. Kept rather than deleted because four
   renderers emit them and they do read as a viewfinder at this weight. */
.corner.tl { top: 6px; left: 6px; border-top: 1px solid var(--line-strong); border-left: 1px solid var(--line-strong); }
.corner.tr { top: 6px; right: 6px; border-top: 1px solid var(--line-strong); border-right: 1px solid var(--line-strong); }
.corner.bl { bottom: 6px; left: 6px; border-bottom: 1px solid var(--line-strong); border-left: 1px solid var(--line-strong); }
.corner.br { bottom: 6px; right: 6px; border-bottom: 1px solid var(--line-strong); border-right: 1px solid var(--line-strong); }

/* ============================================================================
   States — outage, staleness, emptiness
   ==========================================================================*/

/* Invariant 4: an outage is never allowed to look like "no active storms", and a stalled
   pipeline is never allowed to look like a quiet one. Both say so here, in one element,
   sticky under the masthead so the warning is on screen at any scroll position —
   including below the breakpoint where the rail stops being sticky and used to carry a
   banner parked inside it off the top of the screen. */
.databanner {
  position: sticky;
  top: var(--masthead-h);
  z-index: 20;
  margin: 0;
  padding: var(--sp-3) var(--gutter);
  border-bottom: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-body);
  font-size: var(--fs-remarks);
  font-weight: 600;
}
.databanner a { color: var(--paper); }
/* A page-wide banner is for a page-wide problem. One source behind, or one storm's
   advisory aging, belongs beside that source or that storm — see `.freshness` below.
   The rule is in the redesign brief and it is the difference between a reader who knows
   which thing is stale and a reader who only knows that something is. */
/* One source behind is a caution, not the whole product down, so it is ruled rather than
   reversed — the same ladder `.freshness--attached` uses, for the same reason. */
.databanner--warn {
  border-bottom-color: var(--edge);
  background: var(--wash);
  color: var(--ink);
}
.databanner--warn a { color: var(--ink); }
/* A storm that ended is not a fault, and the bar that says so must not read like one. The
   alarm palette is reserved for "something is wrong and someone should look"; a final
   warning is the system working. Same bar, same position, informational surface. */
.databanner--note {
  border-bottom-color: var(--divider);
  background: var(--paper);
  color: var(--ink);
  font-weight: 500;
}
.databanner[hidden] { display: none; }
/* The printed rank marker a maintenance notice of kind `incident` carries on the reversed
   bar (A17.5). The value register, so it reads as a mark rather than a word; it is
   aria-hidden in the markup because the reversal and the title carry the meaning, not the
   glyph. `.databanner-body` sets the notice's detail in the regular weight beside its bolder
   title. */
.databanner-mark { font-family: var(--font-mono); font-weight: 700; }
.databanner-body { font-weight: 400; }

.notice {
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  padding: var(--sp-5) 0;
}
.notice.error { border-color: var(--ink); border-width: 2px; background: var(--wash); }

/* Wide content scrolls inside its own box; the page body never scrolls sideways. The
   API reference's endpoint table is 800px of monospace paths and cannot be made narrow
   without becoming unreadable, so it gets a scroller rather than a reflow. */
.table-scroll { overflow-x: auto; margin: var(--sp-3) 0; border: 1px solid var(--divider); }
.table-scroll > table { margin: 0; }

.empty {
  color: var(--muted); padding: var(--sp-7);
  border: 1px dashed var(--divider); text-align: center;
}

/* The shape of "there is nothing to draw here", wherever it happens.
 *
 * Built by `MaverickShell.stateBlock` and styled once, because the defect it answers was a
 * *size*: a model panel with zero models kept the full-width frame a plot would have had,
 * which reads as a graphic that failed to paint (audit M-11). Nothing here reserves the
 * space of the missing thing — no aspect ratio, no min-height, no ground the size of a map.
 * The panel is as tall as the sentence it contains.
 *
 * Informational, never the alarm palette: a cycle that published no guidance is the
 * pipeline working, and `.notice.error` is reserved for something that needs looking at. */
.statebox {
  padding: var(--sp-4) var(--sp-5);
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2);
}
.statebox-cause { margin: 0; color: var(--ink-2); max-width: var(--wrap-prose); }
/* The cycle and the source. Mono because both are measured values a reader matches against
   the graphics and the provenance block, which print them the same way. */
.statebox-meta {
  margin: 0; font-size: var(--fs-bulletin); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.statebox-note { margin: 0; font-size: var(--fs-bulletin); color: var(--muted); max-width: var(--wrap-prose); }
.statebox .btn { margin-top: var(--sp-1); }

.updated { color: var(--muted); font-size: var(--fs-bulletin); margin-top: var(--sp-5); }
.updated.stale { color: var(--ink); font-weight: 700; }

/* Loading placeholders that hold the shape the real content will take.
 *
 * Not decoration: the storm overview measured a Cumulative Layout Shift of 0.807 before
 * these existed, because it painted one line of text and then replaced the whole page with
 * a two-column layout. Reserving the columns, the rows and the map's aspect ratio takes it
 * to near zero — the content lands in the space already held for it.
 *
 * A gentle pulse rather than a sweeping shimmer, and none at all under reduced motion
 * (the global rule below neutralises the animation). */
.skeleton { background: var(--skeleton); }
.skeleton--row { height: 74px; margin-bottom: var(--sp-3); }
.skeleton--wide { height: 46px; }
.skeleton--tabs { height: 40px; margin-bottom: var(--sp-3); }
/* The frame that will hold a published overview master. Reserving the wrong ratio would
   trade one shift for a smaller one, so this number is measured rather than chosen:
   `tests/render/test_web_frame_parity.py` reads it out of this file and fails if it drifts
   from what the renderer actually draws.

   It was 1.8 when the masters were 2400x1333. The overview's chrome went from 0.295 of the
   figure to 0.13 (`app.render.overview.overview_chrome`), and since the map's pixel size is
   fixed by its own geographic aspect, the same cartography came back in a shorter frame —
   2400x1429 for the reference render, so 1.68. A shorter image at the same column width is
   the whole point: it is less of the page for the same map. */
.skeleton--map { aspect-ratio: 1.68; width: 100%; }
/* The storm table only precedes the figure below 640px, so only there does its space need
   holding — see the markup in `index.html`. */
.skeleton--readout { display: none; }

@media (max-width: 639px) {
  /* Below 640px the frame holds the *phone* rendition, which is a different picture with a
     different shape: it spends a larger share of its height on chrome, because its type is
     4.5% of the width rather than 1.0%. A ratio that drifts from the image is a layout shift
     on the largest element on the page, so this is measured too — same parity test.

     1.60, and it used to be 1.37. The two frames have moved much closer together, which is
     the chrome change showing up as arithmetic: the compact frame spent 0.46 of itself on
     bands against the master's 0.295, and now spends 0.17 against 0.13. Most of what made
     the phone graphic a differently-shaped picture was the bands, not the picture. */
  .skeleton--map { aspect-ratio: 1.60; }
  /* Roughly two stacked system cards and their heading — the common case for an active
     basin. It cannot be exact (the count is not known until the manifest lands), and it does
     not need to be: what CLS charges for is the *footer* arriving inside the viewport and
     then being pushed out, and reserving approximately the right block keeps it outside from
     the first paint. */
  .skeleton--readout { display: block; height: 320px; margin-bottom: var(--sp-4); }
}
.skeleton--table { min-height: 62vh; margin-top: var(--sp-4); }

/* A loading page must be at least as tall as the one it becomes.
 *
 * The remaining shift on the storm overview and the archive was not the font swap — it was
 * the footer. A short skeleton leaves the footer inside the viewport at first paint; the
 * real content then pushes it out, and every pixel it travels is counted. Holding the work
 * surface to the height of the viewport puts the footer below the fold from the start,
 * which is where it is on the loaded page anyway. */
.shell .skeleton--map { min-height: min(58vh, 640px); }
[data-archive-results] > .skeleton { display: block; }
@media (prefers-reduced-motion: no-preference) {
  .skeleton { animation: skeleton-pulse 1.6s var(--ease) infinite; }
}
@keyframes skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* ============================================================================
   Footer
   ==========================================================================*/

/* The footer is where About, methodology, sources, status and the legal/safety line live
   now that the primary nav is five reader destinations. It is a real block of links rather
   than one run-on sentence, because six things were being separated by middots on a single
   wrapped line and none of them were findable. */
footer {
  border-top: 1px solid var(--line);
  background: var(--paper);
  color: var(--muted);
  margin-top: var(--sp-8);
}
footer .shell-inner {
  padding-block: var(--sp-7); font-size: var(--fs-bulletin);
  display: grid; gap: var(--sp-6);
  grid-template-columns: minmax(220px, 1fr) repeat(auto-fit, minmax(150px, auto));
  align-items: start;
}
footer .brand { font-size: var(--fs-remarks); color: var(--ink); margin-bottom: var(--sp-2); }
footer a { color: var(--muted); }
footer a:hover { color: var(--ink); }

.footer-about { max-width: 46ch; }
.footer-about p { margin: 0 0 var(--sp-2); line-height: 1.5; }
.footer-safety { color: var(--ink-2); font-weight: 600; }
.footer-col h2 {
  font-size: var(--fs-bulletin); font-weight: 700; color: var(--ink); margin: 0 0 var(--sp-2);
  letter-spacing: 0;
}
.footer-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
/* WCAG 2.2 AA 2.5.8. These are a list of destinations, not links inside a sentence, so the
   criterion's inline exception does not cover them and 18px is not enough. The prose links
   in `.footer-about` are inside sentences and are left alone. */
.footer-col ul a { display: inline-flex; align-items: center; min-height: 24px; }
@media (max-width: 640px) {
  footer .shell-inner { grid-template-columns: 1fr 1fr; }
  .footer-about { grid-column: 1 / -1; }

  /* On a phone the footer was 613 of a 2,442px document — a quarter of the page — and
     three of its nine links went where the masthead nav already goes: Storm archive is
     ARCHIVE, NWS Guide is NWS GUIDE, Storm API is DEVELOPERS. A reader who has scrolled
     past everything the page had to say does not need a second copy of the bar that has
     been stuck to the top of the screen the whole way down.

     So `.footer-secondary` stands down below 640px, and nothing becomes unreachable:
     Archive, the Guide and the API are one tap away in the masthead; JSON schema and
     Sitemap are on the API page those three columns pointed at; and NOAA/NHC and JTWC are
     named in the paragraph directly above, which is where attribution belongs — the
     guardrail asks that the data be attributed, not that every agency be hyperlinked twice.
     What stays is what has no other route from here: About & methodology, Data health, the
     safety line and weather.gov. `tests/web/test_naming.py` holds `/status` in the footer
     of every page, which is the one of those that a runbook points at. */
  .footer-secondary { display: none; }
}
/* A workspace fills the viewport and scrolls internally; a footer hung below it sits
   somewhere nobody scrolls to. Those sections carry their attribution in the panel foot. */
body[data-layout="workspace"] > footer { display: none; }

/* ============================================================================
   Freshness & health — ONE presentation for six states
   ==========================================================================*/

/* The redesign brief asks for shared wording and a shared presentation for: current,
   aging, stale, source unavailable, product degraded, and last-coherent-state-retained.
   Before this each surface invented its own — the storm rail said "7 HRS AGO" in
   uppercase mono, radar said "unavailable" in a filled badge, /nws said "Data checked …",
   and /status said something else again.

   Three rules the component enforces so no caller can get them wrong:

     1. Never colour alone. Every state renders a dot AND a word. The dot is decoration
        that repeats what the text already says; strip the colour and the sentence is
        unchanged. `tests/web/test_contrast.py` is why the dot may be a brand value that
        could not carry text: it never carries any.

     2. The time is always available. A state without a timestamp is an opinion. Callers
        pass the advisory/source time and it renders in mono beside the word, or as a
        `title`/`datetime` on the <time> when the row is too narrow for both.

     3. "Retained" is not a fault. `--held` uses the informational surface, not the alarm
        palette: showing the last good state on purpose is the system working (invariant
        5), and painting it red teaches readers to ignore red.

   Markup contract:
     <p class="freshness" data-state="current|aging|stale|unavailable|degraded|held">
       <span class="freshness-dot" aria-hidden="true"></span>
       <span class="freshness-text">Advisory 4 hours old</span>
       <time class="mono" datetime="…">18:00Z</time>
     </p> */
.freshness {
  display: inline-flex; align-items: baseline; gap: var(--sp-2);
  margin: 0; font-size: var(--fs-bulletin); color: var(--muted); line-height: 1.5;
  flex-wrap: wrap;
}
/* The dot was a coloured circle; it is a printed marker now.
 *
 * Removing the colour costs nothing semantically — the dot was already `aria-hidden` and
 * rule 1 of this component has always been that the state is spelled out beside it — but it
 * removes a real defect. `--warn-mark` (#b86a12) sat twelve pixels below `--wx-tier-c2`
 * (#ff9f1c) in every rail row, meaning "this advisory is late" and "Category 2" in the same
 * hue. That is the collision `DESIGN.md` names as the reason this system exists, and taking
 * the blue accent out last week left it standing alone and louder than before.
 *
 * A fixed-width slot so the markers line up down a list of storms, and rank carried by how
 * many characters are struck rather than by hue: nothing, one, two. */
.freshness-dot {
  flex: none; align-self: baseline;
  min-width: 1.1em; text-align: left;
  font-family: var(--font-mono); font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
  color: var(--muted);
}
.freshness-dot::before { content: "·"; }
.freshness time, .freshness .mono {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
   color: var(--muted);
}
/* `current` keeps the resting marker: the ordinary case should spend none of the reader's
   attention, which is why it was never coloured green here either. `held` keeps it too —
   showing the last good state on purpose is invariant 5 working, and marking it as an
   exception is how a reader learns to ignore exceptions. The word carries the difference,
   as it always has. */
.freshness[data-state="aging"] .freshness-dot::before { content: "!"; }
.freshness[data-state="stale"] .freshness-dot::before,
.freshness[data-state="unavailable"] .freshness-dot::before,
.freshness[data-state="degraded"] .freshness-dot::before { content: "!!"; }
.freshness[data-state="aging"] .freshness-dot,
.freshness[data-state="stale"] .freshness-dot,
.freshness[data-state="unavailable"] .freshness-dot,
.freshness[data-state="degraded"] .freshness-dot { color: var(--ink); font-weight: 700; }

/* Emphasis from "aging" onwards, by weight rather than hue. */
.freshness[data-state="aging"] .freshness-text { color: var(--ink); font-weight: 600; }
.freshness[data-state="stale"] .freshness-text,
.freshness[data-state="unavailable"] .freshness-text,
.freshness[data-state="degraded"] .freshness-text { color: var(--ink); font-weight: 700; }
.freshness[data-state="held"] .freshness-text { color: var(--ink-2); font-weight: 600; }

/* The attached form: a warning that belongs to one storm or one source, sitting with it
   rather than in a page-wide banner. */
.freshness--attached {
  display: flex; gap: var(--sp-2); align-items: flex-start;
  padding: var(--sp-2) var(--sp-3);
  background: var(--wash); border: 1px solid var(--line);
}
/* Aging is ruled, not reversed: it is a caution, and reversal is the loudest thing this
   system can do to a block of text. Spending it here would leave nothing above it. */
.freshness--attached[data-state="aging"] {
  background: var(--wash); border-color: var(--ink);
}
/* Stale, unavailable and degraded reverse. Paper on ink survives greyscale, a colour-vision
   difference and a forced-colours mode identically, and unlike a red tint it can never be
   mistaken for a Saffir-Simpson tier — the ramp already owns #f4632e and #e01e5a. */
.freshness--attached[data-state="stale"],
.freshness--attached[data-state="unavailable"],
.freshness--attached[data-state="degraded"] {
  background: var(--ink); border-color: var(--ink); color: var(--paper);
}
.freshness--attached[data-state="stale"] .freshness-text,
.freshness--attached[data-state="unavailable"] .freshness-text,
.freshness--attached[data-state="degraded"] .freshness-text,
.freshness--attached[data-state="stale"] .freshness-dot,
.freshness--attached[data-state="unavailable"] .freshness-dot,
.freshness--attached[data-state="degraded"] .freshness-dot,
.freshness--attached[data-state="stale"] time,
.freshness--attached[data-state="unavailable"] time,
.freshness--attached[data-state="degraded"] time,
.freshness--attached[data-state="stale"] .mono,
.freshness--attached[data-state="unavailable"] .mono,
.freshness--attached[data-state="degraded"] .mono { color: var(--paper); }
.freshness--attached[data-state="stale"] a,
.freshness--attached[data-state="unavailable"] a,
.freshness--attached[data-state="degraded"] a { color: var(--paper); }
.freshness--attached .freshness-dot { margin-top: 1px; }

/* ============================================================================
   Accessibility & print
   ==========================================================================*/

/* Touch targets. Anything tappable clears 44px in the coarse-pointer case, which the
   radar row checkboxes and the segmented cells did not before. */
@media (pointer: coarse) {
  .choice, .seg-opt > span, .btn, .panel-item { min-height: 44px; display: flex; align-items: center; }
  .panel-item { display: block; padding-block: var(--sp-3); }
  input[type="checkbox"] { width: 20px; height: 20px; }
  /* The three the first pass missed. Each was already raised to 24px against WCAG 2.2
     2.5.8 and passes it; what they did not meet is the stricter bar this document sets
     for itself two sections up, and a criterion floor is not the same as a promise kept.
     Only under a coarse pointer: on a mouse the tighter sizes are the right density, and
     the 24px minimums stay exactly where their own comments explain them. */
  .drawer-close { width: 44px; height: 44px; }
  /* Full width of its column, not the width of its word. `.footer-col ul` is a grid whose
     items already span the column, so the only thing between "JTWC" and a 35px target was
     the anchor shrinking to its text. Below 640px the footer is two columns, so a filled
     row is ~170px wide and still cannot collide with its neighbour across the gap. */
  .footer-col ul a { display: flex; min-height: 44px; }
  /* The gap was separating 24px rows. At 44 the row separates itself, and keeping both
     spent ~64px of a phone's footer on air between things that were already apart —
     flush 44px rows are what a native list does, and the text sits further apart than it
     did before even with the gap gone. */
  .footer-col ul { gap: 0; }
}

/* Reduced motion takes the movement, not the feedback.
 *
 * This used to end `transition-duration: 0.01ms !important` on every element, which is the
 * common recipe and is heavier than the preference asks for. What 2.3.3 and the vestibular
 * research are about is *motion* — things travelling across the screen. A control that
 * changes colour when it is pressed is not moving, and killing that transition too removes
 * a state change the reader still needs, on the one setting chosen by people most likely to
 * be relying on it.
 *
 * So animation still stops outright: it is where anything looping or self-starting lives,
 * and the one animation here (the skeleton pulse) is already gated on `no-preference` and
 * never runs under this query anyway. Transitions keep their authored durations but are
 * restricted to the properties that paint rather than move, which drops `transform` — the
 * drawer slide at 0.2s is the only movement on the storms page — while `background`,
 * `border-color`, `color` and the focus ring survive. An element with no transition of its
 * own still has a 0s duration and animates nothing.
 */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-property: background-color, border-color, color, fill, stroke, box-shadow,
      outline-color, opacity !important;
  }
}

@media print {
  .masthead, .panel, .panel-toggle, .drawer-scrim, .databanner, .skip-link { display: none; }
  body[data-layout="workspace"] .shell { display: block; height: auto; overflow: visible; }
  .stage, .stage--scroll { overflow: visible; height: auto; }
  .pagehead { border-bottom: 1px solid #999; }
}

/* ============================================================================
   Collapsing the workspace panel
   ==========================================================================*/

/* A desktop reader who has built the map they want should be able to give the whole
   viewport to it. The panel is a grid column, so collapsing it is a change to one
   `grid-template-columns` — the map keeps its WebGL context, its sources and every loaded
   tile, and the library is only told its box changed size. Re-creating a map to reflow it
   would flash and re-request tiles from eleven third parties for a panel toggle.

   Only above the drawer breakpoint. Below it the panel is already a sheet that is
   off-screen, so a collapse control would appear to do nothing; the button is hidden there
   and the script guards the same condition. */
@media (min-width: 901px) {
  body[data-layout="workspace"] .shell.is-panel-collapsed {
    grid-template-columns: 0 minmax(0, 1fr);
  }
  .shell.is-panel-collapsed > .panel {
    /* Not `display: none`: the panel keeps its DOM, its scroll position and its rendered
       rows, so re-opening it is instant and nothing has to be rebuilt. `inert` (set by the
       page script) is what takes it out of the tab order. */
    overflow: hidden;
    border-right: 0;
    padding-inline: 0;
    visibility: hidden;
  }
}
@media (max-width: 900px) {
  /* Below the breakpoint the panel is a drawer; there is nothing to collapse. */
  .radar-collapse { display: none; }
}
