/*
 * static/css/map.css — Styles for the interactive map page (/map/).
 *
 * Kept in a standalone stylesheet rather than @tailwindcss directives
 * because the selectors below target MapLibre internals and a custom
 * MapLibre Popup. Design tokens (colour, type) still come from base.css
 * / @theme — the locals below are MapLibre-chrome-only.
 *
 * SNOW-35 removed the #frame phone-mock wrapper; the page now extends
 * public/base.html and the map fills the viewport minus the site nav.
 * SNOW-36 reintroduces search + save-offline as a floating utility
 * cluster pinned top-right (.map-utility-*). The legend selector is
 * retained for SNOW-38. SNOW-174 replaced the bottom-sheet drawer with
 * a MapLibre Popup (.region-popup / .region-tooltip-*).
 */

:root {
    --paper: #f4f1e8;
    --ink: #1a1a1a;
    --muted: #5f5e5a;
    --border: rgba(0, 0, 0, 0.12);
    /* Icon / label colour painted on top of a dark filled circle (the
       scrubber play-forward and play-reverse buttons at rest). Kept as a
       named token so the inversion stays consistent if --ink ever shifts. */
    --on-ink: #ffffff;
    --serif: Georgia, "Times New Roman", serif;
    --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;

    /* —— Map control size scale ————————————————————————————————
       Exactly two sizes for every round / pill-ended control on the map
       surface. The scale exists because five different heights had
       accumulated (22/26/29/32/40), each picked freehand against whatever
       happened to sit next to it at the time.

       The split is by role, not by position:
         lg — every tap target (search, layers, locate, favourite,
              observations, help, basemap download, view-bulletin).
         sm — the bottom-left informational row (the (i) legend toggle and
              the scrubbed-date ribbon) plus the region-name readout, which
              is a label rather than a control.

       Both values are EVEN on purpose. With border-radius: 999px an odd
       height resolves to a fractional corner radius (29px → 14.5px), which
       Chrome's backdrop-filter clip does not honour — it leaves an
       unclipped rectangular patch at the pill's trailing edge. Every glass
       control here carries backdrop-filter, so keep new values even.

       lg is also the accessibility floor: WCAG 2.2 SC 2.5.8 sets a 24px
       minimum, and the old 22px download roundel cleared it only via the
       spacing exception. Do not add a third size without recording why here.

       These live in map.css rather than main.css @theme deliberately. They
       are map-chrome-only, like the locals above; and Tailwind v4 tree-shakes
       @theme variables that no scanned template references, so a token used
       only from this stylesheet would never reach :root.
       ———————————————————————————————————————————————————————— */
    --map-control-lg: 40px;  /* tap targets                       */
    --map-control-sm: 32px;  /* labels + the (i)/date readout row */
}

/* ——————————————————————————————————————————————————————————————
   Floating utility cluster (SNOW-36)
   Two frosted-glass pills pinned top-right: a search pill that
   expands in-place to reveal the existing combobox input, and a
   round "save offline" icon button. Safe-area insets respected so
   the pills never sit under the iOS notch / Dynamic Island.
   ——————————————————————————————————————————————————————————————*/

.map-utility-cluster {
    position: absolute;
    top: calc(env(safe-area-inset-top, 0px) + 12px);
    right: var(--map-edge-inset-right);
    z-index: 4;
    display: flex;
    align-items: flex-start;
    gap: 8px;
}

/* Bottom-right control stack — every roundel (basemap, locate, and the
   flag-gated favourite + observation pills) in one bottom-anchored column,
   with the help (?) roundel at the foot so it sits level with the bottom-left
   (i) legend toggle (both at bottom:60px). Bottom-anchored + column means a
   hidden roundel simply shortens the stack from the top: the column always
   sits on the baseline, never leaving a gap above the corner. */
.map-controls-br {
    position: absolute;
    right: var(--map-edge-inset-right);
    bottom: calc(env(safe-area-inset-bottom, 0px) + 60px);
    z-index: 4;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 8px;
}

/* —— Collapsible control group ————————————————————————————————
   Six roundels in one column read as clutter over the map. Layers, locate
   and the toggle stay out; the other four (download, favourite,
   observations, help) collapse into the toggle.

   The toggle is the LAST child, so the bottom-anchored stack grows and
   shrinks upward out of it: the toggle never moves under the finger that
   is pressing it, and the revealed controls appear directly beneath layers
   and locate in their existing stack order.

   Height is COMPUTED, not measured. The two-size control scale above
   guarantees every child is exactly --map-control-lg tall with an 8px gap,
   so n children occupy n*lg + (n-1)*8. --map-controls-count is set from the
   live child count by mapControlsCollapseInit, which keeps this correct if
   a control ever becomes conditional again. Measuring instead would mean
   reading offsetHeight off an already-clipped box (it returns 0) or
   un-clipping to measure and forcing a reflow on every toggle.

   The wrapper animates height and is what clips; the inner holds the flex
   column, because transitioning height needs a fixed-height box wrapping an
   auto-height one. margin-bottom cancels the parent's 8px gap while
   collapsed, so a collapsed group leaves no phantom slot in the stack. */
#map-controls-collapsible {
    overflow: hidden;
    flex: 0 0 auto;
    height: 0;
    opacity: 0;
    margin-bottom: -8px;
    transition:
        height 300ms cubic-bezier(0.32, 0.72, 0, 1),
        opacity 200ms ease-out,
        margin-bottom 300ms cubic-bezier(0.32, 0.72, 0, 1);
}

.map-controls-br[data-expanded="true"] #map-controls-collapsible {
    height: calc(
        var(--map-controls-count, 4) * var(--map-control-lg) +
            (var(--map-controls-count, 4) - 1) * 8px
    );
    opacity: 1;
    margin-bottom: 0;
}

/* The lift makes the group rise into place rather than materialise; it is on
   the inner so it composites independently of the wrapper's height. */
.map-controls-collapsible-inner {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 8px;
    transform: translateY(10px);
    transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1);
}

.map-controls-br[data-expanded="true"] .map-controls-collapsible-inner {
    transform: translateY(0);
}

.map-controls-toggle {
    width: var(--map-control-lg);
    flex: 0 0 auto;
    justify-content: center;
    cursor: pointer;
}


/* Chevron points up when collapsed ("open upward") and flips to point down
   when expanded ("close downward"), so it always indicates what the next tap
   will do rather than the current state. */
.map-controls-toggle svg {
    transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1);
}

.map-controls-br[data-expanded="true"] .map-controls-toggle svg {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    #map-controls-collapsible,
    .map-controls-collapsible-inner,
    .map-controls-toggle svg {
        transition: none;
    }
}

.map-utility-pill {
    display: inline-flex;
    align-items: center;
    height: var(--map-control-lg);
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 0.5px solid var(--color-border);
    border-radius: 999px;
    box-shadow: var(--shadow-glass);
    color: var(--color-text-1);
    padding: 0;
}

/* SNOW-79: ``.map-utility-pill--button`` (the round 40×40 "Save
   offline" icon button + its saving/saved/error data-state variants
   and the spin keyframe) was removed when the precache feature
   retired. No active call sites remain in the template. If a future
   ticket reintroduces an icon-only utility pill, recreate the rule
   then rather than carrying it as dead CSS. */

/* Basemap layer picker (SNOW-58) — round 40×40 pill that anchors a
   popover of basemap radio options. ``position: relative`` so the
   absolutely-positioned ``.basemap-menu`` tracks the pill rather than
   the cluster, which keeps the popover under the pill even after the
   search pill expands and shifts the layout. */
.map-utility-pill--basemap {
    position: relative;
    width: var(--map-control-lg);
    flex: 0 0 auto;
    justify-content: center;
}


/* SNOW-328: Locate-me roundel — same geometry as the basemap pill: a
   40×40 flex-centred circle that anchors the #locate-toggle button. No
   popover needed; the pill is purely an icon button. */
.map-utility-pill--locate {
    width: var(--map-control-lg);
    flex: 0 0 auto;
    justify-content: center;
}


/* Hide the MapLibre GeolocateControl's own rendered control — our
   #locate-toggle pill is the visible affordance. Hiding only the button left
   the empty .maplibregl-ctrl-group wrapper (a white box with a shadow) visible
   as a ghost in the top-right corner; it was previously masked by the utility
   cluster but is exposed once that no longer covers the corner. Hide the whole
   group — the control still works, it's triggered programmatically from the
   pill, not via this button. The button rule stays as a :has() fallback. */
.maplibregl-ctrl-geolocate {
    display: none;
}

.maplibregl-ctrl-group:has(.maplibregl-ctrl-geolocate) {
    display: none;
}

.basemap-menu {
    position: absolute;
    /* PROTOTYPE: the basemap pill lives in the bottom-right stack, so the menu
       opens to the LEFT of the roundel column and is bottom-anchored —
       mirroring the info panel, which opens to the right of the bottom-left
       (i). right:48px clears the 40px pill + 8px gap. bottom:-96px drops the
       menu's lower edge down past the locate pill (40px) + gap (8px) + the
       report-roundel offset so it lines up with the legend/report baseline
       (bottom:60px) — the safe-area inset cancels out — giving the menu the
       full screen height to grow into. */
    right: 48px;
    bottom: -96px;
    margin: 0;
    padding: 4px;
    list-style: none;
    min-width: 148px;
    /* Anchored at the bottom baseline, the menu grows upward. This dvh cap is
       the no-JS floor; it stops the menu overrunning the whole viewport but
       does NOT reserve the chrome above #map (nav + the conditional off-season
       banner + the top safe-area). basemapPickerInit (static/js/map.js,
       SNOW-511) tightens max-height on open/resize to the measured room
       between #map's top and the menu's baseline so the top rows (the
       Countries section) never clip behind the header on short viewports.

       SNOW-656: that same JS ALSO lowers the baseline itself. The `bottom`
       above is measured from the basemap pill, which is the FIRST roundel in
       a bottom-anchored column — so on a tall column the pill sits high, the
       baseline lands mid-screen, and the menu is squeezed into the room above
       it while a large band below goes unused. On a 375x812 phone that was a
       333px window over 580px of content: the menu opened already scrolled,
       showing neither the Countries section nor its own heading. The JS drops
       the baseline to just above the season scrubber, which roughly doubles
       the room and removes the scroll entirely at that size. This value stays
       as the no-JS fallback. */
    max-height: calc(100dvh - 96px);
    overflow-y: auto;
    background: var(--color-card);
    border: 0.5px solid var(--color-border);
    border-radius: 12px;
    box-shadow: var(--shadow-glass);
    z-index: 5;
}

.basemap-menu-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 8px 12px;
    background: transparent;
    border: 0;
    border-radius: 8px;
    color: var(--color-text-1);
    font: inherit;
    font-size: 13px;
    text-align: left;
    cursor: pointer;
    /* SNOW-328: keep multi-word labels (e.g. "Bulletin groupings") on a
       single line. The menu is shrink-to-fit (absolutely positioned, no
       max-width), so it grows to the longest label rather than wrapping —
       adaptive to any label length, including a future i18n pass, where a
       fixed min-width would not be. */
    white-space: nowrap;
}

.basemap-menu-item::before {
    content: "";
    display: inline-block;
    width: 14px;
    height: 14px;
    flex: 0 0 auto;
    border: 1.5px solid var(--color-border);
    border-radius: 50%;
    background: transparent;
    transition: background 120ms ease, border-color 120ms ease;
}

.basemap-menu-item[aria-checked="true"]::before {
    background: var(--color-text-1);
    border-color: var(--color-text-1);
    box-shadow: inset 0 0 0 2.5px var(--color-card);
}


.basemap-menu-item:focus-visible {
    outline: 2px solid var(--color-text-1);
    outline-offset: 2px;
}

/* SNOW-59: overlay section appended to the basemap popover. The
   separator + section label divide overlay checkboxes from the
   basemap radios above; checkbox glyphs are square rather than the
   round radio dot so the role distinction reads at a glance. */
.basemap-menu-separator {
    height: 1px;
    margin: 6px 8px;
    background: var(--color-border);
}

.basemap-menu-section-label {
    padding: 4px 12px 2px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-2, var(--color-text-1));
    pointer-events: none;
}

.basemap-menu-item--overlay::before {
    border-radius: 3px;
}

/* SNOW-505: sync-status dot painted onto each overlay row and the basemap
   caption below by static/js/map_layer_sync_status.js. Resolved states: a
   green fill (cached / available offline), a grey fill (not cached yet —
   view online first, pulsing while a fetch is in flight), and a red fill
   (offline AND not cached — genuinely unavailable, row disabled).
   Server-rendered "unknown" (hidden) until the popover's first open resolves
   every probe. Colour comes from the --color-sync-* tokens (src/css/main.css
   @theme), never a literal here, so light/dark contrast is handled
   centrally. Data-attribute selector is the design-system's sanctioned
   exception for state-driven styling (see CLAUDE.md "Design system"). */
.sync-dot {
    display: inline-block;
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
}

.sync-dot[data-sync-state="cached"] {
    background: var(--color-sync-ok);
}

