/*
 * 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;
}

.map-controls-toggle:hover {
    filter: brightness(1.04);
}

/* 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;
}

.map-utility-pill--basemap:hover {
    filter: brightness(1.04);
}

/* 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;
}

.map-utility-pill--locate:hover {
    filter: brightness(1.04);
}

/* 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. */
    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:hover {
    background: rgba(0, 0, 0, 0.04);
}

.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;
}

/* 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;
}

.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. The warning-triangle icon is tinted amber (status-warning)
   to stand out as the report-a-hazard action; the SVG strokes inherit it via
   currentColor. */
.map-report-control {
    width: var(--map-control-lg);
    flex: 0 0 auto;
    justify-content: center;
    color: var(--color-status-warning-text);
}

.map-report-control:hover {
    filter: brightness(1.04);
}

/* 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;
}

.map-utility-pill--favourite:hover {
    filter: brightness(1.04);
}

/* 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;
}

/* ``.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;
}

.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);
    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. */

.season-scrubber,
.season-scrubber-demo {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 16px;
    background: rgba(255, 255, 255, 0.75);
    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. */
.region-readout[hidden] {
    display: none;
}

.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.

       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: rgba(255, 255, 255, 0.75);
    -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. Hidden by default; shown only when a region
   is focused (the sibling rule below), so there is always a bulletin to open. */
.region-readout-action {
    display: none;
    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);
    background: rgba(255, 255, 255, 0.75);
    -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;
    transition: box-shadow 120ms ease, background 120ms ease;
}

#region-readout.has-region ~ .region-readout-action {
    display: inline-flex;
}

.region-readout-action:hover,
.region-readout-action:focus-visible {
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 0 3px 16px rgba(0, 0, 0, 0.16);
}

@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 {
    /* Hidden by default (the sibling rule below reveals it once a region is
       focused); every other visual property still applies once shown, so
       they stay in this same ruleset rather than a separate one keyed off
       the sibling selector. */
    display: none;
    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);
    background: rgba(255, 255, 255, 0.75);
    -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;
    transition: box-shadow 120ms ease, background 120ms ease;
}

/* Hidden until a region is focused — same idiom as .region-readout-action
   immediately above it in the DOM (both are direct siblings of
   #region-readout inside .ribbon-header). */
#region-readout.has-region ~ .map-download-control {
    display: inline-flex;
}

/* SNOW-522: the custom-area download control shares .map-download-control's
   glass-roundel chrome (colour states, busy fill, done tick) but, unlike its
   per-region sibling above, is NOT hidden-until-focused — it lives in the
   bottom-right stack with no dependency on a focused region, so overrides
   the shared display: none default back to always-shown. */
.map-custom-download-control {
    display: inline-flex;
}

.map-download-control:hover,
.map-download-control:focus-visible {
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 0 3px 16px rgba(0, 0, 0, 0.16);
}

/* 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;
}

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

/* 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;
}

.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="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;
}

/* Done: the fill is pinned full — a solid green circle matching the dot. */
.map-download-control[data-download-state="done"]::before {
    height: 100%;
}

/* White download glyph on the green fill (the glyph strokes currentColor).
   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;
}

@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;
}

/* 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);
    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);
}

.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-utility-pill--help:hover {
    filter: brightness(1.04);
}

#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;
}

.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);
}

/* The existing-favourite popup hosts the client-built rename/delete row
 * (favourites.js buildDetailRow, and the _favourite.html the rename swaps
 * back in) — both carry their own card border/background for the manage-page
 * list. Inside the glass popup that reads as a box-in-box, so flatten it to
 * sit flush on the glass, matching the resort/region/observation content. */
.detail-popup [data-favourite-uuid] {
    border: 0;
    background: transparent;
    padding: 0;
}

/* Keep the editable name legible as a field on the glass — a subtle underline
 * signals "editable" without the full card border. */
.detail-popup [data-favourite-uuid] input[name="name"] {
    border-bottom: 1px solid var(--color-border, rgba(0, 0, 0, 0.18));
    padding-bottom: 2px;
}
