/* ============================================================================
   mco-web-style · theme/mco-theme.css · v0.1.0
   The Montana Climate Office shared web house style.

   Consumed via pinned CDN tag + SRI (see README.md). Rules for using and
   extending this file live in HOUSE-STYLE.md; guardrails in AGENTS.md.

   Layers, top to bottom:
     1. Design tokens        — dark (:root), light, high-contrast themes
     2. Stacking ladder      — z-index tiers for every floating surface
     3. Reset & accessibility utilities
     4. MapLibre control polish
     5. Component shells     — navbar, toast, tooltip, modal, panel, scrim
     6. Responsive & touch

   Fonts ARE loaded here (since 0.7.0): Outfit + Space Mono are kit-hosted
   in fonts/ and declared at the top of §1, resolved relative to this file so
   they version with it. Consumers preload the two latin files from
   snippets/head.html; the tokens carry system fallbacks.
   ========================================================================== */

/* ── Fonts ────────────────────────────────────────────────────────────────
   Kit-hosted latin + latin-ext subsets of the Google Fonts builds (SIL OFL;
   license texts in fonts/). url() resolves against THIS stylesheet, so a page
   pinned to @X.Y.Z gets that tag's fonts. The latin files should be preloaded
   (snippets/head.html); latin-ext is fetched only when a page actually uses
   a character in its unicode-range.

   font-display: block, not swap. From Google Fonts with display=swap every
   MCO app painted its text in the system font and re-set it in Outfit a beat
   later — the title visibly jumped. Preloaded from the same origin as this
   render-blocking stylesheet, the fonts normally arrive before first paint;
   when they don't, block costs a few ms of invisible text rather than a
   re-set of every line, and the browser still falls back after ~3s if a file
   never arrives. Outfit is a variable font: one file per subset covers every
   weight in use (400–700). */