.sync-dot[data-sync-state="uncached"] {
    background: var(--color-sync-off);
}

/* SNOW-524: mid-fetch. Same grey fill as "not cached yet" — which is the
   truth while the data is still in flight — but pulsing, so a country click
   visibly shows its layers populating the offline cache (grey, pulsing →
   green) rather than silently flipping green. Driven by the load lifecycle in
   map.js, never by a probe, so it can't race the fetch it represents. */
.sync-dot[data-sync-state="syncing"] {
    background: var(--color-sync-off);
    animation: sync-dot-pulse 900ms ease-in-out infinite;
}

@keyframes sync-dot-pulse {
    0%,
    100% {
        opacity: 1;
        transform: scale(1);
    }

    50% {
        opacity: 0.35;
        transform: scale(0.72);
    }
}

@media (prefers-reduced-motion: reduce) {
    .sync-dot[data-sync-state="syncing"] {
        animation: none;
    }
}

/* Offline AND not cached: a solid red fill. Distinct from the grey "not
   cached yet" (online, advisory) — here the resource genuinely cannot be
   loaded, and its menu row is disabled to match (aria-disabled below). */
.sync-dot[data-sync-state="unavailable-offline"] {
    background: var(--color-sync-blocked);
}

.sync-dot[data-sync-state="unknown"] {
    display: none;
}

/* SNOW-505: basemap sync-status caption row — a non-interactive li carrying
   its own dot plus honest "browsed areas only" copy, distinct from the
   clickable .basemap-menu-item rows above/below it. */
.basemap-menu-caption {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 2px 12px 6px;
    font-size: 11px;
    color: var(--color-text-2, var(--color-text-1));
    pointer-events: none;
}

/* An action row in the layers menu: a one-shot command, not a persistent
   toggle. It never sets aria-checked, so the shared radio-dot glyph would
   otherwise sit permanently empty beside it — hide it for these rows only.

   Introduced by SNOW-492 for "Cache this area for offline", which has since
   moved out of the menu to its own roundel; SNOW-588's "Manage downloads…"
   row (public/partials/_map_embed.html) is the current user. Those rows also
   take role="menuitem" rather than menuitemcheckbox/menuitemradio, which is
   the accessibility half of the same distinction this rule makes visually. */
.basemap-menu-item--action::before {
    display: none;
}

.basemap-menu-item--overlay[aria-checked="true"]::before {
    background: var(--color-text-1);
    border-color: var(--color-text-1);
    box-shadow: inset 0 0 0 2px var(--color-card);
}


/* Disabled overlay (Micro regions — required, can't be toggled). The
   tick still reads as "on" but the row signals it isn't actionable:
   not-allowed cursor, dimmed text, no hover highlight. */
.basemap-menu-item[disabled],
.basemap-menu-item[aria-disabled="true"] {
    color: var(--color-text-2, var(--color-text-1));
    opacity: 0.65;
    cursor: not-allowed;
}

.basemap-menu-item[disabled]:hover,
.basemap-menu-item[aria-disabled="true"]:hover {
    background: transparent;
    /* SNOW-658: and no shared hover infill either — `.hover-affordance`
       covers `[aria-disabled]` itself, but an offline row uses the plain
       `disabled` attribute, and this rule is the one that knows that. */
    background-image: none;
}

/* Search pill — collapsed to a round 40×40 by default; on expansion
   grows to reveal the input beside the icon. */
.map-utility-pill--search {
    width: var(--map-control-lg);
    flex: 0 0 auto;
    overflow: hidden;
    transition: width 180ms cubic-bezier(0.32, 0.72, 0, 1);
}

.map-utility-pill--search[data-state="expanded"] {
    width: min(calc(100vw - 76px), 280px);
}

/* Transparent icon button inside the search pill — also the standalone
   click target when the pill is collapsed. */
.map-utility-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--map-control-lg);
    height: var(--map-control-lg);
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    color: inherit;
    cursor: pointer;
}

/* SNOW-658: the shared hover fill paints THIS element (the 40x40 button),
   which sits inside the pill's own 40x40 circle — so it has to be a circle
   too, or the fill renders as a square inside a round roundel. The radius
   was already stated for :focus-visible below, for the same reason. */
.map-utility-button {
    border-radius: 999px;
}

.map-utility-button:focus-visible {
    outline: 2px solid var(--color-text-1);
    outline-offset: 2px;
    border-radius: 999px;
}

/* SNOW-324: field-report / observations roundel. Reuses .map-utility-pill for
   the 40×40 glass-circle look. As of the homepage rework it is a member of the
   bottom-right stack (#map-controls-br) rather than independently positioned,
   so it shares the stack's flow — same 40×40 icon-pill geometry as the basemap
   / locate pills.

   Review (post-SNOW-645): SNOW-324 originally tinted the warning-triangle
   icon amber (--color-status-warning-text) "to stand out as the
   report-a-hazard action" — Hugo's own reversal of that call, not an
   oversight found later: the colour override is gone, so this icon now
   inherits the same ink/on-ink colour every other roundel in the stack
   does (see .map-utility-pill / #map-controls-br's own colour rules),
   and reads as a hazard-report affordance from its glyph (the warning
   triangle itself) and its position in the stack, not from a one-off
   tint. If a future ticket wants this control to stand out again, that
   is a fresh design decision — do not resurrect this exact rationale by
   copying it back in; it was tried and reversed once already. */
.map-report-control {
    width: var(--map-control-lg);
    flex: 0 0 auto;
    justify-content: center;
}


/* SNOW-414: "Add favourite" pill — same 40x40 icon-button geometry as
   the basemap / locate pills, stacked inside #map-controls-br (not
   independently positioned like .map-report-control, which predates the
   bottom-right stack prototype). The star SVG inherits currentColor. */
.map-utility-pill--favourite {
    width: var(--map-control-lg);
    flex: 0 0 auto;
    justify-content: center;
}


/* Search input inside the expanded pill — hidden until the pill is
   in the expanded state. Width/opacity transition in sync with the
   pill's width transition so the reveal feels like one motion. */
.map-utility-pill--search .search-input {
    flex: 1 1 auto;
    min-width: 0;
    width: 0;
    height: var(--map-control-lg);
    padding: 0;
    margin: 0;
    background: transparent;
    border: 0;
    outline: 0;
    font: inherit;
    font-size: 14px;
    color: var(--color-text-1);
    opacity: 0;
    pointer-events: none;
    transition: opacity 120ms ease 60ms;
}

.map-utility-pill--search[data-state="expanded"] .search-input {
    width: auto;
    padding: 0 14px 0 2px;
    opacity: 1;
    pointer-events: auto;
}

.map-utility-pill--search .search-input::placeholder {
    color: var(--color-text-3);
}

/* Suppress the native clear-button "x" so the pill styling stays uniform. */
.map-utility-pill--search .search-input::-webkit-search-cancel-button,
.map-utility-pill--search .search-input::-webkit-search-decoration {
    -webkit-appearance: none;
    appearance: none;
}

/* Search-results dropdown — anchored below the cluster, right-aligned
   to it. Width caps at 320px on wide viewports and shrinks to fit
   narrow ones. Sits on top of everything that isn't the bottom sheet. */
.search-results {
    position: absolute;
    top: 48px;
    right: 0;
    width: min(calc(100vw - 24px), 320px);
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--color-card);
    border: 0.5px solid var(--color-border);
    border-radius: 12px;
    box-shadow: var(--shadow-glass);
    max-height: 320px;
    overflow-y: auto;
    z-index: 5;
}

.search-result {
    padding: 10px 12px;
    cursor: pointer;
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 10px;
    border-bottom: 0.5px solid var(--border);
}

.search-result:last-child {
    border-bottom: 0;
}

.search-result:hover,
.search-result.active {
    background: #f2efe7;
}