@font-face {
  font-family: 'Outfit'; font-style: normal; font-weight: 400 700; font-display: block;
  src: url(../fonts/outfit-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: 'Outfit'; font-style: normal; font-weight: 400 700; font-display: block;
  src: url(../fonts/outfit-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;
}
@font-face {
  font-family: 'Space Mono'; font-style: normal; font-weight: 400; font-display: block;
  src: url(../fonts/space-mono-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: 'Space Mono'; font-style: normal; font-weight: 400; font-display: block;
  src: url(../fonts/space-mono-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;
}

/* Metric-matched fallbacks (0.7.1). During the block period text is invisible
   but still LAID OUT — in the next family in the stack. Plain Arial sets the
   navbar title ~6% wider and its line box 3px shorter than Outfit, so items
   beside the title shifted when the webfont landed (seen on mesonet-status
   under a throttled connection). These faces rescale a local metric-stable
   font (Arial; Helvetica and Liberation Sans share its metrics) to Outfit's
   proportions, so the layout reserved during the block period is the one
   Outfit then fills.
   - size-adjust: Outfit/Arial advance-width ratio over a corpus of real MCO UI
     strings, measured in Chrome per weight, because Outfit's advances move
     with its weight axis and Arial only has 400 and 700.
   - ascent/descent/line-gap-override: Outfit's own metrics (1.000 / 0.260 /
     0, Space Mono 1.120 / 0.361 / 0) divided by size-adjust, which scales the
     overrides too. Line boxes then match exactly (verified: 15px vs 15px,
     where plain Arial gave 12px).
   - The ITALIC face is fitted to the one italic string in the house style,
     the lockup subtitle ("A service of the Montana Climate Office", §1 —
     every public MCO app carries it),
     which sets the lockup's width in most apps: 169.3px vs Outfit's 169.4px
     (letter-spacing excluded from the fit, since size-adjust doesn't scale
     it). Outfit has no italic, so the real face is a synthesized oblique of
     the upright — same advances as upright.
   Line boxes match exactly in every face. Widths: upright text lands within a
   few percent either way, since no single scale turns Arial's letterforms
   into Outfit's — uppercase Outfit is ~6% narrower than Arial capitals, so an
   uppercase title (weight 500) still reserves ~3–5% too much. Measured on
   all six consumers under a throttled connection (150 ms, 200 KB/s), the
   navbar item beside the lockup moved 16.6px when Outfit landed with plain
   Arial and 0.1px with these faces; mesonet-maintenance, whose uppercase
   title is wider than the subtitle, 11.9px → 5.2px. Space Mono vs Courier
   New is exact (both monospaced: 0.612em vs 0.600em advance). */
@font-face {
  font-family: 'Outfit Fallback'; font-style: normal; font-weight: 400 500;
  src: local('Arial'), local('ArialMT'), local('Helvetica'), local('Liberation Sans');
  size-adjust: 98.80%; ascent-override: 101.21%; descent-override: 26.32%; line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Outfit Fallback'; font-style: normal; font-weight: 600;
  src: local('Arial Bold'), local('Arial-BoldMT'), local('Helvetica Bold'), local('Liberation Sans Bold');
  size-adjust: 95.06%; ascent-override: 105.20%; descent-override: 27.35%; line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Outfit Fallback'; font-style: normal; font-weight: 700;
  src: local('Arial Bold'), local('Arial-BoldMT'), local('Helvetica Bold'), local('Liberation Sans Bold');
  size-adjust: 96.64%; ascent-override: 103.48%; descent-override: 26.90%; line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Outfit Fallback'; font-style: italic; font-weight: 400 500;
  src: local('Arial Italic'), local('Arial-ItalicMT'), local('Helvetica Oblique'), local('Liberation Sans Italic');
  size-adjust: 100.58%; ascent-override: 99.42%; descent-override: 25.85%; line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Space Mono Fallback'; font-style: normal; font-weight: 400;
  src: local('Courier New'), local('CourierNewPSMT'), local('Liberation Mono');
  size-adjust: 101.98%; ascent-override: 109.83%; descent-override: 35.40%; line-gap-override: 0.00%;
}

/* ── 1. Design tokens ─────────────────────────────────────────────────────
   Dark is the default theme. `data-theme` on <html> switches; the anti-flash
   snippet (snippets/anti-flash.html) sets it before first paint.

   Contrast contract (enforced by tools/check-contrast.mjs in CI):
     --text-primary, --text-secondary  ≥ 4.5:1 on deep, surface, AND raised
     --text-muted,  --text-dim         ≥ 4.5:1 on deep and surface ONLY —
                                         never use them on --bg-raised
     --accent-line                     ≥ 3:1 on all three surfaces (1.4.11)
     --text-on-accent                  ≥ 4.5:1 on --accent
     --danger, --warning, --success    ≥ 4.5:1 on deep, surface, raised, and
                                          on their own --*-fill
     --text-on-danger/-warning/-success ≥ 4.5:1 on the matching --*-fill

   --accent is the MCO brand blue. On dark surfaces it only clears ~2.3–2.9:1,
   so it is a FILL color: use it behind --text-on-accent. For borders, icons,
   or text that must read against a surface, use --accent-line. */
:root {
  --font-ui:        'Outfit', 'Outfit Fallback', system-ui, -apple-system, sans-serif;
  --font-mono:      'Space Mono', 'Space Mono Fallback', ui-monospace, monospace;
  /* Display-size readings (≥ 1.75rem hero and tile values, 0.10.0): Outfit,
     with tabular figures from .mco-num-display. Space Mono stays for every
     other numeral (tables, IDs, timestamps, axes, scale labels, <kbd>). */
  --font-display-num: 'Outfit', 'Outfit Fallback', system-ui, -apple-system, sans-serif;

  --bg-deep:        #161b22;
  --bg-surface:     #1e2530;
  --bg-raised:      #2c3545;
  --border:         #3a4558;
  --border-glass:   rgba(255,255,255,0.08);
  --text-primary:   #e8ecf0;
  --text-secondary: #c0cad6;
  --text-muted:     #8a99b0;
  --text-dim:       #8494ab;
  --ctrl-border:    #6a7996;
  --accent:         #1a6faf;
  --accent-light:   #5aaee8;
  --accent-dk:      #114f80;
  --accent-line:    #5aaee8;
  --accent-hover:   rgba(26,111,175,0.10);
  --text-on-accent: #ffffff;
  --accent-fill-hover: #114f80;   /* .nav-btn.is-primary hover; --text-on-accent ≥ 4.5:1 on it */
  --selection-ring: #5aaee8;
  --brand-gradient: linear-gradient(90deg, var(--accent-dk), var(--accent), var(--accent-light), var(--accent));
  --glass:          rgba(30,37,48,0.92);   /* legible WITHOUT the blur (0.10.0) */
  --shadow:         0 4px 24px rgba(0,0,0,0.4);
  --transition:     0.2s cubic-bezier(0.4,0,0.2,1);
  --radius-sm:      4px;
  --radius-md:      8px;
  --radius-lg:      12px;
  --dot-stroke:     #ffffff;
  --input-bg:       #232a35;
  --scrim:          rgba(0,0,0,0.55);
  --ctrl-icon-filter: invert(1);
  /* Status tones (0.8.0) — CHROME, not data (HOUSE-STYLE §2): form errors,
     notices, the basemap-failure banner. Never the only channel: pair with
     a tone word or icon. Each tone is ≥ 4.5:1 on deep/surface/raised and on
     its own -fill; --text-on-* is ≥ 4.5:1 on that fill (check-contrast). */
  --danger:         #ed8780;
  --danger-fill:    #37313a;
  --text-on-danger: #e8ecf0;
  --warning:        #df9412;
  --warning-fill:   #35322c;
  --text-on-warning: #e8ecf0;
  --success:        #36ba6d;
  --success-fill:   #213737;
  --text-on-success: #e8ecf0;
}

[data-theme="light"] {
  --bg-deep:        #f0f2f5;
  --bg-surface:     #ffffff;
  --bg-raised:      #e8ecf0;
  --border:         #c8cdd5;
  --border-glass:   rgba(0,0,0,0.08);
  --text-primary:   #1a1a2e;
  --text-secondary: #3a3f4b;
  --text-muted:     #5a6070;
  --text-dim:       #5f6675;
  --ctrl-border:    #7e8694;
  --accent:         #1a6faf;
  --accent-light:   #1563a0;
  --accent-dk:      #0d4f7a;
  --accent-line:    #1563a0;
  --accent-hover:   rgba(26,111,175,0.08);
  --text-on-accent: #ffffff;
  --accent-fill-hover: #0d4f7a;
  --selection-ring: #1563a0;
  --brand-gradient: linear-gradient(90deg, #0a4a7a, #1266a8, #0d7fd4, #1266a8, #0a4a7a);
  --glass:          rgba(255,255,255,0.94);
  --shadow:         0 4px 24px rgba(0,0,0,0.1);
  --dot-stroke:     #2a2a3a;
  --input-bg:       #f4f5f7;
  --scrim:          rgba(0,0,0,0.45);
  --ctrl-icon-filter: none;
  --danger:         #c2271c;
  --danger-fill:    #f8e5e4;
  --text-on-danger: #1a1a2e;
  --warning:        #8a5b0b;
  --warning-fill:   #f1ebe2;
  --text-on-warning: #1a1a2e;
  --success:        #217343;
  --success-fill:   #e4eee8;
  --text-on-success: #1a1a2e;
}

/* High-contrast: pure-black surfaces, brightened accent, near-white text.
   Adapted from mco-drought-dashboard (the family's first HC theme), with the
   accent tints re-derived from THIS theme's accent (#5bb4ff = rgb(91,180,255))
   rather than the legacy value the dashboard still carries.
   --text-on-accent flips to black: white fails on the brightened accent. */
[data-theme="high-contrast"] {
  --bg-deep:        #000000;
  --bg-surface:     #000000;
  --bg-raised:      #121212;
  --border:         #8a8a8a;
  --border-glass:   rgba(255,255,255,0.28);
  --text-primary:   #ffffff;
  --text-secondary: #f5f5f5;
  --text-muted:     #dcdcdc;
  --text-dim:       #bcbcbc;
  --ctrl-border:    #8a8a8a;
  --accent:         #5bb4ff;
  --accent-light:   #93d0ff;
  --accent-dk:      #2070a8;
  --accent-line:    #93d0ff;
  --accent-hover:   rgba(91,180,255,0.15);
  --text-on-accent: #000000;
  --accent-fill-hover: #93d0ff;   /* lighter, not darker: black text fails on --accent-dk (3.9:1) */
  --selection-ring: #93d0ff;
  --brand-gradient: linear-gradient(90deg, #2a9aff, #6ec0ff, #a0d8ff, #6ec0ff, #2a9aff);
  --glass:          rgba(0,0,0,0.96);
  --shadow:         0 4px 24px rgba(0,0,0,0.7);
  --dot-stroke:     #ffffff;
  --input-bg:       #121212;
  --scrim:          rgba(0,0,0,0.8);
  --ctrl-icon-filter: invert(1);
  --danger:         #f47a71;
  --danger-fill:    #1d0f0e;
  --text-on-danger: #ffffff;
  --warning:        #dc8d06;
  --warning-fill:   #1a1101;
  --text-on-warning: #ffffff;
  --success:        #26b562;
  --success-fill:   #05160c;
  --text-on-success: #ffffff;
}

/* ── 2. Stacking ladder ───────────────────────────────────────────────────
   One ladder for every floating surface. Add to a tier — don't invent a
   number. MapLibre's own .maplibregl-ctrl-* containers ship with z-index 2
   from the vendor stylesheet, so anything meant to sit over the map controls
   (notably map popups, which ship with NO z-index) has to be above that.
   The detail tier stays BELOW the chrome so a docked detail surface can never
   cover the navbar; <dialog> lives in the browser's top layer and outranks
   all of these regardless. */
:root {
  --z-map-ctrl:      2;   /* vendor .maplibregl-ctrl-* containers          */
  --z-map-notice:   20;   /* empty-state / error callouts over the map     */
  --z-map-progress: 30;   /* loading bars and notes                        */
  --z-map-panel:    40;   /* docked panels (legend, sidebar) + scrim       */
  --z-detail:       60;   /* docked detail surface (station sheet etc.)    */
  --z-drawer:       70;   /* off-canvas drawer, over the detail surface    */
  --z-chrome:       90;   /* secondary chrome bars                         */
  --z-chrome-top:  100;   /* navbar                                        */
  --z-flyout:      150;   /* dropdowns / search results                    */
  --z-cursor:      200;   /* pointer-following tooltip                     */
  --z-toast:       400;   /* toast — above everything                      */
}

/* ── 2b. Type & spacing scale (0.9.0) ─────────────────────────────────────
   Theme-invariant, so :root only (tools/check-tokens.mjs knows them as the
   `scale` group, like the z-index ladder). Each step names a cluster the kit
   already uses, so adopting one is near-invisible. 0.9.0 ADDS them for apps;
   the kit's own rules move onto them in 1.0.0 (0.72→0.75 and 0.78→0.8125 are
   visible snaps, so that sweep is MAJOR). --nav-gap stays, defaulting to
   --space-4. */
:root {
  --fs-2xs:     0.55rem;   /* unused by the kit since 0.10.0 (the lockup subtitle is 11px) */
  --fs-xs:      0.65rem;   /* mono meta, tooltip sub */
  --fs-sm:      0.75rem;   /* brand title, panel titles, seg/tooltip, captions */
  --fs-md:      0.8125rem;   /* buttons, toast, eyebrow h3 */
  --fs-base:    0.875rem;   /* prose, table cells */
  --fs-lg:      1.0625rem;   /* modal and card titles */
  --fs-xl:      1.375rem;   /* stat readouts */
  --fs-2xl:     2rem;   /* hero numbers */
  --lh-tight:   1.2;
  --lh-body:    1.6;
  --fw-regular: 400;
  --fw-medium:  500;
  --fw-semibold:600;
  --fw-bold:    700;
  --space-1:    0.25rem;
  --space-2:    0.4rem;   /* the house rhythm (lockup, edge margins) */
  --space-3:    0.5rem;
  --space-4:    0.75rem;
  --space-5:    1rem;
  --space-6:    1.5rem;
  --space-7:    2rem;
}

/* ── 3. Reset & accessibility utilities ──────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: var(--font-ui);
  background: var(--bg-deep);
  color: var(--text-primary);
  transition: background var(--transition), color var(--transition);
}

/* Reduced motion: blanket CSS clamp. Map camera animations are a JS concern —
   gate them with MCO.reducedMotion() (live-reactive), never a boot snapshot. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.001ms !important;
    animation-duration:  0.001ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* The UA's [hidden] { display: none } loses to any rule that sets display
   explicitly, so flex/grid elements stay visible as empty strips when hidden
   from JS. Restate it with priority. */
[hidden] { display: none !important; }

/* Visually-hidden text for screen-reader announcements + skip-link targets. */
.sr-only {
  position: absolute !important;
  width: 1px !important; height: 1px !important;
  padding: 0 !important; margin: -1px !important;
  overflow: hidden !important; clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important; border: 0 !important;
}

/* ONE universal keyboard focus ring. Never enumerate per-selector rules —
   a new control added later would ship without one. */
:focus-visible { outline: 2px solid var(--accent-line); outline-offset: 2px; }
/* WebKit focuses a date/time input's sub-fields, so the input itself never
   matches :focus-visible and showed no ring (0.11.2). Keyboard and pointer
   both open the field for typing, so :focus is the right trigger here. */
input:is([type="date"], [type="time"], [type="datetime-local"], [type="month"], [type="week"]):focus {
  outline: 2px solid var(--accent-line); outline-offset: 2px;
}

/* Skip link (WCAG 2.4.1): first focusable element on the page, jumps past the
   chrome. Markup in snippets/skip-link.html. */
.mco-skip-link {
  position: fixed; left: 0.5rem; top: -100px;
  z-index: calc(var(--z-toast) + 10);
  padding: 0.5rem 0.9rem;
  background: var(--accent); color: var(--text-on-accent);
  border-radius: var(--radius-md);
  font-weight: 600; font-size: 0.85rem; text-decoration: none;
}
.mco-skip-link:focus { top: 0.5rem; }

/* Display numerals (0.10.0, #36): a hero reading in Outfit with tabular
   figures, so digits keep their column as values tick (the hosted subset
   carries tnum) without Space Mono's terminal spacing at 2–4rem. Size is
   the app's (≥ 1.75rem; --fs-2xl is 2rem). Below that, numerals stay mono. */
.mco-num-display {
  font-family: var(--font-display-num);
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-semibold);
  letter-spacing: -0.01em;
  line-height: var(--lh-tight);
}

/* ── 4. MapLibre control polish ───────────────────────────────────────────
   Glass treatment for the vendor controls so they read as part of the app
   chrome. --ctrl-icon-filter inverts the vendor's dark icon glyphs on dark
   themes. */
.maplibregl-ctrl-group {
  background: var(--glass) !important;
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--ctrl-border) !important;
  border-radius: var(--radius-md) !important;
  box-shadow: var(--shadow) !important;
  transition: background var(--transition), border-color var(--transition);
}
.maplibregl-ctrl-group button {
  background: transparent !important;
  border-color: var(--ctrl-border) !important;
  transition: background var(--transition);
}
.maplibregl-ctrl-group button:hover { background: var(--accent-hover) !important; }
.maplibregl-ctrl-group button .maplibregl-ctrl-icon { filter: var(--ctrl-icon-filter, none); }
/* Icon for MCO.map.addFitControl()'s "zoom to full extent" button. The
   stroke is dark like the vendor icons so --ctrl-icon-filter flips it too. */
.maplibregl-ctrl-fit .maplibregl-ctrl-icon {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23202020' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='4 8 4 4 8 4'/><polyline points='20 8 20 4 16 4'/><polyline points='4 16 4 20 8 20'/><polyline points='20 16 20 20 16 20'/></svg>");
  background-size: 60% 60%;
  background-position: center;
  background-repeat: no-repeat;
}
.maplibregl-ctrl-attrib { background: var(--glass) !important; color: var(--text-muted) !important; }
.maplibregl-ctrl-attrib a { color: var(--text-secondary) !important; text-decoration: underline; text-underline-offset: 2px; }
.maplibregl-ctrl-attrib button.maplibregl-ctrl-attrib-button { filter: var(--ctrl-icon-filter, none); }
/* Attribution links are underlined (0.8.0): against the surrounding credit
   text they differ by color alone, 1.74:1 on dark and 1.25:1 on
   high-contrast, and WCAG 1.4.1 wants 3:1 or a non-color cue. axe flags it
   once hillshade's "Terrain: Mapzen/AWS Open Data" credit turns the bar into
   a text block. Same remedy as prose links in .info-section. */

/* Popups (0.8.0): MapLibre's popup is white with a black tip in every theme.
   Repainted from tokens, and put on the --z-detail tier: the vendor sheet
   gives popups NO z-index, so the map controls (z 2) drew over them. Close
   button 44px on touch. Content comes from MCO.map.popupContent()
   (.mco-popup / .mco-facts below), never setHTML of API strings (§7). */
.maplibregl-popup { z-index: var(--z-detail); }
.maplibregl-popup-content {
  background: var(--bg-surface); color: var(--text-primary);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  box-shadow: var(--shadow);
  padding: 0.55rem 0.75rem; font-family: var(--font-ui); font-size: 0.8rem;
}
.maplibregl-popup-anchor-top .maplibregl-popup-tip,
.maplibregl-popup-anchor-top-left .maplibregl-popup-tip,
.maplibregl-popup-anchor-top-right .maplibregl-popup-tip { border-bottom-color: var(--bg-surface); }
.maplibregl-popup-anchor-bottom .maplibregl-popup-tip,
.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip,
.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip { border-top-color: var(--bg-surface); }
.maplibregl-popup-anchor-left .maplibregl-popup-tip { border-right-color: var(--bg-surface); }
.maplibregl-popup-anchor-right .maplibregl-popup-tip { border-left-color: var(--bg-surface); }
.maplibregl-popup-close-button {
  width: 28px; height: 28px; top: 2px; right: 2px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--text-muted); font-size: 1.1rem; line-height: 1;
}
.maplibregl-popup-close-button:hover { background: var(--accent-hover); color: var(--text-primary); }
.mco-popup { min-width: 10rem; }
.maplibregl-popup-content:has(.maplibregl-popup-close-button) .mco-popup { padding-right: 1.4rem; }
.mco-popup-title { margin: 0; font-size: 0.85rem; font-weight: 600; line-height: 1.25; color: var(--text-primary); }
.mco-popup-sub { margin: 0.1rem 0 0; font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-muted); }
.mco-popup-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.55rem; }
.mco-popup-actions a.nav-btn { text-decoration: none; }
/* Label/value facts: a two-column <dl>, also used by detail sheets and
   cards so a popup and a sheet render a station the same way. Labels are
   --text-muted, which is allowed on deep and surface only (never --bg-raised). */
.mco-facts { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.75rem; margin: 0.45rem 0 0; font-size: 0.78rem; }
.mco-facts > div { display: contents; }
.mco-facts dt { color: var(--text-muted); }
.mco-facts dd { margin: 0; color: var(--text-primary); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

/* Vendor default is 2; restated so the whole stacking ladder is greppable.
   With viewport-fit=cover the corner containers need the safe-area insets
   themselves, or the attribution lands under the home indicator. NOTE:
   env() is inert unless the page sets viewport-fit=cover in its viewport
   meta — see snippets/head.html. */
.maplibregl-ctrl-top-left, .maplibregl-ctrl-top-right,
.maplibregl-ctrl-bottom-left, .maplibregl-ctrl-bottom-right {
  z-index: var(--z-map-ctrl);
}
.maplibregl-ctrl-bottom-left, .maplibregl-ctrl-bottom-right {
  padding-bottom: env(safe-area-inset-bottom);
}
.maplibregl-ctrl-top-left, .maplibregl-ctrl-bottom-left { padding-left: env(safe-area-inset-left); }
.maplibregl-ctrl-top-right, .maplibregl-ctrl-bottom-right { padding-right: env(safe-area-inset-right); }

/* ── 5. Component shells ──────────────────────────────────────────────────── */

/* Navbar. Glass bar with the brand-gradient underline. Structure:
   .mco-navbar > .logo-link + .nav-divider + .brand + .controls + .nav-meta */
.mco-navbar {
  flex-shrink: 0;
  position: relative;
  /* --nav-gap drives BOTH the flex gap and the lockup divider's negative
     margin, so the brand rhythm survives any change to the gap (§6 tightens it
     at ≤1060px). Overriding the gap directly would desynchronise them. */
  --nav-gap: var(--space-4);
  display: flex; align-items: center; gap: var(--nav-gap);
  /* Left padding matches the vertical padding so the 40px logo badge sits
     with square margins in the 52px bar (≈6px on all three sides). */
  padding: 0.4rem max(1rem, env(safe-area-inset-right)) 0.4rem max(0.4rem, env(safe-area-inset-left));
  min-height: 52px;
  background: var(--glass);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap; row-gap: 0.4rem;
  z-index: var(--z-chrome-top);
}
/* Sticky (0.10.0, #4), opt-in for long-scrolling pages; full-viewport map
   apps keep the in-flow bar. It stays on --z-chrome-top, clears the notch,
   and core publishes its height as --chrome-h, which html's
   scroll-padding-top reads. Needs the DOCUMENT to scroll: a body with
   overflow: hidden or a fixed height defeats position: sticky. */
.mco-navbar.is-sticky {
  position: sticky; top: 0;
  padding-top: max(0.4rem, env(safe-area-inset-top));
}
.mco-navbar::after {
  content: ''; position: absolute; bottom: 0; left: 0; right: 0;
  height: 2px; background: var(--brand-gradient); pointer-events: none;
}
.logo-link { display: flex; flex-shrink: 0; }
.logo-img  { width: 40px; height: 40px; border-radius: 6px; display: block; }
.nav-divider { width: 1px; height: 28px; background: var(--border); flex-shrink: 0; }
/* The brand lockup reads as one unit: pull the divider in so logo ↔ divider ↔
   title sit at 0.4rem — the same rhythm as the squared 0.4rem edge margins —
   while the full navbar gap resumes after the lockup. Derived from --nav-gap
   rather than hard-coded, so tightening the gap can't squeeze the lockup: at
   the 0.75rem default this is −0.35rem, and at the ≤1060px 0.5rem gap it
   becomes −0.1rem, holding the 0.4rem rhythm either way. */
.mco-navbar > .nav-divider { margin-inline: calc(0.4rem - var(--nav-gap)); }
.brand { display: flex; flex-direction: column; justify-content: center; flex-shrink: 0; line-height: 1.2; }
/* 12px title, 11px subtitle (0.10.0; were 10.4 and 8.8px, below a legible
   floor — #37). The lockup is wider for it: apps tuned to a wrap point
   re-check their ladder. */
.brand-title    { font-size: var(--fs-sm); font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); white-space: nowrap; margin: 0; }
.brand-subtitle { font-size: 0.6875rem; font-weight: 400; letter-spacing: 0.04em; font-style: italic; color: var(--text-dim); white-space: nowrap; }
.controls { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; row-gap: 0.3rem; }
.nav-meta { display: flex; align-items: center; gap: 0.4rem; flex-shrink: 0; margin-left: auto; }

/* Buttons. Pressed/hover borders use --accent-line, NOT --accent: the brand
   blue only clears ~2.3–2.9:1 against dark surfaces (WCAG 1.4.11 needs 3:1). */
.nav-btn {
  display: flex; align-items: center; gap: 5px;
  padding: 6px 12px; min-height: 34px;
  border: 1px solid var(--ctrl-border); border-radius: var(--radius-md);
  background: none; color: var(--text-secondary);
  cursor: pointer; font-size: 0.78rem; font-weight: 500;
  font-family: inherit; white-space: nowrap; flex-shrink: 0;
  transition: var(--transition);
}
.nav-btn:hover { background: var(--accent-hover); border-color: var(--accent-line); color: var(--text-primary); }
.nav-btn svg { flex-shrink: 0; stroke: currentColor; }
.nav-btn.icon-only { padding: 0.4rem; min-width: 34px; justify-content: center; }
.nav-btn[aria-pressed="true"] { background: var(--accent-hover); border-color: var(--accent-line); color: var(--text-primary); }
/* Navigation state (0.10.0, #3): a tab that is a LINK to a view carries
   aria-current="page", not aria-pressed (a toggle). Same look either way. */
.nav-btn[aria-current="page"] { background: var(--accent-hover); border-color: var(--accent-line); color: var(--text-primary); }
a.nav-btn { text-decoration: none; }
.nav-btn[disabled] { opacity: 0.4; cursor: not-allowed; }
/* Primary action (0.8.0): the one button per view that does the thing — Apply,
   Download, Retry. A filled --accent with --text-on-accent, the pairing the
   contrast contract guarantees in all three themes. Never color: #fff on
   --accent: it is 2.2:1 in high contrast, where the accent brightens and
   --text-on-accent flips to black. */
.nav-btn.is-primary { background: var(--accent); border-color: var(--accent-line); color: var(--text-on-accent); }
.nav-btn.is-primary:hover { background: var(--accent-fill-hover); border-color: var(--accent-line); color: var(--text-on-accent); }
.nav-btn.is-primary[disabled]:hover { background: var(--accent); }
.nav-btn[disabled]:hover { background: none; border-color: var(--ctrl-border); color: var(--text-secondary); }

.mco-btn-info {
  border-radius: 50%; border-color: var(--accent-line);
  color: var(--accent-line); font-size: 1rem; font-weight: 600;
  padding: 0; width: 34px; height: 34px; min-height: 0;
  display: flex; align-items: center; justify-content: center;
}
.mco-btn-info:hover { background: var(--accent); color: var(--text-on-accent); }

/* Segmented buttons: a .nav-btn group with fused borders. Deliberately a
   step shorter than the 34px nav buttons — a same-height fused group reads
   as heavy in the bar; flex centering keeps the baselines aligned. */
.seg-btns { display: flex; gap: 0; }
.seg-btn {
  padding: 4px 10px; min-height: 30px; height: 30px;
  border-radius: 0; border-right-width: 0;
  font-size: 0.74rem; font-weight: 600;
}
.seg-btn:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
.seg-btn:last-child  { border-radius: 0 var(--radius-md) var(--radius-md) 0; border-right-width: 1px; }
.seg-btn[aria-pressed="true"] { background: var(--accent); border-color: var(--accent-line); color: var(--text-on-accent); }

/* Radio segmented (0.9.0): a single-choice group as a native radio fieldset —
   free arrow keys, one Tab stop. Styled exactly like the aria-pressed
   segmented group:
     <fieldset class="seg-btns is-radio"><legend class="sr-only">Units</legend>
       <label class="nav-btn seg-btn"><input type="radio" name="u" value="us" checked>US</label> …
   The input is unstyled but covers its label, so the universal focus ring
   lands on it and a click anywhere on the label selects. Corners use
   :first-of-type / :last-of-type, so the <legend> doesn't throw them off. */
.seg-btns.is-radio { border: 0; padding: 0; margin: 0; min-width: 0; }
.seg-btns.is-radio > label.seg-btn { position: relative; display: flex; align-items: center; cursor: pointer; }
.seg-btns.is-radio > label.seg-btn > input {
  position: absolute; inset: 0; margin: 0;
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; border-radius: inherit; cursor: pointer;
}
.seg-btns.is-radio > .seg-btn:first-of-type { border-radius: var(--radius-md) 0 0 var(--radius-md); }
.seg-btns.is-radio > .seg-btn:last-of-type { border-radius: 0 var(--radius-md) var(--radius-md) 0; border-right-width: 1px; }
.seg-btns.is-radio > label.seg-btn:has(> input:checked) { background: var(--accent); border-color: var(--accent-line); color: var(--text-on-accent); }

/* Stepper buttons (0.9.0) — MCO.initStepper. Square .nav-btn, 34px (40 on touch). */
.nav-btn.mco-step { padding: 0.4rem; min-width: 34px; justify-content: center; }

/* Chips (0.9.0): pill toggles in a labelled group; apps own the selection.
     <div class="mco-chips" role="group" aria-labelledby="net-label">
       <button class="mco-chip" aria-pressed="true">HydroMet <span class="mco-chip-count">142</span></button>
   Pressed adds a ✓ (alt text empty), so the state isn't color alone.
   aria-busy="true" while its count loads. 28px, 40px on touch. */
.mco-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.mco-chip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  min-height: 28px; padding: 2px 10px;
  background: none; border: 1px solid var(--ctrl-border); border-radius: 999px;
  color: var(--text-secondary); font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  white-space: nowrap; cursor: pointer; transition: var(--transition);
}
.mco-chip:hover { background: var(--accent-hover); border-color: var(--accent-line); color: var(--text-primary); }
.mco-chip[aria-pressed="true"] { background: var(--accent-hover); border-color: var(--accent-line); color: var(--text-primary); }
.mco-chip[aria-pressed="true"]::before { content: "\2713"; content: "\2713" / ""; font-size: 0.9em; }
.mco-chip-count { font-family: var(--font-mono); font-size: 0.9em; font-variant-numeric: tabular-nums; }
.mco-chip[aria-busy="true"] { cursor: progress; }
.mco-chip[aria-busy="true"] .mco-chip-count { visibility: hidden; }
/* A chip with no count still shows it is loading (0.11.3). */
.mco-chip[aria-busy="true"]:not(:has(.mco-chip-count))::after { content: "\2026"; content: "\2026" / ""; }

/* Refresh/liveness status: read-only, deliberately subordinated. Add .static
   when the view isn't live (historical modes) to stop the pulse. */
.refresh-status {
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--font-mono);
  font-size: 0.65rem; color: var(--text-muted);
  white-space: nowrap;
  padding: 0 4px;
}
.refresh-status .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent-line);
  box-shadow: 0 0 0 2px var(--accent-hover);
  animation: mco-refresh-pulse 2.4s ease-in-out infinite;
}
@keyframes mco-refresh-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
  .refresh-status .dot { animation: none; }
}
.refresh-status.static .dot { animation: none; box-shadow: none; opacity: 0.55; }

/* Toast. One per page; created for you by MCO.showToast() if absent. */
.mco-toast {
  position: fixed; bottom: 1.5rem; left: calc(50% + var(--rail-inset, 0px) / 2);
  transform: translateX(-50%) translateY(0.75rem);
  background: var(--bg-surface);
  border: 1px solid var(--accent-line);
  border-radius: 6px; padding: 0.5rem 1rem;
  font-size: 0.82rem; color: var(--text-primary);
  opacity: 0; transition: opacity 0.2s, transform 0.2s;
  pointer-events: none; z-index: var(--z-toast);
  box-shadow: var(--shadow);
}
.mco-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Footer (0.9.0): data-source credit, then the MCO credit (MCO.credit()),
   then an optional mono version / "last updated" line. Full-viewport map
   apps carry no footer — it costs map height — and put MCO.credit() in the
   info modal's Data section and their exports instead. --text-muted on
   --bg-deep; links underlined (1.4.1). */