.search-result-text {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.search-result-primary {
    font-size: 13px;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search-result-secondary {
    font-size: 11px;
    color: var(--muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search-result-badge {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 11px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-weight: 500;
    color: var(--muted);
    background: #ece8dc;
    padding: 2px 7px;
    border-radius: 10px;
}

/* Country flag inside the search result badge. Width is set per country
   because inline SVG without explicit dimensions defaults to 300×150;
   the square CH flag uses its 1:1 ratio and the 3:2 AT/FR/IT flags all
   share a width. A hairline border keeps white-bordered flags legible
   on the badge's beige background. */
.search-result-flag {
    flex: 0 0 auto;
    height: 9px;
    border: 0.5px solid rgba(0, 0, 0, 0.15);
    border-radius: 1px;
}

.search-result-flag--ch {
    width: 9px;
}

.search-result-flag--at,
.search-result-flag--fr,
.search-result-flag--it {
    width: 13px;
}

.search-empty {
    padding: 12px;
    text-align: center;
    font-size: 12px;
    color: var(--muted);
}

/* Full-viewport map pages (/ and /map/) override the default body class so
   the body is sized to 100dvh rather than Tailwind's `min-h-screen`
   (100vh). On mobile, 100vh measures the *large* viewport — it ignores the
   browser address bar — so a 100vh flex column overshoots the visible area
   and pushes the bottom-pinned season scrubber and the in-flow site footer
   below the fold. 100dvh (the dynamic viewport height) tracks the visible
   area, so nav + map + footer all fit. On desktop the two units are equal,
   so this is a no-op there. The column stays `flex flex-col`, so #map's
   `flex: 1 1 auto` still fills the space between the nav and the footer. */
.map-fullscreen {
    min-height: 100dvh;
}

#map {
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    position: relative;
}

/* SNOW-475: fixed pin for the touch-friendly place-picker
   (static/js/place_picker.js). Sits above the MapLibre canvas — which
   paints with the default z-index:auto and so always loses to any
   explicitly-stacked sibling here — but below every other numbered
   overlay (scrubber=2 and up; see the stacking note above .map-legend
   below), since the pin only needs to clear the map itself, not compete
   with chrome docked at the screen edges. translate(-50%,-100%) lands the
   teardrop's point, not its centre, on the anchor point the picker
   unprojects to get the chosen coordinate. pointer-events: none so it
   never intercepts the pan/zoom gestures used to move the map underneath.

   SNOW-538: --map-place-pin-lift raises that anchor above the map's centre
   so the pin clears the bottom-docked sheet driving the placement — the
   report form is tall enough on a phone to swallow a centred pin. The
   picker writes the value (0 whenever nothing covers the centre, e.g. on a
   wide viewport) and unprojects the same offset, so pin and coordinate can
   never disagree. */
.map-place-pin {
    position: absolute;
    left: 50%;
    top: calc(50% - var(--map-place-pin-lift, 0px));
    transform: translate(-50%, -100%);
    z-index: 1;
    pointer-events: none;
}

.map-place-pin[hidden] {
    display: none;
}

.hint {
    padding: 10px 12px;
    font-size: 11px;
    color: var(--muted);
    text-align: center;
    border-top: 0.5px solid var(--border);
}

/* Collapsible map-info panel (SNOW-38, redesigned SNOW-230) — top-left
   overlay holding the danger-scale legend, the active tile attribution,
   and the SLF data licence. Single (i) entry point so map chrome stays
   uncluttered. The card is absolutely positioned relative to the toggle
   and opens to the right so it never collides with the scrubber.
   Stacking: scrubber=2, legend=3 (also season-ribbon at top-left),
   utility cluster=4. (The off-season notice is no longer a #map chip — it's
   the full-width #map-offseason-bar above the map, SNOW-445.) */

.map-legend {
    position: absolute;
    /* PROTOTYPE (homepage rework): (i) legend moves to the bottom-left corner,
       horizontally level with the report roundel (bottom:60px). The scrubbed-
       date ribbon sits to the right of the (i) toggle on the same row; the
       legend card opens upward (above the row), so it clears both. */
    left: var(--map-edge-inset-left);
    bottom: calc(env(safe-area-inset-bottom, 0px) + 60px);
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 8px;
    /* SNOW-645 review: held as a custom property so .map-legend-card's own
       max-height, below, can reuse the exact figure this element is
       already offset from the viewport bottom by, plus the card's own
       8px gap — see that rule's own comment for why this now has a
       bound at all. */
    --map-legend-card-bottom-offset: calc(env(safe-area-inset-bottom, 0px) + 60px + 8px);
}

/* ``.map-date-pill`` (the 28px readout that floated above the scrubber thumb)
   was removed along with its mapDatePillInit IIFE in static/js/map.js. The
   scrubbed date has been reported by .map-date-ribbon in the bottom-left row
   since SNOW-314; no #map-date-pill element has existed in _map_embed.html
   since, so both the rule and its initialiser were dead. It was also a sixth
   control height competing with the two-value scale above. */

.map-legend-toggle {
    width: var(--map-control-sm);
    height: var(--map-control-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 0.5px solid var(--color-border);
    border-radius: 999px;
    box-shadow: var(--shadow-glass);
    color: var(--color-text-1);
    cursor: pointer;
}

/* Scrubbed-date ribbon — the timeline's own readout, sitting to the right of
   the (i) legend toggle in the bottom-left cluster (a flex sibling on the same
   row), close to the season scrubber that drives it. Moved here from the top
   #region-readout so the timeline and the date it controls share the bottom
   edge, leaving the top bar to name the region. The legend card opens upward
   (above this row), so the two never overlap. Glass pill matching the (i)
   toggle idiom; map.js fills the text and toggles [hidden]. */
.map-date-ribbon[hidden] {
    display: none;
}

/* SNOW-656: the bulletin-fill flyout — five opacity steps, opening to the
   LEFT of the bottom-right roundel column from #map-fill-toggle.

   Left, at right:48px, is the same side and offset .basemap-menu uses, so
   every panel in this stack appears from the same edge.

   It is a child of .map-controls-br, NOT of the roundel it belongs to. The
   roundel sits inside #map-controls-collapsible, which is `overflow: hidden`
   for its height animation — and that clips both axes, so a panel extending
   left out of that box would simply be cut off. Anchoring to the stack keeps
   it out of the clip; mapFillControlInit sets `top` from the roundel's own
   offset on open, so it still lines up with the control that opened it.

   Five 44px targets is 220px, which is why this is a flyout at all: it does
   not fit inline on the canvas at 375px, and a control this size has no
   business being permanently on the map. */
.map-fill-flyout {
    position: absolute;
    right: 48px;
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 0 4px;
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 0.5px solid var(--color-border);
    border-radius: 999px;
    box-shadow: var(--shadow-glass);
    z-index: 5;
}

.map-fill-flyout[hidden] {
    display: none;
}

.map-fill-step {
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: transparent;
    border: 0;
    /* Rounded square, not a circle: everything inside this control — the
       swatches and the selection ring — is a rounded square, and a circular
       hover blob around them read as a different family of shape. */
    border-radius: 10px;
    cursor: pointer;
}


.map-fill-step:focus-visible {
    outline: 2px solid var(--color-text-1);
    outline-offset: -3px;
}

/* The swatch IS the preview: each step paints the same colour at the opacity
   that step applies, so the control shows what it does rather than naming it.
   A percentage label would need translating in five places to say what the
   ramp says on its own — and every button carries an accessible name for
   anyone who can't see it. */
.map-fill-step-swatch {
    width: 18px;
    height: 18px;
    border-radius: 5px;
    border: 1px solid var(--color-border);
    background: var(--color-text-1);
}

.map-fill-step-swatch[data-step="0"] { background: transparent; }
.map-fill-step-swatch[data-step="0.25"] { opacity: 0.25; }
.map-fill-step-swatch[data-step="0.5"] { opacity: 0.5; }
.map-fill-step-swatch[data-step="0.75"] { opacity: 0.75; }
.map-fill-step-swatch[data-step="1"] { opacity: 1; }

/* The selected step.
   The ring hangs off the SWATCH, not the 44px button. On the button it was a
   heavy circle at the full target size, which both fought the rounded-square
   swatch inside it and sat flush against the pill's own edge — reading as if
   the selection had broken out of the ribbon.
   Two box-shadow rings, both taking the swatch's own border-radius so they
   are concentric with it: a card-coloured gap, then a hairline in the muted
   text tone rather than full black. 27px overall inside a 44px target leaves
   8.5px of clear space on every side, so it stays visibly within the pill
   while the tap target is untouched. */
.map-fill-step[aria-checked="true"] .map-fill-step-swatch {
    box-shadow:
        0 0 0 3px var(--color-card),
        0 0 0 4.5px var(--color-text-2);
}

.map-date-ribbon {
    display: inline-flex;
    align-items: center;
    height: var(--map-control-sm);
    padding: 0 12px;
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 0.5px solid var(--color-border);
    border-radius: 999px;
    box-shadow: var(--shadow-glass);
    color: var(--color-text-1);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    white-space: nowrap;
    user-select: none;
}

.map-legend-card {
    position: absolute;
    /* Open upward: anchor the card above the (i)-toggle + date-ribbon row
       (100% = row height) with an 8px gap, left-aligned to the toggle, so it
       clears both controls instead of expanding over them. */
    left: 0;
    bottom: calc(100% + 8px);
    min-width: 200px;
    max-width: 260px;
    padding: 10px 12px;
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 0.5px solid var(--color-border);
    border-radius: 8px;
    box-shadow: var(--shadow-glass);
    /* SNOW-645 review: bounded height, added — the card opens UPWARD from
       a fixed low anchor with no height limit at all, so a longer legend
       (more danger levels, longer attribution strings in a future locale)
       would grow into the top of the viewport with nothing to stop it —
       the same defect class as #home-intro and the "Manage downloads"
       sheet. Bound to the room between the shared top safe-area/gap
       token every floating map surface uses (--map-edge-inset-top,
       src/css/main.css) and this card's own distance from the viewport
       bottom (--map-legend-card-bottom-offset, set on the parent
       .map-legend above — the offset this card is already anchored from,
       plus its own 8px gap). `dvh`, not `vh`: see .home-intro-card's own
       comment for why. */
    max-height: calc(100dvh - var(--map-edge-inset-top) - var(--map-legend-card-bottom-offset));
    overflow-y: auto;
    font-size: 11px;
    line-height: 1.5;
    color: var(--color-text-1);
    transform-origin: bottom left;
    opacity: 0;
    transform: translateY(4px) scale(0.98);
    pointer-events: none;
    transition: opacity 160ms ease-out, transform 160ms ease-out;
}

/* Hairline divider between the three sections of the info card so the
   legend, map attribution and bulletin attribution read as separate
   groups without needing extra chrome. */
.map-legend-section + .map-legend-section {
    border-top: 0.5px solid var(--color-border);
    margin-top: 8px;
    padding-top: 8px;
}

/* Attribution text styling — smaller and more muted than the legend rows
   so the legend remains the primary read in the card. */
.map-legend-attribution {
    margin: 0;
    font-size: 10px;
    line-height: 1.4;
    color: var(--color-text-2);
}

.map-legend-attribution-link {
    color: inherit;
    text-decoration: underline;
}

.map-legend-attribution-link:hover {
    color: var(--color-text-1);
}

.map-legend[data-state="expanded"] .map-legend-card {
    opacity: 1;
    transform: none;
    pointer-events: auto;
}

.map-legend-title {
    font-weight: 500;
    color: var(--color-text-2);
    margin-bottom: 4px;
}

.map-legend-row {
    display: flex;
    align-items: center;
    gap: 6px;
}

.map-legend-swatch {
    width: 10px;
    height: 10px;
    display: inline-block;
    border-radius: 2px;
    border: 0.5px solid rgba(0, 0, 0, 0.15);
}

/* No-rating grey mirrors the choropleth's RATING_COLOURS.no_rating (#e0e0e0
   in map.js) — the fill an unrated region gets on the map; no EAWS token
   exists for it, so the literal is the single source of truth shared with JS. */
.map-legend-swatch--no-rating    { background: #e0e0e0; }
.map-legend-swatch--low          { background: var(--color-eaws-low); }
.map-legend-swatch--moderate     { background: var(--color-eaws-moderate); }
.map-legend-swatch--considerable { background: var(--color-eaws-considerable); }
.map-legend-swatch--high         { background: var(--color-eaws-high); }

/* SNOW-78: legend pin matches the on-map resort circle paint — neutral
 * dark fill with a thin white stroke so it reads against any basemap or
 * EAWS rating fill. Smaller than the rating swatches so the pin looks
 * like a point of interest rather than a coloured area. */
.map-legend-pin {
    width: 8px;
    height: 8px;
    display: inline-block;
    border-radius: 50%;
    background: #1a1a1a;
    border: 1px solid #ffffff;
    box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.25);
}

/* Favourite / observation legend keys — a 12×12 box that centres the marker
   SVG (the star / flag reproduced from the on-map layers). Sized between the
   10px rating swatches and the row text so the marker reads as a point of
   interest, matching the resort-pin treatment above. */
.map-legend-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 12px;
    height: 12px;
    flex: 0 0 auto;
}

.maplibregl-ctrl-attrib {
    font-size: 9px !important;
}

/* SNOW-79: ``.offline-cta`` (the "Save offline" full-width CTA that
   sat below the search field) was retired with the rest of the
   precache feature. Removed wholesale — no remaining call sites.
   SNOW-174: bottom-sheet drawer (.sheet, .region-peek-*, .region-expanded-*)
   replaced by a MapLibre Popup — see .region-popup / .region-tooltip-*
   below. */

/* MapLibre Popup brand overrides (SNOW-174) — applied via className:
 * "region-popup" on the Popup constructor. The default MapLibre popup
 * chrome is replaced with the same frosted-glass card used by the
 * utility cluster; the close button inherits the brand ink colour. */

.region-popup .maplibregl-popup-content {
    padding: 14px 16px;
    border-radius: 12px;
    /* --color-popup-glass (src/css/main.css @theme, SNOW-419) is more
     * translucent than --color-glass so the choropleth shows through;
     * blur is bumped to keep text legible against the busy map
     * background. The token flips to its dark-mode value automatically
     * (redefined inside .dark {} in main.css) — no per-rule .dark
     * override needed here, same as every other var(--color-glass) use
     * in this file. */
    background: var(--color-popup-glass);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    border: 2px solid var(--color-border, rgba(0, 0, 0, 0.12));
    box-shadow: var(--shadow-glass, 0 4px 16px rgba(0, 0, 0, 0.12));
    color: var(--color-text-1, #1a1a1a);
    font-family: var(--sans);
    /* Constrain width; the maxWidth on the Popup constructor is the outer
     * bound — this prevents the content from touching the tip. */
    min-width: 200px;
}

.region-popup[data-level="low"]          .maplibregl-popup-content { border-color: var(--color-eaws-low); }
.region-popup[data-level="moderate"]     .maplibregl-popup-content { border-color: var(--color-eaws-moderate); }
.region-popup[data-level="considerable"] .maplibregl-popup-content { border-color: var(--color-eaws-considerable); }
.region-popup[data-level="high"]         .maplibregl-popup-content { border-color: var(--color-eaws-high); }
.region-popup[data-level="very_high"]    .maplibregl-popup-content { border-color: var(--color-eaws-very-high); }

.region-popup .maplibregl-popup-close-button {
    font-size: 18px;
    line-height: 1;
    color: var(--color-text-2, #5f5e5a);
    padding: 4px 8px;
    border-radius: 4px;
}

.region-popup .maplibregl-popup-close-button:hover {
    background: rgba(0, 0, 0, 0.06);
    color: var(--color-text-1, #1a1a1a);
}

/* Region tooltip content — the server-rendered HTML injected by
 * region_summary() into popup.setHTML(). Structural region metadata:
 * inline chip + name header row, geographic breadcrumb, date caption,
 * and bulletin CTA (SNOW-174). */

.region-tooltip {
    display: flex;
    flex-direction: column;
    gap: 6px;
    /* Pad right so the name doesn't run under MapLibre's close button. */
    padding-right: 24px;
}

/* Header row: danger-tile chip vertically centred against the region name. */
.region-tooltip-header {
    display: flex;
    align-items: center;
    gap: 10px;
}

.region-tooltip-name {
    font-family: var(--serif);
    font-size: 17px;
    font-weight: 500;
    line-height: 1.2;
    margin: 0;
    color: var(--color-text-1, #1a1a1a);
}

.region-tooltip-breadcrumb {
    margin: 0;
    font-size: 11px;
    line-height: 1.5;
    color: var(--color-text-2, #5f5e5a);
}

/* Bulletin CTA — styled as a lightweight secondary button. */
.region-tooltip-bulletin-link {
    display: inline-block;
    margin-top: 8px;
    padding: 6px 12px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 500;
    text-decoration: none;
    color: var(--color-text-1, #1a1a1a);
    background: var(--color-bg-2, rgba(0, 0, 0, 0.05));
    border: 1px solid var(--color-border, rgba(0, 0, 0, 0.1));
}

.region-tooltip-bulletin-link:hover {
    background: var(--color-bg-3, rgba(0, 0, 0, 0.1));
}

/* Default icon shown in place of the danger-tile when no bulletin exists. */
.region-tooltip-icon-default {
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    opacity: 0.45;
}

/* Muted label shown in place of the bulletin CTA when no bulletin exists. */
.region-tooltip-no-bulletin {
    margin-top: 8px;
    padding: 6px 0;
    font-size: 13px;
    font-style: italic;
    color: var(--color-text-2, #5f5e5a);
}

/* Placeholder for future editorial content — empty by default. */
.region-tooltip-editorial {
    margin-top: 4px;
}

/* Season scrubber — floating bottom-pinned pill (SNOW-37 / SNOW-230).
   Attribution was relocated to top-right so the scrubber now sits flush
   at the bottom edge above the safe-area inset.

   Review (post-SNOW-645, Hugo's dark-mode report): background was a
   hardcoded rgba(255, 255, 255, …) with no dark-mode counterpart, so this
   pill stayed a light disc in dark mode while everything around it went
   dark. src/css/main.css's --color-glass is exactly this token — the
   frosted-glass surface the map utility cluster already uses — and it
   inverts under .dark, so this pill now tracks the theme the same way
   .map-utility-pill does. */

.season-scrubber,
.season-scrubber-demo {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 16px;
    background: var(--color-glass);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 0.5px solid var(--border);
    border-radius: 999px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 11px;
    user-select: none;
}

/* On the map page, the scrubber is pinned over the MapLibre viewport. */
.season-scrubber {
    position: absolute;
    left: 12px;
    right: 12px;
    bottom: calc(5px + env(safe-area-inset-bottom, 0px));
    z-index: 2;
}

/* On the component-library page, the demo pill flows in normal document
   order and sits on the parchment surface rather than a map. */
.season-scrubber-demo {
    position: relative;
    margin: 8px 0;
}

.season-scrubber-track {
    flex: 1 1 auto;
    height: 3px;
    background: rgba(0, 0, 0, 0.08);
    border-radius: 2px;
    position: relative;
    /* overflow: visible so the date pill (absolutely positioned above the
       track) is not clipped by the track height. */
    overflow: visible;
    cursor: grab;
    transition: height 160ms cubic-bezier(0.32, 0.72, 0, 1);
}

/* SNOW-314: when a region's season is painted into the track, it grows from
   the thin grey rail into the full ribbon. The thumb rides over the colours;
   the track itself still owns drag/click-to-scrub. */
.season-scrubber-track[data-ribbon="on"] {
    height: 14px;
    border-radius: 7px;
}

@media (prefers-reduced-motion: reduce) {
    .season-scrubber-track { transition: none; }
}

/* Decorative season-ribbon fill inside the track. One flex cell per day,
   no pointer events (the track beneath handles scrubbing), clipped to the
   track's rounded rail. */
.scrubber-ribbon {
    position: absolute;
    inset: 0;
    display: flex;
    border-radius: inherit;
    overflow: hidden;
    pointer-events: none;
    z-index: 0;
}

.scrubber-ribbon-cell {
    flex: 1 1 0;
    min-width: 0;
    height: 100%;
}

/* Month-boundary hairline (SNOW-314). At mobile density each day cell is
   sub-pixel, so per-day gaps are impossible; instead a thin overlay marks the
   first-of-month cells (~6 across the season) for orientation. Drawn as an
   absolutely-positioned pseudo-element so it adds no width — the cells stay
   aligned 1:1 with the scrubber thumb. */
.scrubber-ribbon-cell--month {
    position: relative;
}

.scrubber-ribbon-cell--month::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 1px;
    background: rgba(0, 0, 0, 0.4);
    pointer-events: none;
}

.season-scrubber-thumb {
    position: absolute;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--ink);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    touch-action: none;
    /* Ride over the decorative ribbon fill. */
    z-index: 2;
}

.season-scrubber-track.dragging {
    cursor: grabbing;
}

.season-scrubber-track.animating .season-scrubber-thumb {
    transition: left 200ms cubic-bezier(0.32, 0.72, 0, 1);
}

/* Shared geometry for every transport button on the scrubber — play,
   skip-to-start, play-reverse, play-forward, skip-to-end. Each button is
   a circular flex-centred target with no padding or margin; the per-variant
   rules below add size, fill, and stroke. SVG state-swap (play icon when
   stopped, stop icon when playing) is shared across the play-reverse and
   play-forward variants. */
.scrubber-transport-button {
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    cursor: pointer;
}

.scrubber-transport-button[data-state="playing"] .scrubber-play-icon,
.scrubber-transport-button[data-state="stopped"] .scrubber-stop-icon {
    display: none;
}

/* Play-reverse and play-forward transport buttons — 22×22 dark-filled
   circles that act as a symmetric pair flanking the track. The filled
   dark fill picks them out as the primary actions. SVG state is swapped
   via the data-state attribute set by static/js/map.js. */
.scrubber-play,
.scrubber-reverse {
    width: 22px;
    height: 22px;
    background: var(--ink);
    color: var(--on-ink);
    border: none;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

.scrubber-play:hover,
.scrubber-reverse:hover {
    background: #000;
}

/* Skip-to-start / skip-to-end transport buttons — same shared geometry
   as .scrubber-play but 22×22 so the play button remains the visual
   anchor. Colour inherits from the scrubber context (dark text on the
   frosted pill) rather than the inverted play-button style. */
.scrubber-skip {
    width: 22px;
    height: 22px;
    background: transparent;
    color: var(--ink);
    border: 0.5px solid var(--border);
}

.scrubber-skip:hover {
    background: rgba(0, 0, 0, 0.06);
}


/* Season scrubber loading state (SNOW-234).
   While the season-ratings fetch is in flight the scrubber carries
   data-state="loading". Transport buttons and the track are hidden via
   display:none so pointer events are naturally suppressed until the data
   arrives. The pill retains its full left:12px / right:12px footprint so
   there is no layout reflow when the state switches to "ready".
   The -demo variant mirrors the live widget class for the component library
   demo wrapper, which uses class="season-scrubber-demo" instead of
   class="season-scrubber". */
.season-scrubber[data-state="loading"] .scrubber-transport-button,
.season-scrubber-demo[data-state="loading"] .scrubber-transport-button {
    display: none;
}

.season-scrubber[data-state="loading"] .season-scrubber-track,
.season-scrubber-demo[data-state="loading"] .season-scrubber-track {
    display: none;
}

/* Once the fetch resolves successfully, the loading placeholder is hidden.
   Error state intentionally does NOT hide the placeholder — the JS error
   path writes "Season data unavailable" into the element, so it must stay
   visible. Controls are hidden in error state by the rule below. */
.season-scrubber[data-state="ready"] .season-scrubber-loading,
.season-scrubber-demo[data-state="ready"] .season-scrubber-loading {
    display: none;
}

/* In error state the controls are hidden (no data to scrub) but the
   loading/error placeholder remains visible so the user sees the message. */
.season-scrubber[data-state="error"] .scrubber-transport-button,
.season-scrubber-demo[data-state="error"] .scrubber-transport-button,
.season-scrubber[data-state="error"] .season-scrubber-track,
.season-scrubber-demo[data-state="error"] .season-scrubber-track {
    display: none;
}

/* Loading / error placeholder text — centred single-line label that sits
   inside the existing pill flex container. Inherits the pill's monospace
   font stack and matches the muted label colour used elsewhere on the
   scrubber (the date pill uses --ink; the placeholder is secondary copy). */
.season-scrubber-loading {
    flex: 1 1 auto;
    text-align: center;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 11px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* —————————————————————————————————————————————————————————
   SNOW-314 — Season ribbon
   A compact horizontal strip of coloured day cells. One cell per calendar
   day, coloured by peak danger rating for the selected region. The cells
   are painted into the scrubber's own track and are decorative — the
   thumb sitting over them is what the user drags. (They were clickable
   once, dispatching snowdesk:scrub-to; that stopped when the cells moved
   into the track, and SNOW-615 removed the dead listener.)
   ————————————————————————————————————————————————————————— */

/* PROTOTYPE (homepage rework): the season header (#region-readout) moves
   from the bottom — where it sat above the scrubber — to the top bar,
   centred between the (i) legend toggle (top-left) and the utility cluster
   (top-right). Since SNOW-314 this container holds only the readout chip
   (the per-day colour cells live in .scrubber-ribbon inside the scrubber),
   so repositioning it relocates the header alone; the scrubber stays put.
   z-index 3 keeps it above the choropleth and level with the legend. */
.season-ribbon {
    position: absolute;
    /* Shares the search roundel's top inset (12px). The row centres itself
       against that roundel via .ribbon-header's --map-control-lg min-height
       below, rather than by nudging this container down — it previously sat
       at 18px purely to optically centre a 29px readout against the 40px
       roundel, which had to be re-derived by hand every time a child's
       height changed. */
    top: calc(env(safe-area-inset-top, 0px) + 12px);
    left: var(--map-edge-inset-left);
    /* Never let a long region name push the row under the search roundel.
       Reserves the roundel plus both 12px insets and a 12px gap; the readout
       truncates instead (see .region-readout-name). Long EAWS names
       ("Nördliche Rätikon und Silvretta") overran the roundel on a 375px
       viewport before this cap existed. */
    max-width: calc(100% - 24px - var(--map-control-lg) - 12px);
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
}

.ribbon-header {
    display: flex;
    align-items: center;
    /* Centres every child — the sm readout and the lg action roundels — on
       the same line as the top-right search roundel, whatever their heights. */
    min-height: var(--map-control-lg);
    gap: 8px;
    padding: 0 2px;
    /* Let the readout shrink rather than overflow .season-ribbon's max-width. */
    min-width: 0;
    max-width: 100%;
}


/* SNOW-314: persistent focused-region readout — names the region whose season
   fills the scrubber track and shows its danger for the scrubbed day. Survives
   playback (cache-fed by map.js); the source of truth for "which region", with
   the map popup demoted to on-tap detail. */
/* SNOW-642: the readout is no longer hidden-until-focused. It used to carry
   [hidden] with nothing selected, and because both sibling controls key off
   #region-readout.has-region, the whole ribbon header emptied out at once —
   which read as the ribbon vanishing rather than as "nothing is selected".
   It now persists with a "No region selected" empty state and both controls
   visible-but-disabled. The rule is kept because the element is still
   [hidden] in one case: a page with no ribbon data at all. */
.region-readout[hidden] {
    display: none;
}

/* Empty state — the leaf reads "No region selected" in muted, regular
   weight, so it is legible as an absence rather than as a region whose name
   happens to be those three words. The swatch is suppressed by map.js
   (transparent background), not here, since it is set inline per rating. */
.region-readout:not(.has-region) .region-readout-leaf {
    font-weight: 400;
    color: var(--muted);
}

.region-readout {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    /* 13px, up from 11px: at monospace 11px the region name read as chrome
       rather than as the label for what the map is currently showing. */
    font-size: 13px;
    /* Frosted pill so the label stays legible over the map (it was getting
       lost against the choropleth). Matches the scrubber / utility-pill idiom
       — review (post-SNOW-645): including now using --color-glass rather than
       a hardcoded rgba(255, 255, 255, …), which had no dark-mode counterpart.

       The explicit --map-control-sm height replaces a padding-derived one that
       computed to 29px — odd, so border-radius: 999px resolved to a 14.5px
       corner and Chrome's backdrop-filter clip left an unclipped white
       rectangle at the pill's trailing edge. */
    height: var(--map-control-sm);
    box-sizing: border-box;
    padding: 0 12px;
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 0.5px solid var(--border);
    border-radius: 999px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
    /* PROTOTYPE (homepage rework): the pill is now an info display (a <div>);
       the bulletin link is the separate .region-readout-action roundel. */
    color: var(--ink);
    /* Truncate rather than overflow the .season-ribbon cap above. */
    min-width: 0;
    overflow: hidden;
}

.region-readout-name {
    font-weight: 600;
    color: var(--ink);
    /* Ellipsis the region name when .season-ribbon's max-width bites, so the
       row can never reach the search roundel. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* PROTOTYPE (homepage rework): breadcrumb prefix (Major › Minor ›) — muted and
   regular weight so the micro-region leaf carries the emphasis. Hidden on
   mobile (see the max-width:640px block below). */
.region-readout-crumbs {
    font-weight: 400;
    color: var(--muted);
}

.region-readout-leaf {
    font-weight: 600;
    color: var(--ink);
}

/* PROTOTYPE (homepage rework): the "view bulletin" action is a separate
   roundel (a glass circle with an arrow + a "View bulletin" tooltip), sitting
   to the right of the readout pill.

   SNOW-642: always displayed. It was `display: none` until a
   `#region-readout.has-region ~ …` sibling rule matched, which meant that with
   nothing selected this roundel, the download roundel and the readout all
   disappeared together and the ribbon header collapsed to an empty row. It is
   now present-but-disabled instead (the [aria-disabled] rule below), so the
   header keeps its shape and an unselected map is never mistaken for a ribbon
   that failed to load. */
.region-readout-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--map-control-lg);
    height: var(--map-control-lg);
    flex: 0 0 auto;
    border-radius: 999px;
    color: var(--ink);
    /* Review (post-SNOW-645, Hugo's dark-mode report): was a hardcoded
       rgba(255, 255, 255, …) with no dark-mode counterpart — see
       --color-glass's own comment in src/css/main.css. */
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 0.5px solid var(--border);
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
    text-decoration: none;
}

/* This control has had three hover treatments. It began with a bespoke
   lighter rgba(255, 255, 255, …) background plus a bigger box-shadow, with
   no dark-mode counterpart; the post-SNOW-645 review matched it to the
   roundel family's filter: brightness(1.04); SNOW-658 replaced that family
   idiom, here and everywhere, with the shared `.hover-affordance` class the
   markup now carries — see the note above `.map-download-control`, the
   roundel this one sits beside in the same ribbon header. */

/* SNOW-642: disabled — nothing is focused, so there is no bulletin to open.
   map.js sets aria-disabled and drops the href; this is the matching visual.
   Keyed on [aria-disabled] rather than on a sibling .has-region rule so the
   styling and the accessibility state cannot drift apart — the attribute a
   screen reader reads is the same one that dims it.

   The glyph dims and the glass shell stays at full strength, which is the
   treatment .map-download-control's own unavailable states already use and
   for the same reason: these roundels float over open basemap, where a
   40%-opacity glass circle is close to invisible, and a control the user
   cannot see reads as a missing feature rather than a disabled one. Dimming
   the whole roundel here would have re-introduced exactly the disappearance
   this ticket exists to remove.

   Declared AFTER the hover/focus rules above and restating what they change
   — filter: none, now that hover/focus is a brightness filter rather than a
   background/box-shadow swap (see that rule's own comment) — so an
   equally-specific later rule cannot leave a disabled roundel looking
   hovered; :focus-visible survives pointer-events: none. */
.region-readout-action[aria-disabled="true"],
.region-readout-action[aria-disabled="true"]:hover,
.region-readout-action[aria-disabled="true"]:focus-visible {
    color: var(--muted);
    cursor: not-allowed;
    filter: none;
}

.region-readout-action[aria-disabled="true"] svg {
    opacity: 0.45;
}

@media (prefers-reduced-motion: reduce) {
    .region-readout-action {
        transition: none;
    }
}

/* PROTOTYPE (homepage rework): on narrow viewports the breadcrumb prefix
   (Major › Minor ›) is dropped below 640px so the chip clears the search
   roundel; the leaf and the action roundel stay. */
@media (max-width: 640px) {
    .region-readout-crumbs {
        display: none;
    }
}

/* SNOW-521/SNOW-522: the "Download basemap" control for the focused region.
   An earlier iteration rendered one small roundel per breadcrumb tier
   (Major/Minor/Micro); only micro survived — Major/Minor were dropped because
   their per-tier detail floors made the sizes non-monotonic with containment
   (an L1 region could read smaller than the L2 it contains).

   SNOW-521 moved it out of the region-readout row and into the bottom-right
   stack (#map-controls-br), joining the layers pill it shares a concern with
   — the layers menu is the cache-state dashboard, and this is the other
   control that writes to that cache. Renamed .region-readout-download →
   .map-download-control to match. With that move it stopped being
   hidden-until-focused and became permanently present, carrying a
   "no-region" state instead, so the bottom-right stack's composition never
   shifted as the user selected and deselected regions.

   SNOW-522 moved it BACK into the ribbon header (between #region-readout and
   #region-readout-action) — the bottom-right slot became the new
   custom-area download control's home instead. Back in the header it is
   hidden-until-focused again, exactly like #region-readout-action beside it
   (the sibling rule below): a permanently-present roundel next to two other
   hidden-until-focused controls would be the odd one out, which was only
   true while it stood alone in the bottom-right stack. The class keeps its
   .map-download-control name — nothing in this ruleset is position-specific
   — and the "no-region"/"disabled"/"offline" muted-glyph treatment further
   down still applies for the rarer case where a region IS focused but has no
   computed download summary.

   The now-removed .region-readout-downloads flex wrapper existed only to sit
   three tier roundels in a row inside .ribbon-header. */

.map-download-control {
    /* SNOW-642: always displayed. This was `display: none` with a
       `#region-readout.has-region ~ .map-download-control` sibling rule
       revealing it once a region was focused — so with nothing selected it
       vanished along with its two neighbours in .ribbon-header and the whole
       header collapsed to an empty row, which read as the ribbon failing to
       load rather than as "nothing is selected".

       Nothing new was needed for the unfocused state: the control already
       ships `data-download-state="no-region"` with `aria-disabled="true"` and
       a "Select a region to download its basemap" label, and the
       [data-download-state="no-region"] rule further down already dims it and
       mutes its icon. That state deliberately survived in the machine after
       the CSS took over the hiding (see _map_download_control.html's own
       comment), so this was a deletion, not a new state. */
    display: inline-flex;
    position: relative;
    overflow: hidden;
    align-items: center;
    justify-content: center;
    width: var(--map-control-lg);
    height: var(--map-control-lg);
    flex: 0 0 auto;
    padding: 0;
    border-radius: 999px;
    color: var(--ink);
    /* Review (post-SNOW-645, Hugo's dark-mode report): was a hardcoded
       rgba(255, 255, 255, …) with no dark-mode counterpart — see
       --color-glass's own comment in src/css/main.css. */
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 0.5px solid var(--border);
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
    cursor: pointer;
}

/* SNOW-522: the custom-area download control shares .map-download-control's
   glass-roundel chrome and lives in the bottom-right stack with no
   dependency on a focused region.

   This override used to restore `display: inline-flex` over the shared
   `display: none` default. SNOW-642 made always-shown the shared default —
   the per-region control is no longer hidden-until-focused either — so the
   declaration is now redundant. Kept as an explicit statement that this
   control's visibility has never depended on a focused region, which is the
   one fact about it most likely to be re-broken by a future change to the
   rule it inherits from.

   Review (post-SNOW-645, Hugo's screenshot: "just make it work the same
   as ALL THE OTHER ROUNDELS"): `color` is pinned to --color-text-1
   (src/css/main.css, already dark-mirrored) rather than inheriting the
   shared rule's --ink (map.css's own private token) — the two are a few
   RGB units apart (#1a1a1a vs #1a1916) and read as identical to the eye,
   but a byte-exact computed-style comparison against a neighbouring
   .map-utility-pill (which uses --color-text-1 itself) is the actual bar
   this control now has to clear, in both themes, with one declaration —
   --color-text-1 already resolves to the right value under .dark without
   a second rule. This is the one intentional difference left after the
   "no fill" reversal below: everything else about this control now comes
   from the shared .map-download-control rule, same as its neighbours get
   their own look from .map-utility-pill alone. */
.map-custom-download-control {
    display: inline-flex;
    color: var(--color-text-1);
}

/* Review (post-SNOW-645): was a lighter rgba(255, 255, 255, …) background +
   a bigger box-shadow — the same bespoke, no-dark-counterpart hover
   treatment .region-readout-action's own comment describes, and the same
   fix: matched to the .map-utility-pill idiom (brightness filter, no
   background/box-shadow change) instead of inventing a parallel one. Also
   drops the now-nothing-to-transition `transition: box-shadow …, background
   …` this rule used to animate and the `prefers-reduced-motion` override
   that existed only to cancel it — a plain filter swap needs neither,
   matching every sibling .map-utility-pill--* hover exactly. */
/* SNOW-658: the hover treatment for every control in this file now comes
   from the one shared `.hover-affordance` class (src/css/main.css) that the
   markup carries — pointer cursor plus a translucent --color-chip-strong
   infill. Four treatments were removed to get there: `filter:
   brightness(1.04)` on the glass roundels (a ~4% lift on a frosted
   background, close to invisible), a hardcoded `rgba(0, 0, 0, 0.04)` on the
   layers-menu rows (no dark-mode counterpart), `--color-bg` on the
   bulletin-fill steps, and the panels' own `hover:bg-chip-strong`. Hugo:
   "The affordances are inconsistent... it should be consistent on hover -
   change the mouse pointer, and add infill". A new control here takes the
   class rather than growing a fifth. */

/* Review (post-SNOW-645, Hugo's dark-mode report): --ink/--muted-coloured
   TEXT and glyphs sitting directly on top of the glass surfaces just above
   and around here (.season-scrubber, .region-readout and its children,
   .region-readout-action, .map-download-control) — surfaces this same
   review moved onto --color-glass, which now genuinely goes dark under
   .dark. Before that fix their background was a hardcoded
   rgba(255, 255, 255, …), unaffected by theme, so --ink (a fixed
   near-black with no dark counterpart) painted on top of it was always
   legible; making the background theme-aware without also addressing the
   text it carries would have traded "wrong colour in dark mode" for
   "invisible in dark mode" — confirmed in a real browser (dark mode, port
   3000): the region-readout pill's own label and the download roundel's
   own idle glyph were both unreadable black-on-near-black before this
   block existed.

   Not a --ink/--muted dark variant (this file's own header explains why
   that is a bigger job than this ticket) — these are per-selector
   overrides onto src/css/main.css's ALREADY dark-mirrored --color-text-1 /
   --color-text-2 (the semantic equivalents: --color-text-1 for primary
   label text, --color-text-2 for secondary/muted), scoped to exactly the
   elements whose background this review made theme-aware and nothing
   else — every other --ink/--muted use in this file sits on a surface
   that is still deliberately theme-independent (the scrubber's own
   dark-filled play/pause buttons, the legend card, etc.) and is
   untouched.

   Placed HERE — before the state-specific rules below, not after them —
   on purpose: .map-download-control[data-download-state="error"] shares
   this same (.dark X = two classes) specificity with the plain
   .dark .map-download-control rule below, so which one wins a real tile
   is decided by source order, not specificity. Declaring the general dark
   override first lets the later, more specific-in-INTENT error rule keep
   winning exactly as it already does in light mode — the reverse order
   would have silently repainted an offline-and-uncached download's red
   error state in ordinary text colour under dark mode only. */
.dark .scrubber-skip,
.dark .region-readout,
.dark .region-readout-name,
.dark .region-readout-leaf,
.dark .region-readout-action,
.dark .map-download-control {
    color: var(--color-text-1);
}

.dark .season-scrubber-loading,
.dark .region-readout:not(.has-region) .region-readout-leaf,
.dark .region-readout-crumbs,
.dark .region-readout-action[aria-disabled="true"],
.dark .region-readout-action[aria-disabled="true"]:hover,
.dark .region-readout-action[aria-disabled="true"]:focus-visible,
.dark .map-download-control[data-download-state="no-region"],
.dark .map-download-control[data-download-state="disabled"],
.dark .map-download-control[data-download-state="offline"] {
    color: var(--color-text-2);
}

/* Three distinct reasons the control can't run, one shared treatment:
     no-region  — nothing selected, so there is no basemap to fetch.
     disabled   — over_ceiling, flagged server-side; never runnable.
     offline    — the device is offline (no downloading of layers while offline).

   These used to dim the whole roundel (opacity: 0.4). That worked when it sat
   against the opaque region-readout pill, but in the bottom-right stack it
   floats over open basemap, where a 40%-opacity glass circle is close to
   invisible — a control the user cannot see reads as a missing feature, not a
   disabled one. The shell now stays at full strength and only the glyph dims,
   so the affordance is legible and its unavailability is still obvious. */
.map-download-control[data-download-state="no-region"],
.map-download-control[data-download-state="disabled"],
.map-download-control[data-download-state="offline"] {
    color: var(--muted);
    cursor: not-allowed;
}

.map-download-control[data-download-state="no-region"] .map-download-control-icon,
.map-download-control[data-download-state="disabled"] .map-download-control-icon,
.map-download-control[data-download-state="offline"] .map-download-control-icon {
    opacity: 0.45;
}

/* 'idle' and (SNOW-568) 'error' are the clickable states — handleClick
   returns for every other one. 'busy' (already running) and 'done' (an
   informational success state) are not failures, so they keep their normal
   colour and take the neutral default cursor rather than the not-allowed
   above; without this the pointer contradicted the aria-disabled="true"
   map.js sets for them. */
.map-download-control[data-download-state="busy"],
.map-download-control[data-download-state="done"] {
    cursor: default;
}

/* SNOW-568: the run failed and the layer is NOT available offline — the
   same condition the layers menu paints with --color-sync-blocked, so the
   roundel and the cache dashboard keep saying the same thing in the same
   colour. Unlike the three unavailable states above it stays a pointer:
   clicking retries. */
.map-download-control[data-download-state="error"] {
    color: var(--color-sync-blocked);
    cursor: pointer;
}

/* Only the active state's icon renders; it sits above the busy fill. The
   busy state keeps the download glyph (static) and shows progress as a
   bottom-up fill of the roundel (below), not a numeric percentage.

   SNOW-569: 'done' keeps the download glyph too, white on the green disc,
   rather than swapping to a tick. The glyph is the control's identity —
   swapping it made a completed download read as a different control, and
   the colour inversion already carries "finished" on its own. Only 'error'
   changes the glyph, because that one genuinely is a different message. */
.map-download-control-icon {
    display: none;
    position: relative;
    z-index: 1;
}

/* SNOW-658: the custom-area roundel has no `data-download-state` at all any
   more (its idle/done meaning went — see its own template comment), so it
   matches none of the state-keyed rules below and its glyph would never be
   revealed. It has exactly one glyph and one appearance, so it is stated
   unconditionally rather than given a state to satisfy a display rule. */
#map-custom-download-control .map-download-control-icon--idle {
    display: block;
}

.map-download-control[data-download-state="idle"] .map-download-control-icon--idle,
.map-download-control[data-download-state="no-region"] .map-download-control-icon--idle,
.map-download-control[data-download-state="disabled"] .map-download-control-icon--idle,
.map-download-control[data-download-state="offline"] .map-download-control-icon--idle,
.map-download-control[data-download-state="busy"] .map-download-control-icon--idle,
.map-download-control[data-download-state="done"] .map-download-control-icon--idle,
.map-download-control[data-download-state="other-basemap"] .map-download-control-icon--idle,
.map-download-control[data-download-state="error"] .map-download-control-icon--error {
    display: block;
}

/* Busy/done: the roundel fills from the bottom with --color-sync-ok — the
   SAME green the layers-menu "available offline" sync dot uses — so the
   download speaks one visual language with the cache dashboard. During a
   run the fill rises to --download-progress (set by map.js on each
   warm-cache tick), replacing the numeric percentage; a completed download
   is a full green circle (below) with the download glyph on top in white,
   mirroring the green dot. Clipped to the circle by the host's
   overflow:hidden. */
.map-download-control[data-download-state="busy"]::before,
.map-download-control[data-download-state="done"]::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--download-progress, 0%);
    background: var(--color-sync-ok);
    z-index: 0;
    transition: height 220ms ease;
}

/* SNOW-645: once a region's basemap is known, ITS roundel fill takes that
   basemap's identity colour instead of the generic green — same tokens as
   .basemap-identity-fill in the Manage downloads sheet (src/css/main.css
   §2, generalised to also drive the budget bar's segments), so the
   surfaces agree. Scoped to #map-download-control's own id (SNOW-645
   review) — this is the REGION roundel only, not #map-custom-download-
   control (see that control's own monochrome override further down this
   file, and map_custom_download.js's _renderControl comment for why it
   does not carry a basemap identity colour). No data-basemap-key
   attribute (unresolved, or a record predating this change) keeps the
   plain --color-sync-ok green above — "downloaded, basemap unknown".

   Every rule below carries a var(…, var(--color-sync-ok)) FALLBACK, and it
   is load-bearing, not decorative. This file is NOT compiled by Tailwind —
   the --color-basemap-* custom properties only exist here because
   static/css/output.css (a gitignored build artefact, rebuilt by
   `npx @tailwindcss/cli`) defines them; src/css/main.css's own comment on
   its @theme block explains why consuming each token via var() there is
   what stops Tailwind tree-shaking it out. A checkout with a STALE
   output.css — the default state for anyone who hasn't just run that
   build — has the attribute selector but not the token: the custom
   property is undefined, the declaration is invalid at computed-value
   time, and a BARE var(--color-basemap-…) resolves `background` to its
   initial value, transparent — it does NOT fall through to the base
   ::before rule above, because this more-specific selector has already
   won the cascade. Without the fallback a stale build turns every
   downloaded region's solid green disc into a hollow ring, which reads as
   "my download was wiped" rather than "the CSS build is stale". */
#map-download-control[data-basemap-key="openfreemap_liberty"][data-download-state="busy"]::before,
#map-download-control[data-basemap-key="openfreemap_liberty"][data-download-state="done"]::before {
    background: var(--color-basemap-openfreemap-liberty, var(--color-sync-ok));
}
#map-download-control[data-basemap-key="swisstopo_winter"][data-download-state="busy"]::before,
#map-download-control[data-basemap-key="swisstopo_winter"][data-download-state="done"]::before {
    background: var(--color-basemap-swisstopo-winter, var(--color-sync-ok));
}
#map-download-control[data-basemap-key="swisstopo_light"][data-download-state="busy"]::before,
#map-download-control[data-basemap-key="swisstopo_light"][data-download-state="done"]::before {
    background: var(--color-basemap-swisstopo-light, var(--color-sync-ok));
}
#map-download-control[data-basemap-key="ign_plan"][data-download-state="busy"]::before,
#map-download-control[data-basemap-key="ign_plan"][data-download-state="done"]::before {
    background: var(--color-basemap-ign-plan, var(--color-sync-ok));
}
#map-download-control[data-basemap-key="basemap_at"][data-download-state="busy"]::before,
#map-download-control[data-basemap-key="basemap_at"][data-download-state="done"]::before {
    background: var(--color-basemap-basemap-at, var(--color-sync-ok));
}

/* Review (post-SNOW-645, Hugo's screenshot: every other roundel in the
   stack is glass chrome + a plain glyph; this one alone stood out as a
   solid disc of some colour in every theme tried — an ACTIVE basemap
   colour, then flat --ink, then --ink inverting per theme). Each attempt
   fixed the theme mismatch and re-created the "stands out" complaint in a
   new shape, because the real problem was never which colour to paint —
   it was painting one at all. #map-custom-download-control OPENS a panel;
   it does not need to also tell you what's inside it before you open it.
   SNOW-634 already moved "what have I downloaded" into the Manage
   downloads sheet itself (the row list, the swatches, the budget bar) —
   this roundel restating any part of that answer as a fill colour was
   always redundant with a surface that already answers it properly.

   Two `#map-custom-download-control[data-download-state="busy"|"done"]`
   rules used to sit here (a `content: none` cancelling the shared fill,
   and a `color: inherit` for the glyph on top of it). SNOW-658 deleted
   them along with the attribute they were keyed on: this roundel no longer
   has a download state to cancel the paint of. The paragraph above stays,
   because the reasoning it records — a panel opener does not describe its
   own contents — is what stops the fill coming back a fourth time. The
   shared busy/done fill below still paints a REAL status for
   #map-download-control, the region control; that one is unchanged. */

/* Done: the fill is pinned full — a solid disc matching the sync dot's
   colour language: green on the region roundel. The custom-area control
   briefly shared this fill mechanism under its own monochrome colour; it
   carries no download state at all now (SNOW-658), so this and the white
   glyph below are region-control-only in fact as well as in intent. */
.map-download-control[data-download-state="done"]::before {
    height: 100%;
}

/* White download glyph on the filled disc (the glyph strokes currentColor).
   Reads on the green fill above in both themes (see that fill's own
   dark-mode colour, src/css/main.css). Same icon as every other state —
   see the display rules above for why. */
.map-download-control[data-download-state="done"] .map-download-control-icon--idle {
    color: #fff;
}

/* SNOW-645: 'other-basemap' — the region IS downloaded, just for a
   DIFFERENT basemap than the one on screen (see map_region_download.js's
   module-header comment for the full story). Deliberately distinct from
   both neighbours it sits between: 'idle' paints nothing, 'done' pins a
   SOLID identity-coloured disc (the fill above, height: 100%); this is a
   HOLLOW centre with a solid RING in the identity colour — "downloaded,
   but not usable here" is a third answer, not a shade of the other two.
   The ring lives on its own ::before as an inset box-shadow rather than
   the height-based fill busy/done share above, so the two mechanisms
   never have to negotiate the same clipped area for one state.

   The glyph needs no override here: the base .map-download-control rule
   already sets color: var(--ink), and only 'done' (white, for contrast on
   its solid disc) and 'error' override it — a hollow ring has no solid
   fill to fight for contrast, so the base ink colour already reads
   correctly, unlike the white glyph 'done' needs.

   Same var(…, var(--color-sync-ok)) fallback form as the busy/done fill
   above, and for the identical reason — see that block's comment for why
   it is load-bearing rather than decorative. A keyless record (no
   data-basemap-key at all, not just a missing token) also needs to reach
   this default, which is what the bare (non-attribute-qualified) rule
   below is for.

   Review (Hugo's report, with a screenshot): "the roundel is bleeding the
   basemap colour at the top/bottom/left/right" — four coloured slivers at
   the cardinal points instead of a continuous ring. Root cause: this
   pseudo-element had no border-radius of its own, so `inset: 0` on a
   `border-radius: 999px` HOST produced a SQUARE box-shadow ring, relying
   on the host's `overflow: hidden` to clip it down to a circle. A circle
   inscribed in a square touches the square at exactly those four points —
   the "clip" was doing the shaping work the shadow's own geometry should
   have done, and a square clipped to a circle is still visibly square at
   the corners it never reaches. `border-radius: inherit` (999px, matching
   the host) makes the pseudo-element itself circular, so the ring is
   correct on its own geometry and does not depend on the host clip at
   all — do not "simplify" this back out on the reasoning that
   `overflow: hidden` already clips it; that reasoning is exactly what
   shipped the bug. Scoped to THIS rule only: the busy/done fill's own
   ::before (above) is deliberately a bottom-anchored RECTANGLE that DOES
   rely on the circular clip to read as a rising level inside a circle —
   giving that one a radius too would round the fill's own rising top
   edge and break the effect. */
.map-download-control[data-download-state="other-basemap"]::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    box-shadow: inset 0 0 0 2px var(--color-sync-ok);
    z-index: 0;
}
#map-download-control[data-basemap-key="openfreemap_liberty"][data-download-state="other-basemap"]::before {
    box-shadow: inset 0 0 0 2px var(--color-basemap-openfreemap-liberty, var(--color-sync-ok));
}
#map-download-control[data-basemap-key="swisstopo_winter"][data-download-state="other-basemap"]::before {
    box-shadow: inset 0 0 0 2px var(--color-basemap-swisstopo-winter, var(--color-sync-ok));
}
#map-download-control[data-basemap-key="swisstopo_light"][data-download-state="other-basemap"]::before {
    box-shadow: inset 0 0 0 2px var(--color-basemap-swisstopo-light, var(--color-sync-ok));
}
#map-download-control[data-basemap-key="ign_plan"][data-download-state="other-basemap"]::before {
    box-shadow: inset 0 0 0 2px var(--color-basemap-ign-plan, var(--color-sync-ok));
}
#map-download-control[data-basemap-key="basemap_at"][data-download-state="other-basemap"]::before {
    box-shadow: inset 0 0 0 2px var(--color-basemap-basemap-at, var(--color-sync-ok));
}