.mco-footer {
  padding: var(--space-4) max(var(--space-5), env(safe-area-inset-right)) max(var(--space-4), env(safe-area-inset-bottom)) max(var(--space-5), env(safe-area-inset-left));
  border-top: 1px solid var(--border);
  background: var(--bg-deep);
  font-size: var(--fs-sm); line-height: 1.5; color: var(--text-muted); text-align: center;
}
.mco-footer p + p { margin-top: var(--space-1); }
.mco-footer a { color: var(--text-secondary); text-decoration: underline; text-underline-offset: 2px; }
/* Wordmark (0.11.0, #27): the pair of fixed-color images, one per surface,
   so the theme picks. Both carry alt; the hidden one is display:none, out of
   the accessibility tree. Inline assets/mco-wordmark.svg instead to inherit
   currentColor. Keep it ≥ 40px tall.
     <img class="mco-wordmark is-on-dark"  src="…/assets/mco-wordmark-on-dark.svg"  alt="Montana Climate Office" height="48">
     <img class="mco-wordmark is-on-light" src="…/assets/mco-wordmark-on-light.svg" alt="Montana Climate Office" height="48"> */
.mco-wordmark { display: block; width: auto; }
.mco-footer .mco-wordmark { margin: 0 auto var(--space-3); }
:root:not([data-theme="light"]) .mco-wordmark.is-on-light,
[data-theme="light"] .mco-wordmark.is-on-dark { display: none; }
.mco-footer-meta { font-family: var(--font-mono); font-size: var(--fs-xs); }

/* Card (0.9.0): the IN-FLOW section shell — chart and table cards, sidebar
   blocks, sheet sections. .mco-panel is the over-map glass; this sits in the
   page. Surface background, so the text contract holds (muted/dim are
   allowed on surface, never raised). Collapsible with MCO.initCollapsible.
     .mco-card > .mco-card-head (.mco-card-title + .mco-card-actions) + .mco-card-body
   .is-flush: a body that runs to the edge (tables, charts). .is-glass: panel
   visuals without the absolute positioning. */