@media (prefers-reduced-motion: reduce) {
    .map-download-control[data-download-state="busy"]::before,
    .map-download-control[data-download-state="done"]::before {
        transition: none;
    }
}

/* The numeric percentage is replaced by the fill above; kept out of view. */
.map-download-control-percent {
    display: none;
}

/* SNOW-658: "the overlay this roundel's panel switches is on the map right
   now" — the one state shared by all three overlay roundels (downloads,
   favourites, field observations), painted from one attribute by one module
   (static/js/map_roundel_overlay_state.js). Keyed on the attribute alone,
   with no class in the selector, deliberately: the downloads roundel is a
   .map-download-control and the other two are .map-utility-buttons inside a
   pill, and a per-family rule is exactly how the three signals diverged
   before this ticket. A fourth roundel gets this by carrying the attribute.

   WHY A RING AND NOT A FILL. SNOW-645 removed a FILLED disc from
   #map-custom-download-control after three attempts at it (an active-basemap
   colour, then a flat neutral, then a theme-inverting neutral) all drew the
   same complaint: a solid disc among glass roundels reads as an error or an
   alert, not as a status. The lesson there was that the problem was painting
   a colour at all, not which colour — so this deliberately does not reach
   for a fill again. A 2px inset ring leaves the glass chrome and the glyph
   exactly as they are in the off state and changes only the rim, which is
   the same "third answer, not a shade of the other two" device the
   'other-basemap' state above already uses on the region roundel.

   The colour is --color-accent, and it is not a new choice: it is the ON
   colour of the switch that drives this state (the track in
   templates/includes/_switch.html is `peer-checked:bg-accent`). The control
   and its remote indicator say the same thing in the same colour, in both
   themes — main.css mirrors --color-accent under .dark.

   Drawn on ::after rather than the element's own box-shadow so it cannot
   contend with .map-download-control's drop shadow or with the ::before that
   busy/done/other-basemap already own. `border-radius: inherit` gives the
   pseudo-element its own circular geometry rather than leaning on a host
   clip — see the 'other-basemap' rule's own comment for the bug that
   reasoning shipped last time. If a stale static/css/output.css leaves
   --color-accent undefined the ring simply does not paint; the state is
   still in each roundel's aria-label, which is the accessible expression it
   carries for its own sake. */