.mco-card { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg); min-width: 0; }
.mco-card-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border);
}
.mco-card-title {
  margin: 0; font-size: var(--fs-sm); font-weight: var(--fw-semibold);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-secondary);
}
.mco-card-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
.mco-card-body { padding: var(--space-4); }
.mco-card.is-flush > .mco-card-body { padding: 0; }
.mco-card.is-glass { background: var(--glass); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); box-shadow: var(--shadow); }
/* On glass the surface guarantee is gone: facts labels step up to secondary. */
.mco-card.is-glass .mco-facts dt, .mco-panel .mco-facts dt { color: var(--text-secondary); }

/* Loading (0.9.0) — MCO.loading. A 3px bar pinned to the top of its
   (positioned) container on the --z-map-progress tier: an indeterminate
   sweep, or determinate with aria-valuenow + --progress (0–100). Static
   under reduced motion. .mco-skeleton blocks are aria-hidden placeholders
   (.is-block, .is-chart, or --skeleton-h); flat under reduced motion. */
.mco-progress {
  position: absolute; top: 0; left: 0; right: 0; height: 3px;
  overflow: hidden; z-index: var(--z-map-progress); pointer-events: none;
}
.mco-progress::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 35%;
  background: var(--accent-line);
  animation: mco-progress-sweep 1.2s ease-in-out infinite;
}
.mco-progress[aria-valuenow]::before { animation: none; width: calc(var(--progress, 0) * 1%); transition: width var(--transition); }
@keyframes mco-progress-sweep { from { transform: translateX(-100%); } to { transform: translateX(290%); } }
.mco-skeleton {
  display: block; height: var(--skeleton-h, 0.9em); border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--bg-raised) 0%, var(--border) 50%, var(--bg-raised) 100%) 0 0 / 200% 100%;
  animation: mco-skeleton-shimmer 1.4s linear infinite;
}
.mco-skeleton.is-block { --skeleton-h: 4rem; }
.mco-skeleton.is-chart { --skeleton-h: 12rem; border-radius: var(--radius-md); }
@keyframes mco-skeleton-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) {
  .mco-progress::before { animation: none; width: 100%; opacity: 0.6; }
  .mco-skeleton { animation: none; background: var(--bg-raised); }
}

/* Notice (0.8.0): the persistent, actionable twin of the toast — a load
   failure with Retry, a data caveat, an outage. The toast is text-only and
   pointer-events: none, so it can't hold a button. Built by MCO.notice() or
   written in markup:
     <div class="mco-notice" data-tone="warning">
       <svg class="mco-notice-icon" aria-hidden="true">…</svg>
       <p class="mco-notice-text"><strong class="mco-notice-tone">Warning</strong> …</p>
       <div class="mco-notice-actions"><button class="nav-btn">Retry</button></div>
       <button class="modal-close" aria-label="Dismiss">×</button>
     </div>
   data-tone: info (default) | warning | danger | success. The tone WORD is
   always present (.mco-notice-tone), so color is never the only channel.
   In flow by default; data-place="over" floats it at the top of the nearest
   positioned ancestor (a map container) on the --z-map-notice tier.
   .mco-empty is the same shell with no tone: the empty-state callout. */
.mco-notice, .mco-empty {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.55rem 0.6rem 0.55rem 0.85rem;
  background: var(--bg-surface); color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: 0.82rem; line-height: 1.5;
}
.mco-notice { border-left: 3px solid var(--notice-edge, var(--accent-line)); }
.mco-notice-icon { flex-shrink: 0; width: 16px; height: 16px; color: var(--notice-edge, var(--accent-line)); }
.mco-notice-text { flex: 1; min-width: 0; margin: 0; }
.mco-notice-tone { font-weight: 600; color: var(--notice-tone, var(--text-primary)); }
.mco-notice-text a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.mco-notice-actions { display: flex; gap: 0.4rem; flex-shrink: 0; }
.mco-notice[data-tone="danger"]  { --notice-edge: var(--danger);  --notice-tone: var(--danger);  background: var(--danger-fill);  color: var(--text-on-danger); }
.mco-notice[data-tone="warning"] { --notice-edge: var(--warning); --notice-tone: var(--warning); background: var(--warning-fill); color: var(--text-on-warning); }
.mco-notice[data-tone="success"] { --notice-edge: var(--success); --notice-tone: var(--success); background: var(--success-fill); color: var(--text-on-success); }
.mco-notice[data-place="over"], .mco-empty[data-place="over"] {
  position: absolute; top: 0.75rem; left: 50%; transform: translateX(-50%);
  width: max-content; max-width: min(34rem, calc(100% - 1.5rem));
  box-shadow: var(--shadow);
  z-index: var(--z-map-notice);
}
.mco-empty { justify-content: center; text-align: center; padding: 0.9rem 1.1rem; }

/* Pointer-following tooltip. Decorative for AT: keep it aria-hidden and route
   the same content through a live region (see HOUSE-STYLE.md §5). */
.mco-tooltip {
  position: fixed;
  background: var(--glass);
  border: 1px solid var(--accent-line);
  border-radius: 5px;
  padding: 0.3rem 0.6rem;
  font-size: 0.75rem; font-weight: 500;
  color: var(--text-primary);
  pointer-events: none;
  z-index: var(--z-cursor);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.1s;
  backdrop-filter: blur(8px);
}
.mco-tooltip.visible { opacity: 1; }
.mco-tooltip .tooltip-name { font-size: 0.78rem; font-weight: 600; color: var(--text-primary); line-height: 1.2; }
.mco-tooltip .tooltip-sub {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.65rem;
  color: var(--text-muted);
  margin-top: 1px;
}
.mco-tooltip .tooltip-val {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--accent-line);
  font-weight: 600;
  margin-top: 3px;
}

/* Modal: native <dialog class="mco-modal"> wired by MCO.initInfoModal()
   (backdrop click closes, focus restores to the opener). */
.mco-modal { background: transparent; border: none; padding: 1rem; position: fixed; inset: 0; margin: auto; width: fit-content; height: fit-content; max-width: calc(100vw - 2rem); max-height: calc(100dvh - 2rem); }
.mco-modal::backdrop { background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); }
/* Long modals (0.8.0, back-ported from mesonet-explorer, the family's
   tallest): the header sticks so the title and close button stay reachable
   while the body scrolls, scrolling never chains to the page behind, and a
   soft shade at the bottom edge says "there's more". The shade is two
   layers: a --bg-surface cover attached to the END of the content (local)
   over a shade pinned to the box's bottom edge (scroll). Mid-scroll the
   cover is out of view and the shade shows; at the end it slides over the
   shade and hides it. A modal that doesn't overflow never shows it. */
.info-modal-box {
  background: var(--bg-surface); border: 1px solid var(--border);
  border-radius: 10px; width: min(540px, calc(100vw - 4rem));
  max-height: calc(100dvh - 4rem); overflow-y: auto;
  overscroll-behavior: contain;
  padding: 1.5rem; box-shadow: var(--shadow);
  background-image:
    linear-gradient(var(--bg-surface), var(--bg-surface)),
    linear-gradient(to top, rgba(0,0,0,0.42), rgba(0,0,0,0));
  background-repeat: no-repeat;
  background-size: 100% 32px, 100% 22px;
  background-position: 0 100%, 0 100%;
  background-attachment: local, scroll;
}
.info-modal-header {
  display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem;
  position: sticky; top: -1.5rem; z-index: 1;
  margin: -1.5rem -1.5rem 1.25rem; padding: 1.5rem 1.5rem 0.75rem;
  background: var(--bg-surface);
  border-radius: 10px 10px 0 0;
}
.info-modal-title    { font-size: 1.05rem; font-weight: 700; color: var(--accent-line); margin: 0; }
.info-modal-subtitle { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.2rem; }
.modal-close { display: flex; align-items: center; justify-content: center; background: none; border: 1px solid var(--border); border-radius: 6px; color: var(--text-muted); font-size: 1.2rem; cursor: pointer; line-height: 1; flex-shrink: 0; min-width: 34px; min-height: 34px; transition: var(--transition); }
.modal-close:hover { color: var(--text-primary); border-color: var(--accent-line); }
.info-section { margin-bottom: 1.25rem; }
.info-section:last-child { margin-bottom: 0; }
.info-section h3 { font-size: 0.8rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-secondary); margin-bottom: 0.5rem; padding-bottom: 0.3rem; border-bottom: 1px solid var(--border); }
.info-section p, .info-section ul { font-size: 0.85rem; color: var(--text-secondary); line-height: 1.6; }
.info-section ul { padding-left: 1.1rem; }
.info-section li { margin-bottom: 0.2rem; }
/* Links inside prose are underlined, not color-only (WCAG 1.4.1 —
   link-in-text-block). Color alone fails for CVD users; hover-only
   underlines don't count. */
.info-section a { color: var(--accent-line); text-decoration: underline; text-underline-offset: 2px; }
.info-section a:hover { text-decoration-thickness: 2px; }
.info-section kbd {
  font-family: var(--font-mono);
  font-size: 0.75em;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 1px 5px;
  color: var(--text-primary);
}
.info-section code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  background: var(--bg-raised);
  border-radius: 3px;
  padding: 1px 4px;
}

/* Floating glass panel (legend, filter dock, …). Collapse behavior comes from
   MCO.initCollapsible(); the toggle carries aria-expanded, the body gets
   [hidden]. Structure:
     .mco-panel > .mco-panel-head (> .mco-panel-title + .mco-panel-toggle)
                + .mco-panel-body */
.mco-panel {
  position: absolute;
  background: var(--glass);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  z-index: var(--z-map-panel);
  max-width: min(320px, calc(100vw - 2rem));
}
.mco-panel-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; padding: 0.5rem 0.5rem 0.5rem 0.75rem;
}
.mco-panel-title {
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-secondary);
}
.mco-panel-toggle {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex-shrink: 0;
  background: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text-dim); cursor: pointer; font-family: inherit;
  transition: var(--transition);
}
.mco-panel-toggle:hover { color: var(--text-primary); border-color: var(--accent-line); }
/* The caret shows the ACTION, not the state (markup glyph is an up-chevron):
   expanded → points down ("collapse toward the bottom dock"); collapsed →
   points up ("expand upward"). */
.mco-panel-toggle svg { transition: transform var(--transition); transform: rotate(180deg); }
.mco-panel-toggle[aria-expanded="false"] svg { transform: none; }
/* Collapse is animated (slide + fade — ported from mesonet-status), then
   MCO.initCollapsible sets [hidden] so collapsed content leaves the tab
   order. The max-height cap comfortably fits house panels; raise it locally
   for an unusually tall one. Reduced motion clamps the transition. */
.mco-panel-body {
  padding: 0 0.75rem 0.75rem;
  max-height: 500px;
  opacity: 1;
  overflow: hidden;
  transition: max-height var(--transition), opacity var(--transition);
}
.mco-panel-body.is-collapsing { max-height: 0; opacity: 0; }

/* Legend rows (0.8.0) — click to show/hide a category, wired by
   MCO.initLegendToggles. aria-pressed is the styling source of truth (§5.7):
     <button class="mco-legend-row" aria-pressed="true" data-key="fresh">
       <span class="mco-legend-swatch" style="--swatch: #2a8a86" aria-hidden="true"></span>
       <span class="mco-legend-label">Fresh</span><span class="mco-legend-count">42</span>
     </button>
   The ROW never gets opacity: parent opacity composites the whole subtree,
   so no child rule can restore the label's contrast (three apps shipped
   `.legend-row.off { opacity: .4 }`, a 2.7:1 label). Off dims the swatch
   only and strikes the label through, a non-color cue. The label stays
   --text-secondary: the panel is glass over an arbitrary basemap, where
   --text-muted's surface-only guarantee doesn't hold.
   Swatches always carry a --border edge, so a white "near normal" bin can't
   vanish on light glass. data-shape matches the map mark: circle | hollow |
   ring | line | dash (default: rounded square, a fill). */
.mco-legend-row {
  display: flex; align-items: center; gap: 0.5rem;
  width: 100%; min-height: 28px; padding: 2px 4px;
  background: none; border: 0; border-radius: var(--radius-sm);
  color: var(--text-secondary); font: inherit; font-size: 0.78rem; text-align: left;
  cursor: pointer; transition: background var(--transition);
}
.mco-legend-row:hover { background: var(--accent-hover); color: var(--text-primary); }
.mco-legend-swatch {
  width: 14px; height: 14px; flex-shrink: 0;
  background: var(--swatch, transparent);
  border: 1px solid var(--border); border-radius: 3px;
  transition: opacity var(--transition);
}
.mco-legend-swatch[data-shape="circle"] { border-radius: 50%; }
.mco-legend-swatch[data-shape="hollow"] { border-radius: 50%; background: transparent; border: 2px solid var(--swatch); }
.mco-legend-swatch[data-shape="ring"]   { border-radius: 50%; background: transparent; border: 3.5px solid var(--swatch); }
.mco-legend-swatch[data-shape="line"]   { height: 3px; width: 16px; border: 0; border-radius: 2px; }
.mco-legend-swatch[data-shape="dash"]   { height: 3px; width: 16px; border: 0; border-radius: 0;
  background: repeating-linear-gradient(90deg, var(--swatch) 0 4px, transparent 4px 7px); }
.mco-legend-label { flex: 1; min-width: 0; }
.mco-legend-count { margin-left: auto; padding-left: 0.5rem; font-family: var(--font-mono); font-size: 0.7rem; font-variant-numeric: tabular-nums; }
.mco-legend-row[aria-pressed="false"] .mco-legend-swatch { opacity: 0.35; }
.mco-legend-row[aria-pressed="false"] .mco-legend-label { text-decoration: line-through; text-decoration-thickness: 1px; }
@media (hover: none) { .mco-legend-row { min-height: 40px; } }

/* Search combobox (0.8.0) — MCO.initSearchBox. Field, icon and listbox are
   the kit's now; five apps carried ~90 lines each of this, plus the same
   search-icon data URI stroked in a banned drift value (%237a8190). The icon
   is a CSS mask, so it takes currentColor (--text-dim) in every theme.
     .mco-search > .mco-search-icon + input.mco-search-input + .mco-search-list
   The active option is marked by an inset --accent-line bar as well as the
   tint, the current one by ✓ (alt text empty, so it isn't read), so neither
   is color alone. The list sits on the --z-flyout tier. */
.mco-search { position: relative; display: inline-flex; align-items: center; }
.mco-search-icon {
  position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
  width: 14px; height: 14px; pointer-events: none;
  color: var(--text-dim); background: currentColor;
  -webkit-mask: var(--mco-search-glyph) center / contain no-repeat;
          mask: var(--mco-search-glyph) center / contain no-repeat;
  --mco-search-glyph: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'><circle cx='11' cy='11' r='7'/><path d='m20 20-3.6-3.6'/></svg>");
}
.mco-search-input {
  background-color: var(--input-bg); color: var(--text-primary);
  border: 1px solid var(--ctrl-border); border-radius: var(--radius-md);
  height: 32px; width: 220px; max-width: 100%;
  padding: 0 10px 0 29px;
  font: inherit; font-size: 0.82rem;
  transition: border-color var(--transition);
}
.mco-search-input:hover { border-color: var(--accent-line); }
.mco-search-input::placeholder { color: var(--text-dim); opacity: 1; }
.mco-search-list {
  position: absolute; top: calc(100% + 4px); left: 0;
  min-width: 100%; width: max-content; max-width: min(360px, calc(100vw - 1.5rem));
  max-height: min(340px, 60vh); overflow-y: auto; overscroll-behavior: contain;
  padding: 4px;
  background: var(--bg-surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); box-shadow: var(--shadow);
  z-index: var(--z-flyout);
}
.mco-search-group {
  padding: 6px 8px 2px;
  font-size: 0.65rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-secondary);
}
.mco-search-option {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  min-height: 32px; padding: 6px 8px;
  border-radius: var(--radius-sm);
  font-size: 0.8rem; color: var(--text-primary); cursor: pointer;
}
.mco-search-option:hover { background: var(--accent-hover); }
.mco-search-option[aria-selected="true"] { background: var(--accent-hover); box-shadow: inset 3px 0 0 var(--accent-line); }
.mco-search-option.is-current { font-weight: 600; }
.mco-search-option.is-current > :first-child::before { content: "\2713\00a0"; content: "\2713\00a0" / ""; }
/* The native clear (×) glyph is dark on every theme; flip it like the map
   control icons (0.11.1). */
.mco-search-input::-webkit-search-cancel-button { filter: var(--ctrl-icon-filter, none); }
.mco-search-meta { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-muted); white-space: nowrap; }
.mco-search-note { padding: 6px 8px; font-size: 0.75rem; color: var(--text-secondary); cursor: default; }
@media (hover: none) {
  .mco-search-input { min-height: 40px; }
  .mco-search-option { min-height: 40px; align-items: center; }
}

/* Collapsible search (MCO.initSearchCollapse) ───────────────────────────────
   At compact widths a navbar search field costs more width than it earns, but
   hiding it strands the only keyboard route to a named feature. Instead it
   collapses into a disclosure button grouped with the other nav buttons and
   expands as a full-width overlay bar under the navbar.

   The breakpoint is the ladder's existing 640px compact edge rather than a
   fifth number: compact is exactly where an inline field stops paying for
   itself. Deliberately width-only — unlike MCO.viewport.COMPACT_MQ it has no
   max-height clause, because a short landscape window is still wide enough for
   an inline field. Raised from 460px in v0.6.0: at 460 a control-dense bar
   (mesonet-status) still wrapped to a third row between 461 and 527px, and the
   wrap point drifted with live data in the labels.

   This is the COLLAPSE mechanics only; the field is .mco-search +
   MCO.initSearchBox above (0.8.0), or still an app's own. Add
   .mco-search-collapse to the search wrapper and .mco-search-toggle to the
   button:
     <div class="mco-search mco-search-collapse" id="search-wrap"> … </div>
     <button class="nav-btn icon-only mco-search-toggle"
             aria-expanded="false" aria-controls="search-wrap"> … </button>
   Inner selectors are generic on purpose (input[type=search], kbd), so this
   works whatever the app calls its own parts. */