[data-overlay-shown="true"] {
    position: relative;
}

[data-overlay-shown="true"]::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    box-shadow: inset 0 0 0 2px var(--color-accent);
    pointer-events: none;
    z-index: 1;
}

/* The max-width:640px rule that hid the major/minor download icons went with
   them. Only the micro control exists, it now lives in the bottom-right stack
   rather than beside the breadcrumb, and that stack is not breakpoint-dependent. */

/* Colour block leading the chip; the background is the EAWS danger colour for
   the scrubbed day, set inline by map.js (transparent when no_rating). */
.region-readout-swatch {
    /* Tracks the 13px chip text so the swatch keeps leading the label rather
       than floating in it. */
    width: 12px;
    height: 12px;
    flex: 0 0 auto;
    border-radius: 2px;
    background: transparent;
    box-shadow: inset 0 0 0 0.5px var(--border);
}

/* EAWS rating fills for the .scrubber-ribbon-cell elements that paint
   the season-scrubber track when a region is focused (SNOW-314).
   Use the same CSS custom properties as the legend swatches and the
   MapLibre choropleth fill layer. */
.ribbon-cell--no_rating   { background: rgba(0, 0, 0, 0.08); }
.ribbon-cell--low         { background: var(--color-eaws-low); }
.ribbon-cell--moderate    { background: var(--color-eaws-moderate); }
.ribbon-cell--considerable { background: var(--color-eaws-considerable); }
.ribbon-cell--high        { background: var(--color-eaws-high); }
.ribbon-cell--very_high   { background: var(--color-eaws-very-high); }