.mco-search-toggle { display: none; }
@media (max-width: 640px) {
  .mco-search-toggle { display: flex; }
  .mco-navbar .mco-search-collapse { display: none; }
  .mco-navbar .mco-search-collapse.is-open {
    display: flex;
    /* .mco-navbar is the positioned ancestor, so top:100% lands the bar
       directly beneath the whole (possibly wrapped) navbar. */
    position: absolute;
    top: 100%; left: 0; right: 0;
    z-index: var(--z-flyout);
    padding: 0.5rem max(0.75rem, env(safe-area-inset-right))
             0.5rem max(0.75rem, env(safe-area-inset-left));
    background: var(--glass);
    backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow);
  }
  /* Fill the bar — flex-basis beats the field's own fixed and :focus widths. */
  .mco-navbar .mco-search-collapse.is-open input[type="search"],
  .mco-navbar .mco-search-collapse.is-open input[type="search"]:focus {
    flex: 1 1 0; width: auto;
  }
  /* A "press /" hint is a physical-keyboard affordance; in the overlay it would
     sit on top of the text being typed. */
  .mco-navbar .mco-search-collapse.is-open kbd { display: none; }
  /* The overlay's padding moves the field in; the kit icon follows it. */
  .mco-navbar .mco-search-collapse.is-open .mco-search-icon { left: calc(max(0.75rem, env(safe-area-inset-left)) + 9px); }
}

/* Scrim behind off-canvas drawers on compact viewports. Pair with a panel at
   --z-drawer; the scrim sits one tier below it. Always aria-hidden="true"
   (it is a click target, not content); clicking it closes its owner.
   data-scope="container" (0.8.0) fills its nearest positioned ancestor
   instead of the viewport, so a drawer inside the map frame can dim the map
   without dimming the navbar above it (explorer's #sidebar-scrim, and the
   dashboard's compact drawer). */
.mco-scrim {
  position: fixed; inset: 0;
  background: var(--scrim);
  z-index: var(--z-map-panel);
}
.mco-scrim[data-scope="container"] { position: absolute; }

/* Off-canvas drawer (0.9.0) — MCO.initDrawer. A labelled <aside> on the
   --z-drawer tier, sliding in from data-side (start | end). [hidden] after the
   slide-out, so nothing off-screen stays tabbable. data-scope="container"
   keeps it inside a positioned frame (with a scoped scrim) so the navbar
   stays live; data-docked (set by modal: 'compact' above compact) turns it
   into a plain column in flow. */
.mco-drawer {
  position: fixed; top: 0; bottom: 0; left: var(--rail-inset, 0px);
  width: min(var(--drawer-w, 320px), 88vw);
  display: flex; flex-direction: column;
  background: var(--bg-surface);
  border-right: 1px solid var(--border);
  box-shadow: var(--shadow);
  z-index: var(--z-drawer);
  padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom);
  transform: translateX(-100%);
  transition: transform var(--transition);
}
.mco-drawer[data-side="end"] { left: auto; right: 0; border-right: 0; border-left: 1px solid var(--border); transform: translateX(100%); }
.mco-drawer[data-scope="container"] { position: absolute; }
/* Inside its frame, which the rail's body padding already clears. */
.mco-drawer[data-scope="container"]:not([data-side="end"]) { left: 0; }
.mco-drawer.is-open { transform: none; }
.mco-drawer[data-docked] {
  position: static; transform: none; box-shadow: none;
  width: var(--drawer-w, 320px); flex-shrink: 0; z-index: auto;
  padding-top: 0; transition: none;
}
.mco-drawer[data-docked] [data-close-drawer] { display: none; }
.mco-drawer-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-3) var(--space-3) var(--space-3) max(var(--space-4), env(safe-area-inset-left));
  border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.mco-drawer-body {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: var(--space-4) var(--space-4) var(--space-4) max(var(--space-4), env(safe-area-inset-left));
}
.mco-drawer[data-side="end"] .mco-drawer-head,
.mco-drawer[data-side="end"] .mco-drawer-body { padding-right: max(var(--space-4), env(safe-area-inset-right)); }

/* Bottom sheet (0.9.0) — MCO.initSheet, after mesonet-explorer's panel. Docks
   at the end edge (full height under the chrome) above compact, and at the
   bottom with peek/full detents on compact. --z-detail: under the drawer and
   the chrome, over the map controls. Heights are 100dvh math. */
.mco-sheet {
  position: fixed; z-index: var(--z-detail);
  top: var(--chrome-h, 0px); bottom: 0; right: 0;
  width: min(var(--sheet-w, 360px), 40vw);
  display: flex; flex-direction: column;
  background: var(--bg-surface);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow);
  padding-bottom: env(safe-area-inset-bottom);
  transform: none;
  transition: transform 0.22s cubic-bezier(0.4,0,0.2,1), max-height 0.22s cubic-bezier(0.4,0,0.2,1);
}
.mco-sheet.is-entering, .mco-sheet.is-leaving { transform: translateX(100%); }
.mco-sheet-head {
  position: relative; flex-shrink: 0;
  display: flex; align-items: center; gap: var(--space-3);
  min-height: 48px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.mco-sheet-title { flex: 1; min-width: 0; margin: 0; font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--text-primary); }
.mco-sheet-title:focus { outline: none; }          /* programmatic focus target; never reached by Tab */
.mco-sheet-grip { display: none; }
.mco-sheet-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: var(--space-4) max(var(--space-4), env(safe-area-inset-right)) var(--space-4) var(--space-4);
}
.is-compact .mco-sheet {
  top: auto; left: var(--rail-inset, 0px); right: 0; bottom: 0; width: auto;
  max-height: calc((100dvh - var(--chrome-h, 0px)) * 0.85);
  border-left: 0; border-top: 1px solid var(--border);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.is-compact .mco-sheet.is-entering, .is-compact .mco-sheet.is-leaving { transform: translateY(100%); }
.is-compact .mco-sheet[data-state="peek"] { max-height: var(--sheet-peek-h, 180px); }
.is-compact .mco-sheet[data-state="peek"] .mco-sheet-body { overflow: hidden; }
.is-compact .mco-sheet-head { touch-action: none; cursor: grab; padding-top: 18px; }
/* The grip: a visible 40×4 bar inside a 44×24+ button — the keyboard and
   tap twin of the drag. */
.is-compact .mco-sheet-grip {
  display: block; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 64px; height: 24px; min-height: 0; padding: 0;
  background: none; border: 0; border-radius: var(--radius-sm); cursor: pointer;
}
.is-compact .mco-sheet-grip::before {
  content: ''; position: absolute; left: 12px; right: 12px; top: 10px; height: 4px;
  border-radius: 999px; background: var(--ctrl-border);
}
@media (hover: none) { .is-compact .mco-sheet-grip { width: 88px; height: 44px; top: -10px; } .is-compact .mco-sheet-grip::before { top: 20px; left: 24px; right: 24px; } }

/* First-paint hold (0.9.0): see MCO.ready(). visibility, never display, so
   the reserved layout doesn't shift when the hold lifts. */
html.mco-booting [data-hold] { visibility: hidden; }
html:not(.mco-booting) [data-skeleton] { display: none; }

/* Overlay metrics + auto-lift (0.9.0). --chrome-h / --sheet-h / --tabbar-h
   are written on <html> by MCO.metrics (the sheet publishes its own). Lifting
   the toast and MapLibre's bottom corners is opt-in via html.mco-autolift
   until 1.0.0 — the toast's position is an observable default. */
:root { --chrome-h: 0px; --sheet-h: 0px; --tabbar-h: 0px; --overlay-bottom: calc(var(--sheet-h) + var(--tabbar-h)); }
html { scroll-padding-top: var(--chrome-h); }
html.mco-autolift .mco-toast { bottom: calc(1.5rem + var(--overlay-bottom) + env(safe-area-inset-bottom)); }
html.mco-autolift .maplibregl-ctrl-bottom-left,
html.mco-autolift .maplibregl-ctrl-bottom-right { transform: translateY(calc(-1 * var(--overlay-bottom))); transition: transform var(--transition); }

/* ── 6. Responsive & touch ────────────────────────────────────────────────
   Breakpoint ladder (HOUSE-STYLE.md §3): 1400 shed button labels · 1060
   tighten chrome · 750 collapse the brand to the badge · 640 compact. "Compact" also has
   a height variant — KEEP IN SYNC with MCO.viewport.COMPACT_MQ in
   core/mco-core.js and the copy in snippets/anti-flash.html:
   (max-width: 640px), (max-height: 560px). Layout lives in
   real @media rules (no flash before deferred JS runs); behavior decisions
   (sheet vs popup, auto-collapse) key off MCO.viewport. */
@media (max-width: 1400px) {
  /* .btn-label lives INSIDE its button, so removing it is safe: the house rule
     is that such a button carries a permanent aria-label (§3). */
  .mco-navbar .btn-label { display: none; }
  /* .control-label is usually a <label for> naming a real control, so hide it
     VISUALLY rather than removing it — display:none takes it out of the
     accessibility tree and the input it names is left with NO accessible name
     from 1400px down (axe: label / select-name). Same reasoning as the brand
     lockup collapse. */
  .mco-navbar .control-label {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0);
    white-space: nowrap; border: 0;
  }
  /* Once the label is gone, the icon is left sitting in label-sized padding
     (6px 12px) — visibly wider than the .icon-only buttons next to it in the
     same bar. Square it up so a mixed row of labelled and icon-only buttons
     reads as one set. .mco-btn-info is untouched: its circle is a deliberate
     distinction, and it never carries a .btn-label. */
  .mco-navbar .nav-btn:has(> .btn-label) {
    padding: 0.4rem; min-width: 34px; justify-content: center;
  }
}
@media (max-width: 1060px) {
  /* Tighten via the custom property, never `gap` directly — the lockup
     divider's margin is derived from it. */
  .mco-navbar { padding-right: 0.6rem; --nav-gap: 0.5rem; }
  .nav-meta { gap: 0.3rem; }
}
/* Mobile brand collapse: the logo badge alone carries the identity, because at
   phone widths the controls need every pixel. Both the title and the subtitle
   go, along with the lockup divider that would otherwise dangle beside nothing.

   The lockup is VISUALLY hidden rather than display:none'd on purpose:
   .brand-title is often the page's <h1> (HOUSE-STYLE §3), and a document
   outline shouldn't disappear at a breakpoint. Screen readers still get the
   app name; sighted users get the badge.

   Consumers that keep branding at every width must restore BOTH the lockup's
   static position and the divider — see mesonet-status.

   Scoped to the navbar (0.7.0): a lockup an app moves elsewhere — the top of
   a compact drawer, say (mesonet-explorer) — has room and must stay visible.
   :where() keeps the specificity at the old bare `.brand`, so an existing
   consumer override still wins. */