/* —————————————————————————————————————————————————————————
   Map-scoped z-index band (SNOW-486; extended SNOW-522)
   #home-intro, #map-help-overlay and #map-frame-overlay are positioned
   against other #map controls (utility cluster, scrubber, legend) and
   live inside #map's own stacking context, so they intentionally do NOT
   use the global --z-banner/--z-toast/--z-popup/--z-modal scale declared
   in src/css/main.css's @theme — lifting them out of #map would detach
   them from the controls they're anchored to. This 0-8 scale is local
   to #map only: 0-4 cover the choropleth/legend/utility-cluster layers
   above; 5 is #home-intro, 6 is the coachmark ring, 7 is its tooltip,
   8 is the custom-area download's framing overlay (mask + frame + CTA
   bar) — deliberately above the coachmark tour, since opening the
   framing overlay while the tour happens to be open should read as the
   framing overlay winning, not layering oddly beneath a highlight ring.
   ————————————————————————————————————————————————————————— */

/* —————————————————————————————————————————————————————————
   SNOW-314 — Homepage intro overlay (#home-intro)
   Absolutely positioned over #map; rendered with show_intro=True from
   the home() view. Dismissed via button or Escape key; restored by
   visiting /#about. home_intro.js owns the state machine.
   ————————————————————————————————————————————————————————— */