@media (max-width: 750px) {
  :where(.mco-navbar) .brand {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0);
    white-space: nowrap; border: 0;
  }
  .mco-navbar > .nav-divider { display: none; }
}
@media (max-width: 640px) {
  .refresh-status { display: none; }   /* reclaim space on phones */
}

/* WCAG 2.5.5 — comfortable touch targets on touch-only devices. */
@media (hover: none) {
  .nav-btn, .seg-btn { min-height: 40px; }
  .nav-btn.mco-step { min-width: 40px; }
  .mco-chip { min-height: 40px; padding: 4px 14px; }
  /* The :has() arm covers label-shed buttons squared by §6; above 1400px the
     label makes them wider than 40px anyway, so it's a no-op there. */
  .nav-btn.icon-only, .mco-navbar .nav-btn:has(> .btn-label) { min-width: 40px; }
  .mco-btn-info { width: 40px; height: 40px; }
  .modal-close { min-width: 44px; min-height: 44px; }
  .maplibregl-popup-close-button { width: 44px; height: 44px; top: 0; right: 0; }
  .maplibregl-popup-content:has(.maplibregl-popup-close-button) .mco-popup { padding-right: 2.4rem; }
  /* 40px, not 36 (0.9.0): the verify harness's touch-target audit caught it. */
  .mco-panel-toggle { width: 40px; height: 40px; }
  /* MapLibre's own controls are 29px (zoom/fit) and 24px (the compact
     attribution ⓘ) — under the 40px floor on touch (0.9.0). */
  .maplibregl-ctrl-group button { width: 40px; height: 40px; }
  .maplibregl-ctrl-attrib.maplibregl-compact { min-height: 40px; padding-right: 40px; }
  .maplibregl-ctrl-attrib-button { width: 40px; height: 40px; border-radius: 20px; background-position: center; background-repeat: no-repeat; }
}

/* Short-landscape rail (0.10.0, #38) — opt-in: .mco-navbar[data-rail] +
   MCO.initNavRail. On a landscape phone the bar wraps to two rows and eats
   ~30% of the height, while width sits unused. In rail mode the bar becomes
   a 56px column at the left edge (fixed, so it works whether the body is a
   flex column or scrolls) and the page is padded clear of it; the app's
   quick controls live in .mco-rail, everything else in .mco-nav-drawer,
   which opens beside the rail.

     <header class="mco-navbar" data-rail>
       <a class="logo-link" …>…</a>
       <div class="mco-rail">        <!-- display:none outside rail mode -->
         <button class="nav-btn icon-only" aria-label="Menu" aria-expanded="false"
                 aria-controls="nav-drawer">☰</button>   <!-- + 1–3 hot controls -->
       </div>
       <div class="nav-divider" aria-hidden="true"></div>
       <div class="mco-nav-drawer" id="nav-drawer">
         <!-- display:contents outside rail mode: the bar lays out as before -->
         <div class="brand">…</div> <div class="controls">…</div> <div class="nav-meta">…</div>
       </div>
     </header>
     <div class="mco-scrim" aria-hidden="true" hidden></div>

   KEEP IN SYNC with MCO.viewport.RAIL_MQ: the landscape half of the compact
   query, not a new number. Pitfalls this encodes: the closed drawer is
   display:none, not visibility:hidden (a transitioned child flips a frame
   late and focus() fails); the navbar's backdrop-filter makes it the
   containing block, so the drawer sits at left:100% of the rail; the plain
   .mco-scrim (--z-map-panel) dims the page below the rail (--z-chrome-top). */
.mco-rail { display: none; }
.mco-nav-drawer { display: contents; }
@media (max-height: 560px) and (orientation: landscape) {
  body:has(> .mco-navbar[data-rail]) {
    --rail-w: 56px;
    /* What the rail covers (0.11.1). Fixed surfaces anchored at the left
       edge (the compact sheet, a start-side drawer, the toast's centre)
       read it, so nothing lands under the rail; it is unset, so 0, outside
       rail mode. */
    --rail-inset: calc(var(--rail-w) + env(safe-area-inset-left));
    padding-left: var(--rail-inset);
  }
  /* The rail isn't above anything: nothing needs scroll padding for it. */
  html:has(> body > .mco-navbar[data-rail]) { --chrome-h: 0px !important; }
  .mco-navbar[data-rail] {
    position: fixed; top: 0; bottom: 0; left: 0;
    width: calc(var(--rail-w, 56px) + env(safe-area-inset-left));
    min-height: 0;
    flex-direction: column; flex-wrap: nowrap; justify-content: flex-start;
    padding: max(0.4rem, env(safe-area-inset-top)) 0.4rem
             max(0.4rem, env(safe-area-inset-bottom)) max(0.4rem, env(safe-area-inset-left));
    border-bottom: 0; border-right: 1px solid var(--border);
  }
  .mco-navbar[data-rail]::after { top: 0; bottom: 0; left: auto; right: 0; width: 2px; height: auto; }
  .mco-navbar[data-rail] > .nav-divider { display: none; }
  .mco-navbar[data-rail] .mco-rail {
    display: flex; flex-direction: column; align-items: center; gap: 0.35rem;
    min-height: 0; overflow-y: auto; scrollbar-width: none;
  }
  .mco-navbar[data-rail] .mco-rail .nav-btn { width: 40px; min-height: 40px; padding: 0; justify-content: center; }

  .mco-navbar[data-rail] .mco-nav-drawer {
    display: none; flex-direction: column; align-items: stretch; gap: 0.9rem;
    position: absolute; top: 0; bottom: 0; left: 100%;
    width: min(var(--drawer-w, 300px), calc(100vw - var(--rail-w, 56px) - env(safe-area-inset-left)));
    padding: max(0.75rem, env(safe-area-inset-top)) 0.9rem max(0.75rem, env(safe-area-inset-bottom));
    overflow-y: auto; overscroll-behavior: contain;
    background: var(--bg-surface); border-right: 1px solid var(--border); box-shadow: var(--shadow);
    transition: transform 0.18s ease-out, opacity 0.18s ease-out;
  }
  .mco-navbar[data-rail] .mco-nav-drawer.is-open { display: flex; }
  /* Undo the bar-only collapses: the drawer has the room. */
  .mco-navbar[data-rail] .mco-nav-drawer .brand,
  .mco-navbar[data-rail] .mco-nav-drawer .control-label {
    position: static; width: auto; height: auto; margin: 0; overflow: visible; clip: auto;
  }
  .mco-navbar[data-rail] .mco-nav-drawer .brand-title,
  .mco-navbar[data-rail] .mco-nav-drawer .brand-subtitle { white-space: normal; }
  .mco-navbar[data-rail] .mco-nav-drawer .btn-label { display: inline; }
  .mco-navbar[data-rail] .mco-nav-drawer .nav-btn:has(> .btn-label) { padding: 6px 12px; justify-content: flex-start; }
  .mco-navbar[data-rail] .mco-nav-drawer .controls { flex-direction: column; align-items: stretch; }
  .mco-navbar[data-rail] .mco-nav-drawer .nav-meta { margin-left: 0; flex-wrap: wrap; }
  .mco-navbar[data-rail] .mco-nav-drawer .mco-search-collapse { display: flex; position: relative; }
  .mco-navbar[data-rail] .mco-nav-drawer .mco-search > input { flex: 1; min-width: 0; }
  .mco-navbar[data-rail] .mco-nav-drawer .mco-search-toggle { display: none; }
}
@starting-style {
  .mco-navbar[data-rail] .mco-nav-drawer.is-open { transform: translateX(-12px); opacity: 0; }
}

/* Glass is legible on its own (0.10.0, #37): --glass is near-opaque and the
   backdrop blur is an enhancement on top, because an engine that drops it
   (headless WebKit, low-power modes) or a visitor who asks for Reduce
   Transparency gets the bare fill. Under that preference every glass surface
   goes solid: --glass becomes --bg-surface, and the blur is dropped.
   html:root outranks the [data-theme] blocks without matching their selector. */
@media (prefers-reduced-transparency: reduce) {
  html:root { --glass: var(--bg-surface); }
  .mco-navbar, .mco-panel, .mco-card.is-glass, .mco-tooltip, .maplibregl-ctrl-group,
  .mco-navbar .mco-search-collapse.is-open, .mco-modal::backdrop {
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
}