.home-intro {
    position: absolute;
    /* Offset from safe areas so the card clears the status bar / notch. */
    top: calc(env(safe-area-inset-top, 0px) + 64px);
    /* SNOW-645 review: `bottom` is new — this element used to set `top`
       ONLY, which leaves its height "auto" (intrinsic, sized to whatever
       .home-intro-card inside it needs). Setting `bottom` too gives it a
       DEFINITE height instead (this element's own containing block,
       #map, has one: #map is `flex: 1 1 auto` inside a `flex-col` body
       whose `min-height: 100dvh` makes #map's own height determinate in
       the standard case — the nav bar and any off-season banner above it
       are already excluded from #map's box by that flex layout, which a
       raw `100dvh` figure computed HERE could not know about). That
       definite height is what lets .home-intro-card's own `max-height:
       100%`, below, resolve against "the real room inside #map below the
       nav/banner chrome" rather than the full viewport including it —
       measured directly against a live page (nav + off-season banner
       both present) rather than assumed: a raw `100dvh`-minus-insets
       figure here was off by the nav+banner's own height, letting the
       card's computed max-height run past the bottom of the viewport.
       `--map-edge-inset-bottom` is the same bottom clearance every other
       floating map surface already respects. */
    bottom: var(--map-edge-inset-bottom);
    left: calc(env(safe-area-inset-left, 0px) + 16px);
    z-index: 5;
    /* Pointer events on the card only; the map behind the card stays interactive. */
    pointer-events: none;
}

.home-intro[hidden] {
    display: none;
}

.home-intro-card {
    position: relative;
    pointer-events: auto;
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border: 0.5px solid var(--color-border);
    box-shadow: var(--shadow-glass);
    border-radius: 12px;
    padding: 20px 22px;
    /* SNOW-535: widened from 320px — the two-paragraph copy this carries read
       as cramped at the old width. The card must also stay short enough to
       clear the bottom-left (i) legend toggle (tests/e2e/test_map_bottom_bar.py).

       The min() caps the card against the viewport so it never runs under the
       right-anchored utility cluster: at 412px the flat 380px card spanned
       x 16–396 while #basemap-toggle sits at x 360–400, so the card won the
       hit test and the toggle could not be clicked. 96px = the 16px left
       offset + the cluster's 44px column + 36px of breathing room. */
    max-width: min(380px, calc(100vw - 96px));
    color: var(--color-text-1);
    font-family: var(--sans);
    /* SNOW-645 review: bounded height, added — there was none before, so
       long copy (an off-season notice paragraph plus both taglines) grew
       DOWN past the bottom controls with nothing to stop it, the same
       defect class as the "Manage downloads" sheet's own unbounded
       growth (templates/includes/_overlay_sheet.html). `max-height: 100%`
       resolves against .home-intro's OWN height, which is now definite
       (top AND bottom both set — see that rule's own comment for why a
       plain `100dvh`-minus-insets figure computed HERE was wrong by the
       nav bar's own height, verified against a live page rather than
       assumed). The whole card scrolls as one unit when it overflows
       (`overflow-y: auto`) — the same "no internal pinned regions"
       default the favourites/report map sheets use, not the downloads
       sheet's pinned-header/footer treatment: this card has no such
       structure to split, and this fix is deliberately NOT the
       anchored-top-and-bottom redesign a future bulletin-panel rework
       may still do — see .home-intro's own comment for why a plain
       bound is now the right amount of fix here. The "×" close button
       (.home-intro-close, absolutely positioned WITHIN this card)
       scrolls with the content as a result — Escape and the primary CTA
       in .home-intro-actions (itself part of the scrolling content,
       reached by scrolling down) remain the way to dismiss without
       scrolling back up first. */
    max-height: 100%;
    overflow-y: auto;
}

.home-intro-identity {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
    /* Clears the "×" close button (top-right, 28px) so a short wordmark row
       never sits underneath it. */
    padding-right: 28px;
}

.home-intro-wordmark {
    font-size: 18px;
    font-weight: 600;
    letter-spacing: -0.02em;
    color: var(--color-text-1);
}

.home-intro-taglines {
    display: flex;
    flex-direction: column;
}

.home-intro-tagline {
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-text-2);
    margin: 0 0 12px;
}

/* The taglines stack in one column at every viewport. SNOW-535 laid them out
   side by side above 960px (in a card widened to 640px) to hold the card's
   height down; that traded height for a 640px measure, which is too long a
   line for 13px body text. The copy is short enough to stack instead. */

/* SNOW-445: the persistent off-season archive notice is now a full-width bar
   above the map (#map-offseason-bar, styled with Tailwind status tokens in
   _map_embed.html), so the old bottom-left .map-offseason-note chip and its
   styles were removed. */

/* Off-season reference line inside the intro card — rendered only when
   is_offseason is True. The full-width #map-offseason-bar above the map is
   the persistent counterpart that survives #home-intro dismissal. */
.home-intro-offseason-ref {
    font-size: 11px;
    line-height: 1.4;
    color: var(--color-text-3);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    margin: 0 0 12px;
    padding: 6px 8px;
    background: rgba(0, 0, 0, 0.04);
    border-radius: 6px;
}

/* Inline link inside a tagline paragraph (the "Register" call-out). Underlined
   rather than colour-only so it reads as a link against the muted body text. */
.home-intro-inline-link {
    color: var(--color-text-1);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.home-intro-inline-link:hover {
    text-decoration-thickness: 2px;
}

.home-intro-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.home-intro-dismiss {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 8px 16px;
    border-radius: 999px;
    background: var(--ink);
    color: var(--on-ink);
    font-size: 13px;
    font-weight: 500;
    border: none;
    cursor: pointer;
    font-family: var(--sans);
    transition: opacity 120ms ease;
}

.home-intro-dismiss:hover {
    opacity: 0.85;
}

.home-intro--no-motion .home-intro-dismiss {
    transition: none;
}

/* SNOW-535: "×" close, top-right — mirrors .map-help-close below (SNOW-457),
   the same corner-dismiss idiom applied to this card. */
.home-intro-close {
    position: absolute;
    top: 8px;
    right: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: 999px;
    background: transparent;
    color: var(--color-text-3);
    cursor: pointer;
    transition: color 120ms ease, background 120ms ease;
}

.home-intro-close:hover {
    color: var(--color-text-1);
    background: rgba(0, 0, 0, 0.06);
}

.home-intro--no-motion .home-intro-close {
    transition: none;
}

.home-intro-close:focus-visible {
    outline: 2px solid var(--color-text-1);
    outline-offset: 2px;
}

/* Dark mode: the glass card uses the dark glass token automatically via
   the .dark body class set by the theme head partial. */
.dark .home-intro-card {
    background: var(--color-glass);
    color: var(--color-text-1);
}

/* —————————————————————————————————————————————————————————
   SNOW-457 — Map help / coachmark tour (#map-help-overlay)
   A first-run walkthrough of the map's controls: a highlight ring plus a
   tooltip card that steps through each control in turn. static/js/map_help.js
   positions the ring/tooltip against getBoundingClientRect() of the current
   target in viewport coordinates, so both use position: fixed rather than
   tracking #map's own offset parent. No backdrop dimming — a passive ring,
   not a spotlight, so the map stays fully visible and interactive underneath.
   No .dark override needed: --color-glass/--color-text-* already resolve to
   the dark-mode values via the .dark body class (see main.css @theme).
   ————————————————————————————————————————————————————————— */

.map-utility-pill--help {
    width: var(--map-control-lg);
    flex: 0 0 auto;
    justify-content: center;
}


#map-help-overlay {
    position: fixed;
    inset: 0;
    z-index: 6;
    /* The overlay itself never intercepts pointer events — only the
       tooltip card (below) opts back in for its buttons. */
    pointer-events: none;
}

#map-help-overlay[hidden] {
    display: none;
}

@keyframes map-help-ring-pulse {
    0%, 100% { box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.22); }
    50%      { box-shadow: 0 0 0 7px rgba(0, 0, 0, 0.10); }
}

#map-help-ring {
    position: fixed;
    border: 1.5px solid var(--ink);
    box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.22);
    pointer-events: none;
    animation: map-help-ring-pulse 1.8s ease-in-out infinite;
    transition:
        top 160ms ease,
        left 160ms ease,
        width 160ms ease,
        height 160ms ease,
        border-radius 160ms ease;
}

.map-help--no-motion #map-help-ring {
    animation: none;
    transition: none;
}

.map-help-tooltip {
    position: fixed;
    z-index: 7;
    pointer-events: auto;
    max-width: 300px;
    background: var(--color-glass);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border: 0.5px solid var(--color-border);
    box-shadow: var(--shadow-glass);
    border-radius: var(--radius-card, 12px);
    padding: 16px 18px;
    color: var(--color-text-1);
    font-family: var(--sans);
    transition: top 160ms ease, left 160ms ease;
    /* SNOW-645 review: bounded height, added — this card is JS-positioned
       per coachmark step (map_help.js computes `top`/`left` at runtime,
       hence no fixed anchor to measure a precise remaining-room figure
       from, unlike .home-intro-card / .map-legend-card above), so the
       general viewport-minus-edge-insets cap every floating map surface
       now uses is the right level of precision here: a longer step's
       copy scrolls within the card rather than growing past whichever
       edge JS happened to place it near. `dvh`, not `vh` — see
       .home-intro-card's own comment for why. */
    max-height: calc(100dvh - var(--map-edge-inset-top) - var(--map-edge-inset-bottom));
    overflow-y: auto;
}

.map-help--no-motion .map-help-tooltip {
    transition: none;
}

.map-help-tooltip:focus {
    outline: none;
}

/* SNOW-457: "×" close button, top-right of the tooltip card. The card is
   position: fixed, so this absolute child anchors to its corner. The step
   label and title reserve right padding so their text never runs under it. */
.map-help-close {
    position: absolute;
    top: 8px;
    right: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: 999px;
    background: transparent;
    color: var(--color-text-3);
    cursor: pointer;
    transition: color 120ms ease, background 120ms ease;
}

.map-help-close:hover {
    color: var(--color-text-1);
    background: rgba(0, 0, 0, 0.06);
}

.map-help--no-motion .map-help-close {
    transition: none;
}

.map-help-close:focus-visible {
    outline: 2px solid var(--color-text-1);
    outline-offset: 2px;
}

.map-help-tooltip-step {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--color-text-3);
    margin: 0 0 6px;
    padding-right: 28px;
}

.map-help-tooltip-title {
    font-size: 15px;
    font-weight: 600;
    margin: 0 0 6px;
    padding-right: 28px;
    color: var(--color-text-1);
}

.map-help-tooltip-body {
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-text-2);
    margin: 0 0 14px;
}

/* SNOW-457: Back + Next sit right-aligned. "Skip" moved out to the "×"
   close button, so the row no longer needs space-between. */
.map-help-tooltip-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
}

.map-help-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 6px 14px;
    border-radius: 999px;
    border: none;
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    font-family: var(--sans);
    transition: opacity 120ms ease;
}

.map-help--no-motion .map-help-btn {
    transition: none;
}

/* Primary (Next / Done) reuses the same dark-pill look as .home-intro-dismiss. */
.map-help-btn--primary {
    background: var(--ink);
    color: var(--on-ink);
}

.map-help-btn--primary:hover {
    opacity: 0.85;
}

.map-help-btn--text {
    background: transparent;
    color: var(--color-text-2);
    padding: 6px 10px;
}

.map-help-btn--text:hover {
    color: var(--color-text-1);
}

.map-help-btn:disabled {
    opacity: 0.35;
    cursor: default;
    pointer-events: none;
}

.map-help-btn:focus-visible {
    outline: 2px solid var(--color-text-1);
    outline-offset: 2px;
}

/* —————————————————————————————————————————————————————————
   SNOW-522 — Custom-area basemap download framing overlay
   (#map-frame-overlay, public/partials/_map_embed.html)
   A Google-Maps-style dim mask with a fixed, centred frame: the user
   pans/zooms the map underneath it, static/js/map.js's
   mapCustomDownloadControlInit recomputes the "up to N MB" readout once
   per animation frame. z-index 8 on the map-scoped band above (the
   comment block above .home-intro further up this file) — deliberately
   above the coachmark tour (6/7).
   ————————————————————————————————————————————————————————— */

#map-frame-overlay {
    /* Absolute, not fixed: the overlay is a child of #map (position:
       relative, overflow: hidden), so this scopes the frame, the dim mask
       and the CTA sheet to the map itself. Fixed positioning measured
       against the viewport instead, which pushed the cutout up over the
       site header and the season banner — lighting page chrome that has
       nothing to do with the framed area — and left the season scrubber
       lit INSIDE the frame while the CTA sheet sat below the map entirely.
       #map's overflow: hidden also clips the 9999px mask spread to the map,
       so the header and footer stay undimmed. */
    position: absolute;
    inset: 0;
    z-index: 8;
    /* A column: the frame takes every pixel the CTA sheet doesn't, so the
       two are physically adjacent rather than two objects floating apart.
       The sheet used to be position:fixed to the viewport bottom while the
       frame was centred in the whole viewport, which left a band of dimmed
       map between them and made the readout read as unrelated chrome. */
    display: flex;
    flex-direction: column;
    /* The overlay itself never intercepts pointer events — panning/
       zooming the map underneath must never be blocked by it. Only
       .map-frame-cta (below) opts back in for its own buttons. */
    pointer-events: none;
}

#map-frame-overlay[hidden] {
    display: none;
}

/* The frame: fills the column above the CTA sheet, inset just enough to
   read as a frame rather than a viewport edge. Deliberately large — the
   point of framing is to choose an area, and a small rectangle both hides
   the surroundings you are choosing between and makes the "up to N MB"
   number feel arbitrary. Sized entirely in CSS so static/js/map.js never
   hardcodes dimensions — it reads getBoundingClientRect(). */
/* The box the frame may occupy. Owns the gutter and centres the frame
   inside it — an even 48px on every side; tighter than that and the frame
   stops reading as a frame and reads as the map's own edge.

   The frame is a separate element because it does not always fill this
   box: once the framed ground area would exceed DOWNLOAD_CEILING_MB,
   map.js caps the frame's pixel size so its footprint sticks at the
   maximum (see _fitFrameToCeiling). Ground area per pixel quadruples with
   every zoom level out, so the capped frame halves in size per level —
   it visibly shrinks as you zoom out, rather than the readout simply
   going red. */
.map-frame-area {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 48px;
    min-height: 0;
    pointer-events: none;
}

.map-frame-rect {
    /* Default: fill the area. Once the ceiling bites, map.js overrides
       width/height inline and offsets the box with an inline `transform`,
       clearing all three again when it does not (SNOW-567). The offset is
       a transform rather than a position change for two reasons: the box
       stays flex-centred here, so the offset is measured from a stable
       origin; and moving it costs no layout, which matters because the dim
       mask below is this element's own box-shadow — relaying it out would
       repaint a 9999px spread on every frame of a zoom. */
    width: 100%;
    height: 100%;
    border: 2px solid var(--on-ink);
    border-radius: var(--radius-card, 12px);
    /* The dim mask: a single box-shadow spread 9999px in every direction
       darkens everything OUTSIDE this element's own box — no separate
       full-screen mask element, no clip-path. Paints correctly at any
       viewport size with nothing to recompute on resize. */
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
    pointer-events: none;
}

/* Below ~480px a fixed 96px of horizontal gutter eats most of the screen,
   so scale it back proportionally — the frame stays inset but remains big
   enough to aim with on a phone. */
@media (max-width: 480px) {
    .map-frame-area {
        padding: 32px 24px;
    }
}

@media (min-width: 640px) {
    .map-frame-area {
        /* Cap the frame's width on a large viewport so it never becomes an
           unusably wide letterbox on an ultrawide monitor; the 48px base
           gutter still applies below that threshold. */
        padding-left: max(48px, calc((100vw - 1100px) / 2));
        padding-right: max(48px, calc((100vw - 1100px) / 2));
    }
}

/* Instruction sheet: says what the frame is FOR. Deliberately the visual
   twin of .map-frame-cta below — same surface, same border, same padding
   rhythm — so the pair brackets the frame: the instruction at the top, the
   cost and the actions at the bottom. Framing strips the map furniture, so
   without this the user is handed a dimmed map and a rectangle with
   nothing naming the task. Not interactive, so it does NOT opt back into
   pointer events: a drag started on the instruction bar still pans the map
   underneath. */
.map-frame-instruction {
    /* Lifts the bar above .map-frame-rect's dim mask. The mask is that
       element's own box-shadow, so it paints over every sibling EARLIER in
       DOM order — and this bar precedes the frame. Without a stacking
       context the instruction rendered visibly greyed while the CTA sheet
       (which follows the frame, so paints after it) sat at full strength,
       making a matched pair look like two different surfaces. */
    position: relative;
    z-index: 1;
    padding: 14px 20px;
    background: var(--color-card);
    border-bottom: 0.5px solid var(--border);
    box-shadow: var(--shadow-glass);
    color: var(--color-text-1);
    font-family: var(--sans);
    font-size: 15px;
    font-weight: 500;
    text-align: center;
}

/* CTA sheet: the live size readout plus Cancel/Download, full-bleed along
   the foot of the viewport in normal flow (the last child of the column
   above) so it sits flush under the frame — the Google Maps reference
   shape. Full-bleed rather than inset-with-radius because it deliberately
   covers the season scrubber and footer beneath it: neither the timeline
   nor the site chrome has any bearing on the area being downloaded, and
   leaving them visible around a floating card was the "disconnected"
   read. */
.map-frame-cta {
    /* Same lift as .map-frame-instruction above. This sheet FOLLOWS the
       frame in DOM order, so it already painted above the mask by source
       order alone — stated explicitly so the pair cannot drift apart if
       the markup is ever reordered. */
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    padding: 14px 20px calc(env(safe-area-inset-bottom, 0px) + 14px);
    background: var(--color-card);
    border-top: 0.5px solid var(--border);
    box-shadow: var(--shadow-glass);
    color: var(--color-text-1);
    font-family: var(--sans);
    /* Re-enables pointer events within the mask — this sheet (and only
       this sheet) is interactive. */
    pointer-events: auto;
}

.map-frame-readout {
    font-size: 15px;
    font-weight: 500;
}

/* Furniture stripped while framing (static/js/map.js's openFraming adds
   the class). Framing is a modal act with exactly two answers — Download
   or Cancel, both on the CTA sheet — so every control that steers the map
   for some OTHER purpose is noise, and worse, each one sits lit inside the
   cutout looking like part of the area being chosen.

   What goes, and why none of it is load-bearing here:
     #season-ribbon      names a region the custom area does not follow
     #map-date-ribbon    describes a day; a download has no date
     #map-utility-cluster search/locate — navigating away mid-frame
     #map-controls-br    layers/favourite/report/help, incl. this very
                         control (the sheet carries Cancel, so there is no
                         way to get stuck with it hidden)
     #map-legend         a key for the choropleth, which is dimmed anyway

   The season scrubber needs no rule: the full-bleed CTA sheet covers it.
   Pan and zoom are untouched — they are the whole interaction. */
body.map-framing #season-ribbon,
body.map-framing #map-date-ribbon,
body.map-framing #map-utility-cluster,
body.map-framing #map-controls-br,
body.map-framing #map-legend {
    display: none;
}

/* SNOW-522: painted when the framed area exceeds DOWNLOAD_CEILING_MB — the
   same "too large" backstop the per-region control's disabled state
   carries, mirrored here since a custom area's size is only known once
   framed. static/js/map.js toggles this class alongside disabling
   #map-frame-confirm. */
.map-frame-readout--over-ceiling {
    color: var(--color-status-error-text);
}

.map-frame-cta-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* Reuses .map-help-btn's pill shape/weight (immediately above) rather than
   a third near-identical button ruleset — renamed only because it lives in
   a different overlay and .map-help-btn's own selectors (.map-help--no-motion
   scoping) don't apply here. */
.map-frame-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 8px 16px;
    border-radius: 999px;
    border: none;
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    font-family: var(--sans);
    transition: opacity 120ms ease;
}

.map-frame-btn--primary {
    background: var(--ink);
    color: var(--on-ink);
}

.map-frame-btn--primary:hover {
    opacity: 0.85;
}

.map-frame-btn--primary:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.map-frame-btn--text {
    background: transparent;
    color: var(--color-text-2);
}

.map-frame-btn--text:hover {
    color: var(--color-text-1);
}

.map-frame-btn:focus-visible {
    outline: 2px solid var(--color-text-1);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .map-frame-btn {
        transition: none;
    }
}

/* SNOW-419: community-reports popup — a small, DOM-built card shown on
 * tapping an unclustered community-report pin (static/js/map.js). Reuses
 * the same frosted-glass chrome as .region-popup (--color-popup-glass,
 * src/css/main.css @theme) for visual consistency with the region
 * tooltip, but has no data-level border colour since a report has no
 * danger rating. */
.community-report-popup-wrapper .maplibregl-popup-content {
    /* Extra right padding reserves room for the absolutely-positioned close
     * button so the type label never runs under it. */
    padding: 10px 34px 10px 14px;
    border-radius: 12px;
    background: var(--color-popup-glass);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    border: 2px solid var(--color-border, rgba(0, 0, 0, 0.12));
    box-shadow: var(--shadow-glass, 0 4px 16px rgba(0, 0, 0, 0.12));
    color: var(--color-text-1, #1a1a1a);
    font-family: var(--sans);
    min-width: 0;
}

/* Close button: mirror .region-popup's treatment — a small, muted glyph
 * pinned to the top-right, vertically centred against the type label rather
 * than floating in the corner (SNOW-472). */
.community-report-popup-wrapper .maplibregl-popup-close-button {
    top: 6px;
    right: 6px;
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-size: 16px;
    line-height: 1;
    border-radius: 6px;
    color: var(--color-text-2, #5f5e5a);
}

.community-report-popup-wrapper .maplibregl-popup-close-button:hover {
    background: rgba(0, 0, 0, 0.06);
    color: var(--color-text-1, #1a1a1a);
}

.community-report-popup__type {
    font-weight: 600;
    font-size: 14px;
    line-height: 1.25;
}

.community-report-popup__meta {
    margin-top: 2px;
    font-size: 12px;
    color: var(--color-text-2, #5f5e5a);
}

/* SNOW-499: resort- and existing-favourite-detail popups are anchored to a
 * point fixed on the map (a resort pin / a saved favourite), so they share
 * the same frosted-glass MapLibre-popup chrome as .region-popup and
 * .community-report-popup-wrapper — one consistent popup family, rather than
 * MapLibre's default white box. No data-level border (neither has a danger
 * rating). Mirrors the community-report treatment (the simpler sibling). */
.detail-popup .maplibregl-popup-content {
    /* Right padding reserves room for the absolutely-positioned close button
     * so content never runs under it. */
    padding: 12px 34px 12px 14px;
    border-radius: 12px;
    background: var(--color-popup-glass);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    border: 2px solid var(--color-border, rgba(0, 0, 0, 0.12));
    box-shadow: var(--shadow-glass, 0 4px 16px rgba(0, 0, 0, 0.12));
    color: var(--color-text-1, #1a1a1a);
    font-family: var(--sans);
    min-width: 200px;
}

.detail-popup .maplibregl-popup-close-button {
    top: 6px;
    right: 6px;
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-size: 16px;
    line-height: 1;
    border-radius: 6px;
    color: var(--color-text-2, #5f5e5a);
}

.detail-popup .maplibregl-popup-close-button:hover {
    background: rgba(0, 0, 0, 0.06);
    color: var(--color-text-1, #1a1a1a);
}

/* SNOW-658: two rules lived here to flatten the client-built rename/delete
 * row (and the _favourite.html a rename swapped in) against the glass, and
 * to underline its editable name field. The favourite popup carries neither
 * now — renaming and removing moved to the favourites panel, and what is
 * left is a name plus a saved time, styled by the same token utilities as
 * the observation popup's own two lines.
 *
 * The 200px floor above was sized for that rename field and its Remove
 * button, and the resort popup still needs it — but a two-line favourite
 * card in a box a third wider than the observation card it now mirrors
 * reads as a different component. Keyed off the card's own attribute
 * (favourites.js buildDetailCard) rather than a popup-level modifier class,
 * so map.js's shared mountDetailPopup keeps one className for both. */
.detail-popup .maplibregl-popup-content:has([data-favourite-card]) {
    min-width: 0;
    padding: 10px 34px 10px 14px;
}
