/* trailmaps.app Map Generator — Styles
 *
 * Mobile-first (one-thumb operation, 44 px tap targets, safe-area
 * insets) with desktop layered on top.
 *
 * Light + dark theme. The chrome (sheet bg / text / dividers / etc.)
 * uses CSS custom properties on :root for light defaults; an
 * [data-color-scheme="dark"] selector overrides the same tokens for
 * dark mode. The data-color-scheme attribute is set on <html> by an
 * inline bootstrap script in <head> BEFORE this stylesheet loads, so
 * first paint already has the right scheme — no FOUC.
 *
 * Per-map accent colour (--accent) survives both schemes — it's
 * branding, not a theme concern.
 *
 * On-map elements that need per-scheme tweaking (trail labels,
 * arrows, marker shadow) live in their respective sections with a
 * matching [data-color-scheme="dark"] selector.
 */

:root {
    --sheet-bg: rgba(255, 255, 255, 0.98);
    --sheet-text: #222;
    --sheet-text-muted: #666;
    --sheet-border: #d8d8d8;
    --sheet-shadow: rgba(0, 0, 0, 0.18);
    --sheet-hover: rgba(0, 0, 0, 0.05);
    --sheet-divider: #ececec;
    /* Surface tokens for elements that need a subtly-different bg
     * from the main sheet — input fields (white inset against the
     * sheet), segmented pills + filter chips (slightly darker for
     * visual containment), section headers (faint tint to mark the
     * group). All flip in dark mode via the [data-color-scheme]
     * override below. */
    --input-bg: white;
    --pill-bg: #e3e3e3;
    --section-header-bg: #fafafa;
    /* html/body background — only visible in the slivers around the
     * map canvas (notch / gesture-nav safe-area insets, scrollbar
     * gutters). Light scheme = the historical #f7f7f7 page tone.
     * Flips to a dark slate in dark mode (see [data-color-scheme]
     * override) so that on devices like the Pixel 8 PWA — where
     * safe-area-inset-right is non-zero and the map's `width: 100%`
     * doesn't extend into the inset — the strip doesn't show as a
     * bright white line against the otherwise-dark UI. */
    --page-bg: #f7f7f7;
    /* Popup body background — same as sheet in light mode (no
     * change). In dark mode, overridden to a slightly lighter
     * shade than --sheet-bg so popups read as a raised surface
     * against the dark basemap (which has roughly the same
     * luminance as --sheet-bg, causing them to blend without
     * either an outline or an elevation cue). */
    --popup-bg: var(--sheet-bg);
    /* Faint hairline outline for layer-swatch chips in Options.
     * Light scheme: dark line on a white-ish sheet. Dark scheme:
     * light line on a dark sheet. Both hover at low opacity so the
     * chip's filled colour does the heavy lifting and the hairline
     * just provides a "this is a chip" edge. */
    --swatch-outline: rgba(0, 0, 0, 0.08);
    --accent: #2980b9;
    /* Derived from --accent so a per-map override (set on :root by
     * JS from CONFIG.accentColor) cascades automatically — the
     * runtime only has to set --accent and link colour + the darker
     * pressed/hover variant follow. color-mix(in srgb, X 75%, black)
     * darkens X by 25%, matching the historical relationship between
     * #2980b9 and #1f6391. Browser support for color-mix() is
     * universal in evergreen browsers as of 2023. */
    --link-color: var(--accent);
    --accent-strong: color-mix(in srgb, var(--accent) 75%, black);
    --emergency-color: #c0392b;
    --highlight-amber: #ffb700;
    --safe-bottom: env(safe-area-inset-bottom, 0px);
    --safe-top: env(safe-area-inset-top, 0px);

    /* POI marker design tokens — shared between Options-overlay
     * swatches AND on-map markers so each Options chip is a true
     * miniature of the marker it represents. Tweaking any of these
     * updates both surfaces in lockstep.
     *
     * Trailheads use a wider chip because "TH" is two characters;
     * trail markers use a smaller, variable-width chip because the
     * ref ("23", "EAP-1") is hung off the existing OSM tag and
     * varies wildly in length. Everything else is the standard
     * square. */
    --poi-marker-size: 24px;
    --poi-marker-size-trailhead-w: 28px;
    --poi-marker-size-trail-marker-h: 20px;
    --poi-marker-size-trail-marker-min-w: 20px;
    --poi-marker-radius: 4px;
    --poi-marker-border: 2px;
    --poi-marker-font-text: 13px;          /* P (parking) */
    --poi-marker-font-text-tight: 11px;    /* TH, trail-marker refs */
    --poi-marker-svg-size: 14px;           /* toilet, water glyphs */
    /* Drop-shadow for on-map markers ONLY — gives them apparent
     * depth above the basemap. Options swatches use a different
     * shadow (a 1px dark outline for contrast against the sheet)
     * so this token isn't shared with .layer-swatch. */
    --poi-marker-map-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
    /* Feature-marker label (DOM text below the dot, painted directly
     * on the basemap). Mirrors MAP_PAINT_TOKENS for trail labels:
     * dark text + white halo on light basemap, light text + dark
     * halo on dark basemap. */
    --feature-label-color: #1e1e1e;
    --feature-label-halo: rgba(255, 255, 255, 0.9);
}

/* Dark-mode token overrides. Selector targets <html data-color-scheme="dark">,
 * which is set by the inline bootstrap script in <head> from the rider's
 * LS preference / curator default / OS prefers-color-scheme.
 *
 * Variables not listed here keep their light-theme values — that's how the
 * accent colour, marker design tokens (size/border/radius), and POI
 * brand colours all survive both schemes.
 */
[data-color-scheme="dark"] {
    --sheet-bg: rgba(28, 28, 30, 0.98);
    --sheet-text: #f0f0f0;
    --sheet-text-muted: #a0a0a0;
    --sheet-border: #3c3c3e;
    --sheet-shadow: rgba(0, 0, 0, 0.6);
    --sheet-hover: rgba(255, 255, 255, 0.08);
    --sheet-divider: #2c2c2e;
    /* Inverted surface tokens — see :root for the light defaults.
     * Inputs match the sheet body (just contained by their border);
     * pills sit slightly LIGHTER than the dark sheet so the active-
     * state accent fill still has a neutral resting bg to overlay;
     * section headers a touch lighter than the sheet for grouping. */
    --input-bg: #1c1c1e;
    --pill-bg: #3a3a3c;
    --section-header-bg: #2c2c2e;
    /* Dark page bg — matches the dark basemap's background tone so
     * any sliver of body showing through (safe-area insets, etc.)
     * blends into the rest of the dark UI rather than flashing
     * white. */
    --page-bg: #1c1c1e;
    /* Popup raised-surface override — Material elevation pattern. */
    --popup-bg: #2c2c2e;
    /* Light hairline against the dark sheet so chips have a visible
     * edge in dark mode (the dark-line variant from the light scheme
     * disappears against the dark sheet). */
    --swatch-outline: rgba(255, 255, 255, 0.18);

    /* On-map POI markers need a stronger shadow against dark
     * basemap tiles — drop shadow alone fades into the dark map.
     * Add a faint inner-edge ring (the second shadow) so the chip's
     * white border has some structural contrast against the basemap. */
    --poi-marker-map-shadow: 0 1px 4px rgba(0, 0, 0, 0.5),
                              0 0 0 1px rgba(255, 255, 255, 0.1);
    --feature-label-color: #f0f0f0;
    --feature-label-halo: rgba(0, 0, 0, 0.7);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* Honour the user's OS-level "Reduce motion" preference (WCAG 2.1
 * SC 2.3.3 — Animation from Interactions). When set, drop animations
 * to near-zero so vestibular-sensitive users don't get vertigo from
 * the locate-button spinner, sheet drag/snap, toast fades, or
 * highlight-chip transitions. Setting to 0.01ms (rather than 0)
 * keeps the timing-aware JS that reads transitionend events working.
 *
 * The map itself (basemap pan/zoom under the user's finger) is not
 * an animation in this sense and isn't covered by the spec; that
 * stays responsive as before. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

html,
body {
    overflow: hidden;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    color: var(--sheet-text);
    background: var(--page-bg);
    /* Suppress the Chrome/Safari mobile default tap-highlight (a
     * brief translucent gray rectangle painted over the tapped
     * element). Our UI controls communicate tap feedback through
     * aria-pressed state changes, sheet open/close animations, and
     * the highlight chip — the gray flash is redundant. Inherits
     * to all children. Override on specific content links if needed
     * (none currently). */
    -webkit-tap-highlight-color: transparent;
}

#map {
    /* Pin all four edges instead of width/height: 100% so the map
     * canvas fills the viewport precisely without leaving a sub-
     * pixel sliver on any side. Earlier `width: 100%; height: 100%`
     * showed a thin (1-2 px) bright strip on the right edge of the
     * Pixel 8 PWA, visible as light grey in light mode and stark
     * white-on-dark in dark mode (because html/body's --page-bg
     * shows through). Using `right: 0; bottom: 0` makes the map
     * size itself from the actual viewport rectangle each frame,
     * matching MapLibre's resize observer behaviour without depending
     * on devicePixelRatio rounding or browser-specific 100% inter-
     * pretation in PWA mode. */
    position: absolute;
    inset: 0;
}

.hidden {
    display: none !important;
}

/* ----- Highlight chip (top of map, when a route/trail is highlighted) */
.highlight-chip {
    position: absolute;
    top: calc(12px + var(--safe-top));
    left: 50%;
    transform: translateX(-50%);
    z-index: 6;
    background: var(--sheet-bg);
    border: 1px solid var(--sheet-border);
    border-radius: 999px;
    box-shadow: 0 3px 10px var(--sheet-shadow);
    padding: 6px 12px 6px 10px;
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    color: var(--sheet-text);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    max-width: calc(100vw - 32px);
}

.highlight-chip-swatch {
    display: inline-block;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--highlight-amber);
    border: 1.5px solid white;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);
    flex-shrink: 0;
}

/* Text container — primary label on top, optional second-line note
 * below. Column-flex with min-width:0 so each child can ellipsis-
 * truncate independently. The wrapper is always present in the DOM;
 * the .highlight-chip-note inside it is .hidden until there's
 * something to say (POI force-show "hidden in Options" message). */
.highlight-chip-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    gap: 1px;
}

.highlight-chip-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 60vw;
}

/* Second-line note — smaller, muted. Kept lighter weight than the
 * primary label so the count/name reads as primary content and the
 * note reads as explanatory meta. Same ellipsis treatment so a
 * narrow viewport doesn't blow out the chip width. */
.highlight-chip-note {
    font-size: 11px;
    font-weight: 400;
    color: var(--sheet-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 60vw;
}

/* When the note is present (sibling .highlight-chip-note is NOT
 * .hidden), drop the chip to a less-aggressive border-radius. The
 * default 999px stadium shape looks oblong at two lines; 14px still
 * reads as a rounded pill but sits more comfortably with the taller
 * footprint. :has() is universally supported in evergreen browsers
 * and degrades gracefully (chip stays a stadium) in any holdouts. */
.highlight-chip:has(.highlight-chip-note:not(.hidden)) {
    border-radius: 14px;
}

/* Per-route stats ("8.2 mi · 410 ft ↑") rendered next to the route
   name. Muted color so the name stays the visual anchor; smaller font
   size since the stats are reference data, not the main message.
   Hidden via .hidden when no stats are available so the chip still
   reads cleanly with just label + color. */
.highlight-chip-stats {
    color: var(--sheet-text-muted);
    font-size: 12px;
    font-weight: 400;
    white-space: nowrap;
    flex-shrink: 0;
}

.highlight-chip-close {
    color: var(--sheet-text-muted);
    flex-shrink: 0;
}

.highlight-chip:hover .highlight-chip-close,
.highlight-chip:focus-visible .highlight-chip-close {
    color: var(--sheet-text);
}

/* ----- Options-overlay section headers -----------------------------
 *
 * Uppercase categorical labels in the iOS / macOS Settings style. Not
 * interactive — the older accordion behaviour was removed because the
 * total row count (~14) scrolls comfortably without per-section
 * collapse. The body's row list sits visually as a group under each
 * label. */
.opt-section-header {
    display: block;
    padding: 16px 16px 6px 16px;
    margin: 0;
    background: transparent;
    border: none;
    font: inherit;
    text-align: left;
}

.opt-section-title {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--sheet-text-muted);
    margin: 0;
}

.opt-section-body {
    padding: 0 8px 4px 8px;
}

/* ----- Options-overlay toggle rows (full-width buttons) ------------
 *
 * Layout: swatch on left + label in middle + ON/OFF state on right.
 * Driven by aria-pressed. Wired up by wirePeekToggle() in app.js
 * (the helper kept its peek-era name; behaviour is unchanged). */
.opt-toggle-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 10px 10px;
    background: transparent;
    border: none;
    border-radius: 10px;
    color: var(--sheet-text);
    font: inherit;
    text-align: left;
}

/* Binary on/off rows opt into whole-row click via the
 * .opt-toggle-row-clickable marker class added by wirePeekToggle().
 * Multi-option rows (Labels / Season) deliberately stay inert
 * outside their inner buttons — there's no sensible "toggle" for
 * a 3-state choice, and a row-level cycle would clash with
 * direct-click on the buttons. */
.opt-toggle-row-clickable {
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
    .opt-toggle-row-clickable:hover {
        background: var(--sheet-hover);
    }
}

/* Invisible placeholder used by rows that don't have a colored
 * swatch (currently just the Labels row) — preserves the
 * 28px-of-left-indent that every other row gets from its swatch, so
 * the label text aligns vertically across the whole "What to show"
 * section even on rows without an icon. */
.layer-swatch-placeholder {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
}

.opt-toggle-row .layer-swatch {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    /* Swatch text labels (#, P, TH) inherit font-size 10px from the
     * base .layer-swatch rule. In the Options overlay's larger 28px
     * swatches that reads as tiny — bump to 13px to keep proportion
     * with the swatch dimensions. "TH" still fits at this size
     * (~2.4 ex per character × 2 chars = ~5 ex, swatch inner is ~25px
     * after 1.5px borders → comfortable). */
    font-size: 13px;
    /* MUST be positioned so the feature-swatch's ::before
     * coloured-dot pseudo-element AND the off-state ::after diagonal
     * slash both anchor inside the swatch instead of floating to
     * whatever ancestor happens to be positioned. */
    position: relative;
    overflow: visible;
}

.opt-toggle-row-label {
    flex: 1 1 auto;
    font-size: 14px;
    font-weight: 500;
    color: var(--sheet-text);
}

/* (.opt-toggle-row-state was the old single On/Off pill on the
 * right side of each toggle-row. It's been replaced by the
 * segmented On/Off pill (.opt-segmented-pill with two buttons),
 * which matches the Season row's visual pattern. The CSS for the
 * segmented pill lives further up in the file under "Segmented-
 * pill (Labels: Routes / Trails / None)".) */

/* Off state on swatches inside Options-overlay toggles — three-cue
 * treatment (transparent fill + grey border + slash). The
 * `data-multi-off="true"` selector is for multi-option rows whose
 * "off" state isn't a binary aria-pressed=false but a specific
 * value choice (e.g. Labels = "None" → row visually reads as off
 * the same way a binary toggle in off state does). JS sets the
 * data attribute when the chosen value is the "off" one. */
.opt-toggle-row[aria-pressed="false"] .layer-swatch,
.opt-toggle-row[data-multi-off="true"] .layer-swatch {
    background: transparent !important;
    border: 1.5px solid #9aa0a6 !important;
    color: #9aa0a6 !important;
    box-shadow: none !important;
}

.opt-toggle-row[aria-pressed="false"] .layer-swatch::after,
.opt-toggle-row[data-multi-off="true"] .layer-swatch::after {
    content: "";
    position: absolute;
    top: 50%;
    left: -2px;
    right: -2px;
    height: 1.5px;
    background: #9aa0a6;
    transform: translateY(-50%) rotate(-45deg);
    transform-origin: center;
    pointer-events: none;
}

.opt-toggle-row[aria-pressed="false"] .opt-toggle-row-label,
.opt-toggle-row[data-multi-off="true"] .opt-toggle-row-label {
    color: #6a6a6a;
}

/* Season toggle never shows the off-slash treatment — it cycles
 * between Summer and Winter, never "off." */
/* Season pill — same visual weight as the on/off pill, but its text
 * content is set inline by app.js (Summer ↔ Winter) rather than via
 * the ::after pseudo-element. */
.season-pill {
    flex: 0 0 auto;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 3px 10px;
    border-radius: 999px;
    background: var(--accent);
    color: white;
}

/* ----- Segmented-pill (Labels: Routes / Trails / None) ------------
 *
 * Sits on the right side of a .opt-toggle-row-segmented row in the
 * same visual weight as the on/off pill. Each child button is a small
 * pill segment; aria-checked drives the active fill. Stays compact
 * enough to fit alongside the row label on a phone screen. */
.opt-toggle-row-segmented {
    /* Row layout matches the toggle-row, but suppress the toggle-row's
     * "tap-anywhere-to-flip" cursor — only the segmented buttons do
     * anything; the rest of the row is passive. */
    cursor: default;
}

.opt-segmented-pill {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    gap: 0;
    padding: 2px;
    border: none;
    border-radius: 999px;
    background: var(--pill-bg);
}

.opt-segmented-pill .opt-segmented-btn {
    /* Override the default .opt-segmented-btn rules so the pill
     * variant doesn't inherit the 36px min-height, flex-grow, or
     * separator border that the original card-style segmented
     * control assumes. */
    flex: 0 0 auto;
    min-height: 0;
    border: none;
    border-left: none;
    background: transparent;
    color: var(--sheet-text-muted);
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 3px 8px;
    border-radius: 999px;
    cursor: pointer;
    line-height: 1.2;
    transition: background 0.12s, color 0.12s;
}

.opt-segmented-pill .opt-segmented-btn:hover {
    color: var(--sheet-text);
}

.opt-segmented-pill .opt-segmented-btn[aria-checked="true"] {
    background: var(--accent);
    color: white;
}

.opt-segmented-pill .opt-segmented-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

@media (prefers-reduced-motion: reduce) {
    .opt-segmented-pill .opt-segmented-btn {
        transition: none;
    }
}

/* ----- Sheet expanded area ----------------------------------------- */
.opt-section {
    border-top: 1px solid var(--sheet-divider);
    padding: 12px 0;
}

.opt-section:first-child {
    border-top: none;
    padding-top: 8px;
}

.opt-section-title {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--sheet-text-muted);
    margin: 0 0 8px 0;
}

.opt-row {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 4px 0;
}

.opt-row-stacked {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
}

.opt-row-help {
    font-size: 12px;
    color: var(--sheet-text-muted);
    line-height: 1.35;
    margin: 0 4px;
}

.opt-rows {
    display: flex;
    flex-direction: column;
    gap: 0;
}

/* ----- Field rows (Labels / Basemap selects in Options overlay) ---- */
.opt-row-field {
    justify-content: space-between;
    gap: 12px;
}

.opt-field-label {
    font-size: 13px;
    font-weight: 500;
    color: var(--sheet-text);
}

.opt-select {
    flex: 0 1 auto;
    padding: 8px 28px 8px 10px;
    border: 1px solid var(--sheet-border);
    border-radius: 6px;
    background: var(--input-bg);
    color: var(--sheet-text);
    font-family: inherit;
    font-size: 14px;
    cursor: pointer;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    min-height: 36px;
}

/* (Removed: the old "card-style" .opt-segmented + .opt-row-segmented
 * + .opt-segmented-btn base rules used to live here. They were dead
 * code — every segmented control in the current HTML uses
 * .opt-segmented-pill, and that variant's rules above cover styling
 * comprehensively. The base rules were also actively harmful: the
 * elements carry BOTH classes (.opt-segmented .opt-segmented-pill),
 * so .opt-segmented's `background: white` had the same specificity
 * as .opt-segmented-pill's `background: var(--pill-bg)`, and
 * appearing later in source it won — pinning every pill bg to white
 * regardless of the per-scheme --pill-bg token. Removed wholesale.) */

/* ----- Action rows (Install / About) ------------------------------- */
.opt-section-action {
    border-top: 1px solid var(--sheet-divider);
    padding: 4px 0;
}

.opt-action-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 52px;
    padding: 8px 4px;
    border-radius: 6px;
    border: none;
    background: transparent;
    text-align: left;
    cursor: pointer;
    font: inherit;
    color: var(--sheet-text);
    transition: background 0.1s;
}

.opt-action-row:hover {
    background: var(--sheet-hover);
}

/* Non-interactive variant — used for the install row on iOS, where
 * the icon + label appear for visual consistency with Android but
 * the actual install flow happens in the browser chrome (Share →
 * Add to Home Screen). Suppresses the pointer cursor + hover wash
 * so the row doesn't read as a tap target whose tap does nothing. */
.opt-action-row.is-static {
    cursor: default;
}
.opt-action-row.is-static:hover {
    background: transparent;
}

.opt-action-icon {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    /* Faint accent wash on the badge — a very low-opacity tint of
     * the per-map accent. Brings a subtle brand presence to the
     * action rows without coloring the icons themselves (which
     * would dilute the accent's role as "interactive state"
     * indicator). color-mix means a per-map override of --accent
     * cascades automatically. */
    background: color-mix(in srgb, var(--accent) 15%, transparent);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    color: var(--sheet-text);
    flex-shrink: 0;
}

.opt-action-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.opt-action-title {
    font-size: 14px;
    font-weight: 500;
}

.opt-action-help {
    font-size: 12px;
    color: var(--sheet-text-muted);
    line-height: 1.3;
}

/* Install button — replaces the generic ⇩ glyph with the actual app
 * icon (apple-touch-icon, 180×180) at thumbnail size, plus a small
 * "+" badge in the bottom-right. The visual reads as "add THIS app",
 * showing the user exactly what their home screen will look like
 * after install. Uses the same icon Android/iOS will use post-install
 * so there's no surprise. */
.install-btn .install-btn-icon {
    /* Override the inherited .opt-action-icon-style here in case a
     * future selector accidentally matches. */
    position: relative;
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    background: transparent;
    border-radius: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.install-btn .install-btn-icon img {
    width: 40px;
    height: 40px;
    border-radius: 8px;
    object-fit: cover;
    /* The android-chrome icon preserves source-image alpha (no white
     * composite at build time), so a transparent source would show
     * the page through the icon. A soft neutral background fills any
     * transparent regions — close enough to the indeterminate
     * background a real Android launcher renders behind the icon. */
    background: #f0f0f0;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

.install-btn .install-btn-icon::after {
    /* SVG cross instead of "+" text — most sans-serif glyphs render
     * the plus with a typographic centre that's above the visual
     * centre, so flex-centering it makes the badge look top-heavy.
     * Pure geometry has no such bias. Inline SVG via background-
     * image keeps the change to one rule and stays crisp at any DPI. */
    content: "";
    position: absolute;
    right: -6px;
    bottom: -6px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background-color: var(--accent);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='white' stroke-width='2' stroke-linecap='round'><line x1='6' y1='2' x2='6' y2='10'/><line x1='2' y1='6' x2='10' y2='6'/></svg>");
    background-repeat: no-repeat;
    background-position: center;
    background-size: 10px 10px;
    /* White ring separates the badge from the icon underneath, so it
     * stays legible on any icon — light, dark, busy, monochrome. */
    box-shadow: 0 0 0 2px var(--sheet-bg), 0 1px 3px rgba(0, 0, 0, 0.3);
}

/* ----- Layer swatches (POI marker color cues in toggle rows) -------
 *
 * Sized via the shared --poi-marker-* tokens so each swatch in the
 * Options overlay is a true miniature of the on-map marker it stands
 * for. The thin outline (box-shadow with --swatch-outline) is unique
 * to swatches — gives the chip an edge against the panel background.
 * The on-map markers use --poi-marker-map-shadow instead (drop-shadow,
 * not outline).
 */
.layer-swatch {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--poi-marker-size);
    height: var(--poi-marker-size);
    border-radius: var(--poi-marker-radius);
    font-size: var(--poi-marker-font-text);
    font-weight: 700;
    line-height: 1;
    color: white;
    border: var(--poi-marker-border) solid white;
    box-sizing: border-box;
    flex-shrink: 0;
    /* Faint outer outline. Light scheme: a darker hairline against
     * the white sheet — gives the chip's white border something to
     * contrast against, so it reads as a "white frame around the
     * coloured interior" the way it does naturally on the map. Dark
     * scheme: a lighter hairline against the dark sheet, same role.
     * The token --swatch-outline flips per scheme. Opacity is
     * intentionally low — high enough to perceive an edge, low
     * enough that the chip's perceived size matches its on-map
     * equivalent. */
    box-shadow: 0 0 0 1px var(--swatch-outline);
}

/* Override the inline SVG width/height attributes (which are baked
 * into index.html for the toilet, water, label, and season glyphs)
 * so the SVG size flows from the same token as everything else.
 * Means tweaking --poi-marker-svg-size updates all four glyphs
 * without touching HTML. */
.layer-swatch svg {
    width: var(--poi-marker-svg-size);
    height: var(--poi-marker-svg-size);
}

.marker-swatch {
    background: var(--marker-color, #795548);
    color: var(--marker-text-color, #fff);
    border: var(--poi-marker-border) solid var(--marker-border-color, #fff);
    font-size: var(--poi-marker-font-text-tight);
}

/* Emergency route-bucket swatch — still used on the Map-options
 * Emergency Access row even though POI markers are merged. */
.emergency-swatch {
    background: #c0392b;
}

.parking-swatch {
    background: var(--parking-color, #2980b9);
    color: var(--parking-text-color, #fff);
    border: var(--poi-marker-border) solid var(--parking-border-color, #fff);
}

/* Direction arrows — chrome indicator (not a sign), so it adapts to
 * colour scheme like the rest of the chrome and mirrors the on-map
 * arrow's fill+halo pairing. Light scheme: light-grey chip with a
 * black arrow + white halo (matches drawArrow's "#000 fill, white
 * halo" on a light basemap; the chip is intentionally NOT pure
 * white so the white halo reads against it the same way it reads
 * against a tinted basemap). Dark scheme: charcoal chip with a
 * white arrow + dark halo (matches drawArrow's "#fff fill, black
 * halo" on a dark basemap; charcoal rather than pure black so the
 * chip retains an edge against the dark panel).
 *
 * The halo is rendered as an SVG stroke under the fill (paint-order:
 * stroke fill) so the path's silhouette matches drawArrow's
 * stroke-then-fill rendering. stroke-width 4.36 in the 24-unit
 * viewBox is the same proportion as drawArrow's lineWidth=4 in its
 * 22-unit canvas. overflow: visible on the SVG keeps the halo from
 * being clipped at the path corners (which sit close to the
 * viewBox edges). */
.direction-arrows-swatch {
    background: var(--pill-bg);
    color: #000;
}

.direction-arrows-swatch svg {
    overflow: visible;
}

.direction-arrows-swatch path {
    stroke: rgba(255, 255, 255, 0.9);
    stroke-width: 4.36;
    stroke-linejoin: round;
    paint-order: stroke fill markers;
}

[data-color-scheme="dark"] .direction-arrows-swatch {
    background: #444;
    color: #fff;
}

[data-color-scheme="dark"] .direction-arrows-swatch path {
    stroke: rgba(0, 0, 0, 0.7);
}

.trailhead-swatch {
    background: var(--trailhead-color, #27ae60);
    color: var(--trailhead-text-color, #fff);
    border: var(--poi-marker-border) solid var(--trailhead-border-color, #fff);
    /* Wider chip to fit "TH" without cramping — matches the on-map
     * trailhead marker's aspect ratio so the swatch reads as the
     * same shape, just shrunk to row height. */
    width: var(--poi-marker-size-trailhead-w);
    font-size: var(--poi-marker-font-text-tight);
}

/* Labels — chrome indicator (not a sign), so it adapts to colour
 * scheme like the rest of the chrome. Light scheme: white chip
 * with slate tag icon (mirrors how labels render on a light
 * basemap — slate text on white halo). Dark scheme: dark chip
 * with light tag icon (mirrors how labels render on a dark
 * basemap — light text on dark halo). The chip doubles as a
 * tiny "label colour sample" in either mode. */
.label-swatch {
    background: var(--label-text-color, #fff);
    color: var(--label-color, #5d6d7e);
}

[data-color-scheme="dark"] .label-swatch {
    background: var(--input-bg);
    color: #f0f0f0;
}

/* Appearance — chrome indicator (not a sign), same adaptive pattern
 * as the Labels chip. Light scheme: light chip with charcoal sun-
 * moon glyph; dark scheme: dark chip with light sun-moon glyph.
 * The mdi:theme-light-dark icon already reads as a "theme switcher"
 * in both schemes, so no per-scheme glyph swap needed (unlike the
 * Season chip's sun ⇌ snowflake). */
.appearance-swatch {
    background: var(--input-bg);
    color: var(--sheet-text);
}

[data-color-scheme="dark"] .appearance-swatch {
    background: var(--input-bg);
    color: #f0f0f0;
}

/* Toilets — neutral slate to read as "facility" without competing
 * with parking blue or trailhead green. Glyph is a stylised standing
 * figure rendered as an SVG inside the swatch. */
.toilet-swatch {
    background: var(--toilet-color, #6c5ce7);
    color: var(--toilet-text-color, #fff);
    border: var(--poi-marker-border) solid var(--toilet-border-color, #fff);
}

/* Drinking water — saturated blue with a droplet glyph. Distinct
 * enough from the parking blue (which is darker) to read as its
 * own category at a glance. */
.drinking-water-swatch {
    background: var(--drinking-water-color, #3498db);
    color: var(--drinking-water-text-color, #fff);
    border: var(--poi-marker-border) solid var(--drinking-water-border-color, #fff);
}

/* Event-mode POI swatch — matches the on-map .event-poi-marker
 * background (saturated red, configurable via event_mode.poi_color
 * → --event-poi-color). White flag glyph inside. Same border
 * treatment as the other POI swatches. */
.event-swatch {
    background: var(--event-poi-color, #d32f2f);
    color: #fff;
    border: var(--poi-marker-border) solid #fff;
}

/* (Removed: a duplicate locate-state-* block that targeted
 * `.locate-swatch` and `.layer-swatch` descendants of #toggle-locate.
 * Those selectors don't match anything in the current FAB markup —
 * the actual SVG has class `.locate-icon`, not `.locate-swatch`. The
 * live state machine lives further down (search "Locate FAB state
 * machine") and targets `#toggle-locate.locate-state-*` directly.
 * The mtb-locate-spin @keyframes referenced by the live block
 * also lives further down. */

/* Feature swatch — the chip itself gets the same outer treatment as
 * Parking / Markers / Trailheads: a visible 1.5px border and the
 * subtle dark halo inherited from .layer-swatch base. What it does
 * NOT get is a coloured (purple) chip fill — the space between the
 * chip's border and the inner dot's white ring stays transparent so
 * the ring actually reads against the sheet background, the way
 * it does against map tiles on the map. The ::before reproduces
 * the on-map feature marker (see .feature-marker-icon below):
 * 12×12 purple circle, 2px white ring, soft drop shadow. */
.feature-swatch {
    background: transparent;
    /* border + box-shadow (halo) inherited from .layer-swatch base. */
}
.feature-swatch::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 12px;
    height: 12px;
    background: var(--feature-color, #8e44ad);
    border: 2px solid var(--feature-ring-color, #fff);
    border-radius: 50%;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
    transform: translate(-50%, -50%);
    box-sizing: border-box;
    pointer-events: none;
}

/* Difficulty swatch — sign-like chip preserving the IMBA "black
 * diamond on white" convention in BOTH light and dark schemes.
 * Real trail signage doesn't invert at sunset; the chip stays the
 * same so the symbology riders learn on the trailhead carries to
 * the in-app legend. The chip stands out from the dark sheet on
 * its own (white on dark = high contrast); the outline (from
 * .layer-swatch base) gives it edge in light mode where the white
 * blends with the sheet. */
.difficulty-swatch {
    background: #fff;
}
.difficulty-swatch::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 10px;
    height: 10px;
    background: #111;
    border: 1px solid #000;
    transform: translate(-50%, -50%) rotate(45deg);
    box-sizing: border-box;
    pointer-events: none;
}

/* Off-state: the base .opt-toggle-row[aria-pressed="false"]
 * .layer-swatch rule above already greys the chip and draws the
 * slash. Also hollow out the inner ::before shapes (purple dot,
 * black diamond) so they match the outline-only motif instead of
 * staying full-colour. */
/* ----- Finder rows --------------------------------------------------
 * (Pre-search-overlay finder container styles — .finder, .finder-search,
 * .finder-input, .finder-list — removed; the current Search overlay
 * uses .search-overlay-* classes for the chrome and these per-row
 * classes for the result rows it builds dynamically.) */

.finder-section-header {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--sheet-text-muted);
    padding: 8px 12px 4px 12px;
    background: var(--section-header-bg);
    border-bottom: 1px solid var(--sheet-divider);
}

.finder-section-header:first-child {
    border-top: none;
}

.finder-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: none;
    border-top: 1px solid var(--sheet-divider);
    background: transparent;
    text-align: left;
    cursor: pointer;
    font: inherit;
    color: var(--sheet-text);
    width: 100%;
    min-height: 44px;
    transition: background 0.1s;
}

.finder-row:hover,
.finder-row:focus-visible {
    background: var(--sheet-hover);
    outline: none;
}

/* Keyboard-navigation active state. Stronger than :hover because
 * the rider can't see a mouse cursor while their hands are on the
 * keyboard — the target row needs to be readable at a glance. The
 * left-edge accent stripe gives it a directional anchor that scans
 * fast, even in a long list. */
.finder-row.is-active {
    /* Accent at 18% opacity. color-mix lets per-map accent overrides
     * (CONFIG.accentColor → :root --accent) cascade through to this
     * keyboard-active background without any JS plumbing. */
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    box-shadow: inset 3px 0 0 var(--accent);
}

.finder-row-swatch {
    display: inline-block;
    width: 14px;
    height: 4px;
    border-radius: 2px;
    flex-shrink: 0;
    background: #888;
    /* Subtle outline so light-coloured swatches (e.g. white trail
     * colours) remain visible against the list's white background. */
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
}

.finder-row-trail-mark {
    display: inline-block;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--highlight-amber);
    border: 1.5px solid white;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);
    flex-shrink: 0;
}

/* (Removed: .finder-row-text + .finder-row-title — orphan classes
 * from the pre-search-overlay finder layout. Current row rendering
 * (makeRouteRow / makeTrailRow / makePoiRow in app.js) doesn't use
 * a wrapping text container or a separate title class.) */

.finder-row-meta {
    font-size: 12px;
    color: var(--sheet-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Make the route name flex-grow so the stats span gets right-aligned
   on the row. min-width: 0 lets the ellipsis kick in for long route
   names instead of pushing the stats off-screen. Only applies when
   the row actually has stats (otherwise the tag/empty space fills). */
.finder-row-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Per-route stats ("8.2 mi · 410 ft ↑") in the Finder row. Muted,
   smaller, never wraps, never shrinks — the row truncates the route
   name first if space is tight. */
.finder-row-stats {
    color: var(--sheet-text-muted);
    font-size: 12px;
    white-space: nowrap;
    flex-shrink: 0;
}

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

/* ----- About modal -------------------------------------------------- */
.about-modal {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
}

.about-modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    cursor: pointer;
}

.about-modal-content {
    position: relative;
    background: var(--sheet-bg);
    color: var(--sheet-text);
    border: 1px solid var(--sheet-border);
    border-radius: 10px;
    box-shadow: 0 6px 24px var(--sheet-shadow);
    max-width: 480px;
    width: calc(100vw - 32px);
    max-height: 80vh;
    overflow-y: auto;
    padding: 20px 24px 24px 24px;
    font-size: 13px;
    line-height: 1.5;
}

.about-modal-close {
    position: absolute;
    top: 8px;
    right: 10px;
    width: 32px;
    height: 32px;
    padding: 0;
    background: none;
    border: none;
    color: var(--sheet-text);
    cursor: pointer;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Welcome modal CTA — primary action button at the bottom of the
   first-visit modal. Reuses the toast primary-action color so the
   "tap me to confirm" affordance is consistent across the app. */
.welcome-modal-cta {
    display: block;
    width: 100%;
    margin-top: 16px;
    padding: 10px 16px;
    background: #33b5e5;
    color: #fff;
    border: 1px solid #33b5e5;
    border-radius: 8px;
    font: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.welcome-modal-cta:hover,
.welcome-modal-cta:focus-visible {
    background: #1ea5d8;
    border-color: #1ea5d8;
    outline: none;
}

/* Welcome modal body — three optional sections stacked vertically:
   curator-supplied paragraphs (welcome.body), the controls hint
   (skippable via show_controls_hint: false), and the sober
   attribution footer. Each section has its own class so styling
   stays independent. */
#welcome-modal-body {
    margin: 0;
}

.welcome-modal-body-p {
    margin: 0 0 12px 0;
    line-height: 1.5;
}

/* Controls-hint section header — matches the "Map style" / "What
 * to show" sentence-case pattern used elsewhere in the chrome. */
.welcome-modal-section-heading {
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--sheet-text-muted);
    margin: 18px 0 8px 0;
}

/* Each control row pairs the FAB icon with name + one-line
 * description. Icon sits in its own column so multi-line
 * descriptions wrap cleanly under the text, not under the icon. */
.welcome-modal-controls-list {
    list-style: none;
    padding: 0;
    margin: 0;
}

.welcome-modal-control-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 0 0 8px 0;
    line-height: 1.5;
}

.welcome-modal-control-icon {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    color: var(--sheet-text);
    margin-top: 1px;  /* nudge icon to align with cap-height of text */
}

.welcome-modal-control-text {
    flex: 1 1 auto;
    min-width: 0;
}

.about-modal-close:hover {
    background: var(--sheet-hover);
}

.about-modal-content h2 {
    font-size: 18px;
    font-weight: 600;
    margin: 0 28px 14px 0;
    color: var(--sheet-text);
}

.about-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    margin: 0 28px 14px 0;
}

.about-modal-header h2 {
    margin: 0;
    flex: 1 1 auto;
    min-width: 0;
}

.about-modal-logo {
    flex: 0 0 auto;
    max-width: 140px;
    max-height: 56px;
    width: auto;
    height: auto;
}

.about-modal-content h3 {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--sheet-text-muted);
    margin: 14px 0 4px 0;
}

.about-modal-content p {
    margin: 0 0 4px 0;
    white-space: pre-wrap;
    color: var(--sheet-text);
}

.about-modal-content ul {
    list-style: none;
    padding: 0;
    margin: 0 0 4px 0;
}

.about-modal-content li {
    margin: 2px 0;
}

.about-modal-content a {
    color: var(--link-color);
    text-decoration: none;
}

.about-modal-content a:hover {
    text-decoration: underline;
}

.about-modal-version {
    color: var(--sheet-text);
    opacity: 0.8;
    margin: 0 0 4px 0;
}

/* ----- MapLibre control overrides ---------------------------------- */
.maplibregl-ctrl-group {
    background: var(--sheet-bg) !important;
    border: 1px solid var(--sheet-border) !important;
    border-radius: 8px !important;
    box-shadow: 0 2px 6px var(--sheet-shadow) !important;
    overflow: hidden;
}

.maplibregl-ctrl-group button {
    background-color: transparent !important;
    border-bottom: 1px solid var(--sheet-border) !important;
}

.maplibregl-ctrl-group button:last-child {
    border-bottom: none !important;
}

.maplibregl-ctrl-group button:hover {
    background-color: var(--sheet-hover) !important;
}

.maplibregl-ctrl-attrib {
    background: var(--sheet-bg) !important;
    color: var(--sheet-text) !important;
    font-size: 10px !important;
}

/* Native MapLibre geolocate control is hidden — its function has
 * been promoted to the Locate FAB (bottom of the bottom-right stack),
 * which calls geolocate.trigger() directly. The control is still
 * added to the map because that's what wires up its event handlers /
 * state machine; we just don't want the redundant top-left button. */
.maplibregl-ctrl-geolocate {
    display: none !important;
}
/* Hiding the button alone leaves an empty .maplibregl-ctrl-group
 * wrapper, which has its own background + border + box-shadow and
 * renders as a ~2px-wide light gray rounded "dot" in the top-left
 * corner of the viewport. Hide the parent group too, but only when
 * it contains the (hidden) geolocate button — this leaves any
 * future top-left controls untouched. */
.maplibregl-ctrl-group:has(> .maplibregl-ctrl-geolocate) {
    display: none !important;
}

.maplibregl-ctrl-attrib a {
    color: var(--sheet-text) !important;
}

/* MapLibre's collapsed-attribution (i) button — when collapsed, the
 * chip shows just this round button with a circle-i SVG inside.
 * Vendor CSS embeds the SVG as a background-image with the path
 * defaulting to black fill, which is fine on light mode but reads
 * as a near-invisible dark glyph on the near-black popup chip in
 * dark mode. Override the background-image with the same path but
 * a light fill, so the (i) glyph stays readable. The path data is
 * identical to vendor's, only the fill='%23f0f0f0' attribute is
 * added to colour the glyph. */
[data-color-scheme="dark"] .maplibregl-ctrl-attrib-button {
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23f0f0f0' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E");
}

/* ----- Feature markers --------------------------------------------- */
/* ============================================================
 * On-map POI markers — base + per-type modifiers
 *
 * The visual styling for parking, trailheads, toilets, drinking
 * water, and trail markers used to live as inline `markerStyle`
 * template strings in app.js. They've been hoisted to CSS classes
 * so both Options swatches AND map markers reference the same
 * --poi-marker-* design tokens. Single source of truth — bumping
 * --poi-marker-size in :root resizes both surfaces in lockstep.
 *
 * Differences from .layer-swatch (the Options chip equivalent):
 *   - drop-shadow instead of dark-outline (gives apparent depth
 *     above the basemap; outline would compete with map content)
 *   - cursor: pointer (markers are tap targets)
 *
 * Per-type modifiers below set the color triple (background, text,
 * border) plus any size overrides. Trail marker is special:
 * variable-width to fit ref strings of any length.
 * ============================================================ */

.poi-marker {
    width: var(--poi-marker-size);
    height: var(--poi-marker-size);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--poi-marker-radius);
    border: var(--poi-marker-border) solid white;
    color: white;
    font-weight: 700;
    line-height: 1;
    box-sizing: border-box;
    box-shadow: var(--poi-marker-map-shadow);
    cursor: pointer;
}

.poi-marker svg {
    width: var(--poi-marker-svg-size);
    height: var(--poi-marker-svg-size);
}

.parking-marker {
    background: var(--parking-color, #2980b9);
    color: var(--parking-text-color, #fff);
    border-color: var(--parking-border-color, #fff);
    font-size: var(--poi-marker-font-text);
}

.trailhead-marker {
    background: var(--trailhead-color, #27ae60);
    color: var(--trailhead-text-color, #fff);
    border-color: var(--trailhead-border-color, #fff);
    font-size: var(--poi-marker-font-text-tight);
    /* Wider chip than the standard square so "TH" reads cleanly. */
    width: var(--poi-marker-size-trailhead-w);
}

/* Toilets + drinking water — no popup wired (the marker IS the
 * signal). Mirror .trail-marker's pointer-events: none + cursor:
 * default so the cursor doesn't lie about clickability and so taps
 * pass through to the trail underneath (where they DO open a
 * popup). The base .poi-marker rule sets cursor: pointer, which
 * is correct for parking/trailheads but wrong here. */
.toilet-marker {
    background: var(--toilet-color, #6c5ce7);
    border-color: var(--toilet-border-color, #fff);
    pointer-events: none;
    cursor: default;
}

.drinking-water-marker {
    background: var(--drinking-water-color, #3498db);
    border-color: var(--drinking-water-border-color, #fff);
    pointer-events: none;
    cursor: default;
}

/* Event POIs (event_mode.pois) — race-day fixtures: start / finish,
 * aid stations, support areas. Always visible (no toggle), distinct
 * saturated red so they don't get mistaken for any OSM POI category.
 * Configurable via event_mode.poi_color → --event-poi-color. Popup
 * IS wired here (the curator usually has context to share — aid
 * station hours, support contact, etc.) so we keep the standard
 * pointer-events / cursor (inherited from .poi-marker base).
 *
 * Note: do NOT add `position: relative` here. MapLibre's
 * .maplibregl-marker rule sets `position: absolute` to anchor the
 * marker at the lat/lng. Both rules have equal specificity (0,1,0);
 * our stylesheet loads AFTER MapLibre's, so any position declaration
 * here would override and break the marker's pixel positioning
 * (each marker would render at its in-container flow position +
 * MapLibre's transform, producing a per-marker pixel offset that
 * looks like a fixed error on screen and gets visually amplified
 * at low zoom). The label hangs below the chip via position:
 * absolute on the label itself; absolute elements look up to the
 * nearest positioned ancestor for their containing block, and
 * MapLibre's `position: absolute` qualifies — no `position: relative`
 * needed on this rule. */
.event-poi-marker {
    background: var(--event-poi-color, #d32f2f);
    border-color: #fff;
}

/* Name label below the event-POI chip. Absolutely positioned so the
 * chip stays the marker's bounding box (MapLibre's anchor math is
 * unchanged). White-ish pill behind the text gives contrast over any
 * basemap; the box-shadow lifts it a half-step off the map for
 * legibility. `pointer-events: none` lets clicks pass through to the
 * chip below (the chip is the popup trigger, not the label). */
.event-poi-marker-label {
    position: absolute;
    top: calc(100% + 4px);
    left: 50%;
    transform: translateX(-50%);
    padding: 2px 6px;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.92);
    color: var(--sheet-text, #222);
    font-size: 11px;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
    pointer-events: none;
}

/* Dark-scheme variant — semi-opaque dark pill with light text so the
 * label reads against a dark basemap. */
[data-color-scheme="dark"] .event-poi-marker-label {
    background: rgba(28, 28, 30, 0.92);
    color: var(--sheet-text, #f0f0f0);
}

/* Description line under the popup-title for event POIs. Smaller
 * than the title, slightly muted, line-wraps cleanly inside the
 * 240px max-width set on the popup. */
.popup-event-poi .popup-description {
    margin-top: 4px;
    font-size: 12px;
    line-height: 1.35;
    color: var(--sheet-text-muted, #555);
}

/* Trail markers — variable-width to accommodate ref strings of
 * varying length ("23", "EAP-1", "#"). min-width keeps a single-
 * character chip from collapsing below readability; padding gives
 * multi-character refs breathing room. pointer-events: none so
 * the marker doesn't intercept map clicks (no popup wired). */
.trail-marker {
    background: var(--marker-color, #795548);
    color: var(--marker-text-color, #fff);
    border-color: var(--marker-border-color, #fff);
    font-size: var(--poi-marker-font-text-tight);
    height: var(--poi-marker-size-trail-marker-h);
    min-width: var(--poi-marker-size-trail-marker-min-w);
    width: auto;
    padding: 0 4px;
    pointer-events: none;
    cursor: default;
}

.feature-marker {
    display: flex;
    flex-direction: column;
    align-items: center;
    transform: translateY(-7px);
    pointer-events: none;
}

.feature-marker-icon {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 2px solid var(--feature-ring-color, #fff);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
    box-sizing: border-box;
}

/* Feature marker label — DOM-rendered text under the dot icon, sits
 * on the basemap. Light scheme: dark text + white halo (mirrors
 * MAP_PAINT_TOKENS.light for trail labels). Dark scheme: light
 * text + dark halo (mirrors MAP_PAINT_TOKENS.dark). The 8-direction
 * text-shadow stack fakes a 1 px halo around the glyphs since
 * text-stroke isn't reliable cross-browser. */
.feature-marker-label {
    margin-top: 3px;
    font-size: 11px;
    font-weight: 600;
    line-height: 1.1;
    color: var(--feature-label-color);
    text-align: center;
    white-space: nowrap;
    text-shadow:
        -1px -1px 0 var(--feature-label-halo),
         1px -1px 0 var(--feature-label-halo),
        -1px  1px 0 var(--feature-label-halo),
         1px  1px 0 var(--feature-label-halo),
         0   -1px 0 var(--feature-label-halo),
         0    1px 0 var(--feature-label-halo),
        -1px  0   0 var(--feature-label-halo),
         1px  0   0 var(--feature-label-halo);
}

/* ----- Popups ------------------------------------------------------ */
/* Popup wrapper — shadow lives here (not on .content) so it
 * follows the COMBINED silhouette of body + tip as a single
 * shape. With the shadow on .content alone, the tip cast no
 * shadow of its own and the body's edge "ended" abruptly at
 * the tip's connection point, looking visually broken. Using
 * filter: drop-shadow on the wrapper paints one continuous
 * shadow around the rectangle-plus-triangle outline. */
.maplibregl-popup {
    filter: drop-shadow(0 3px 12px var(--sheet-shadow));
}

.maplibregl-popup-content {
    font-family: inherit;
    font-size: 13px;
    padding: 8px 14px 10px 14px;
    border-radius: 8px;
    background: var(--popup-bg);
    color: var(--sheet-text);
    /* Pseudo-element accent strip needs a positioned ancestor + a
     * clip boundary so the strip respects the popup's rounded
     * corners. */
    position: relative;
    overflow: hidden;
}

/* Type-coded accent strip on the left edge — visually links the
 * popup back to its marker. 4 px wide, full body height. Uses the
 * same per-type CSS variables that drive marker fill, so curator
 * colour overrides (parking_color, trailhead_color) cascade
 * automatically. The popup-parking / popup-trailhead class is
 * added to the OUTER wrapper (.maplibregl-popup) by MapLibre's
 * Popup.addClassName() — that's why the selectors below pair the
 * wrapper class with a descendant .maplibregl-popup-content. */
.maplibregl-popup.popup-parking .maplibregl-popup-content::before,
.maplibregl-popup.popup-trailhead .maplibregl-popup-content::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    width: 4px;
}

.maplibregl-popup.popup-parking .maplibregl-popup-content::before {
    background: var(--parking-color, #2980b9);
}

.maplibregl-popup.popup-trailhead .maplibregl-popup-content::before {
    background: var(--trailhead-color, #27ae60);
}

/* Bump left padding by the strip's width so text doesn't sit on
 * top of the colored bar. Only applies to popups that actually
 * have a strip; default popups keep their original 14 px padding. */
.maplibregl-popup.popup-parking .maplibregl-popup-content,
.maplibregl-popup.popup-trailhead .maplibregl-popup-content {
    padding-left: 18px;
}

/* Popup tip — the triangle pointing from the popup body to its
 * anchor point. MapLibre's vendor CSS hardcodes border colors per
 * anchor position with two-class specificity (e.g.
 * `.maplibregl-popup-anchor-bottom .maplibregl-popup-tip { border-
 * top-color: #fff }`), which beats a bare `.maplibregl-popup-tip`
 * override. We mirror MapLibre's selector shape here so the tip
 * picks up `var(--sheet-bg)` regardless of which side of the
 * marker the popup landed on (and therefore adapts to dark mode
 * the same way the popup body does). Each anchor uses a different
 * border-side to form its triangle, so each rule sets the right
 * one. */
.maplibregl-popup-anchor-top .maplibregl-popup-tip,
.maplibregl-popup-anchor-top-left .maplibregl-popup-tip,
.maplibregl-popup-anchor-top-right .maplibregl-popup-tip {
    border-bottom-color: var(--popup-bg);
}

.maplibregl-popup-anchor-bottom .maplibregl-popup-tip,
.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip,
.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip {
    border-top-color: var(--popup-bg);
}

.maplibregl-popup-anchor-left .maplibregl-popup-tip {
    border-right-color: var(--popup-bg);
}

.maplibregl-popup-anchor-right .maplibregl-popup-tip {
    border-left-color: var(--popup-bg);
}

.maplibregl-popup-close-button {
    font-size: 18px;
    line-height: 1;
    padding: 4px 6px;
    right: 0;
    top: 0;
    color: inherit;
}

.popup-title {
    font-weight: 600;
    margin-bottom: 4px;
    /* Long titles (e.g. multi-word trail names) wrap onto multiple
     * lines inside the popup's max-width rather than horizontally
     * overflowing the popup container (which is `overflow: hidden`,
     * so the overflow would be silently clipped AND would throw
     * off MapLibre's popup-tail auto-anchor calculation). */
    overflow-wrap: break-word;
    word-break: break-word;
}

.popup-hr {
    margin: 4px 0;
    border: none;
    border-top: 1px solid #ccc;
}

.popup-routes {
    font-size: 12px;
    margin-top: 2px;
    /* Same reason as .popup-title above: a long route name like
     * "2026 Shelden's Big Bang Course" needs to wrap inside the
     * popup, not punch through .maplibregl-popup-content's
     * `overflow: hidden`. */
    overflow-wrap: break-word;
    word-break: break-word;
}

.popup-directions {
    display: inline-block;
    margin-top: 6px;
    color: var(--link-color);
    text-decoration: none;
    font-size: 12px;
    font-weight: 500;
}

.popup-directions:hover {
    text-decoration: underline;
}

/* ----- Geolocate user dot ------------------------------------------ */
.maplibregl-user-location-accuracy-circle {
    background-color: rgba(52, 152, 219, 0.15) !important;
}

.maplibregl-user-location-dot {
    background-color: #3498db !important;
}

/* Push the user-location marker (dot + accuracy circle) above POI
 * markers in the DOM overlay. MapLibre renders each location element
 * inside its own .maplibregl-marker wrapper; raising that wrapper's
 * z-index keeps the blue dot visible even when a parking / trailhead
 * / feature marker sits at the same spot. */
.maplibregl-marker:has(> .maplibregl-user-location-dot),
.maplibregl-marker:has(> .maplibregl-user-location-accuracy-circle) {
    z-index: 2;
}

/* ----- Spotlight dim (map_dim_on_highlight) ------------------------
 * POI markers that aren't adjacent to the highlighted route/trail are
 * faded. The fade is driven by an INLINE style set in app.js
 * (updateMarkerDimState), not a CSS class — marker elements already
 * carry inline cssText from `markerStyle`, so keeping the fade on the
 * same style layer avoids any specificity/override surprises. */

/* ----- Off-screen location indicator -------------------------------
 * (The legacy .location-indicator class was renamed to
 * .off-screen-indicator earlier in the UI rework; the comma-paired
 * selectors below have been trimmed to just the live class.) */
.off-screen-indicator {
    position: absolute;
    z-index: 3;
    display: none;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    transform: translate(-50%, -50%);
    cursor: pointer;
    pointer-events: auto;
    -webkit-user-select: none;
    user-select: none;
}

.off-screen-indicator-arrow {
    font-size: 22px;
    line-height: 1;
    color: #3498db;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

.off-screen-indicator-dist {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    font-weight: 600;
    line-height: 1;
    color: #fff;
    background: rgba(0, 0, 0, 0.55);
    /* Symmetric padding — flex's align-items: center handles the
     * vertical centering. The previous `padding: 2px 7px 0` (top
     * inset, no bottom) was an old workaround that overcorrected
     * and pushed the digits up off-center. */
    padding: 0 8px;
    height: 18px;
    border-radius: 9px;
    white-space: nowrap;
}

/* ----- Toasts ------------------------------------------------------ */
/* Centered both axes — keeps toasts well clear of the FAB stack and
 * the safe-area edges on phones. Drops `white-space: nowrap` so
 * longer messages wrap to multiple lines instead of overflowing off
 * the viewport. */
.map-toast {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: rgba(0, 0, 0, 0.85);
    color: white;
    padding: 12px 18px;
    border-radius: 10px;
    font-size: 14px;
    font-family: inherit;
    line-height: 1.35;
    text-align: center;
    /* Cap width so wrapping kicks in cleanly. min() picks whichever
     * is smaller: 90vw (works at narrow phone widths) or 360px (a
     * nice paragraph-width on tablet/desktop). */
    max-width: min(90vw, 360px);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
    /* Sits above the about / welcome modals (z=100). The SW-update
     * toast in particular needs to surface on top of the welcome
     * modal a first-visit rider sees, otherwise they can't reach
     * the Reload button without dismissing welcome first. Transient
     * toasts auto-dismiss; persistent toasts (e.g. SW update) carry
     * a × so the rider can defer if mid-task — neither variant
     * permanently blocks whatever's underneath. */
    z-index: 200;
    pointer-events: none;
    transition: opacity 0.3s;
}

.map-toast.visible { opacity: 1; }
.map-toast.hidden  { opacity: 0; }

/* Persistent / actionable toast (B.7 SW update, etc.). Becomes
 * pointer-active so the action + close buttons are clickable. Lays
 * the message + actions + close out in a row with comfortable
 * spacing; on narrow screens (max-width: min(90vw, 360px)), the row
 * naturally wraps and the close button sits at the bottom-right. */
.map-toast.map-toast-actionable {
    pointer-events: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    flex-wrap: wrap;
    text-align: left;
    padding: 10px 12px 10px 16px;
}

.map-toast-message {
    flex: 1 1 auto;
    min-width: 0;
}

/* Action button — pill, slightly inset from the toast's edge. The
 * primary variant uses the same sky-blue accent as the highlight
 * chip and Locate-active state for consistency. */
.map-toast-action {
    flex-shrink: 0;
    background: rgba(255, 255, 255, 0.12);
    color: white;
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 16px;
    padding: 6px 14px;
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.map-toast-action:hover,
.map-toast-action:focus-visible {
    background: rgba(255, 255, 255, 0.22);
    outline: none;
}

.map-toast-action-primary {
    background: #33b5e5;
    border-color: #33b5e5;
    color: #fff;
}

.map-toast-action-primary:hover,
.map-toast-action-primary:focus-visible {
    background: #1ea5d8;
    border-color: #1ea5d8;
}

/* Small ✕ to dismiss the persistent toast without taking the action. */
.map-toast-close {
    flex-shrink: 0;
    background: transparent;
    border: none;
    color: rgba(255, 255, 255, 0.7);
    padding: 4px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    -webkit-tap-highlight-color: transparent;
}

.map-toast-close:hover,
.map-toast-close:focus-visible {
    color: white;
    background: rgba(255, 255, 255, 0.12);
    outline: none;
}

/* ----- Mobile / narrow viewport tweaks ----------------------------- */
@media (max-width: 480px) {
    .about-modal-content {
        padding: 16px 18px 20px 18px;
        max-height: 85vh;
    }

    .about-modal-content h2 {
        font-size: 16px;
    }
}

/* ============================================================
 * Floating chrome (Phase 3 of the UI v2 rework)
 *
 * The map fills the entire viewport. Pieces of chrome float over
 * it, occupying corners:
 *   - #brand              top-left     : logo or title text
 *   - #fab-stack-top      top-right    : Locate (corner) + Options
 *   - #fab-stack-bottom   bottom-right : Search (one-handed thumb)
 *   - attribution         bottom-left  : OSM/Protomaps/Mapterhorn
 *   - .options-overlay    full-screen  : opens via Options FAB
 *   - .search-overlay     half-sheet   : opens via Search FAB
 *
 * FAB stacks sit BELOW open overlays in the z-stack so the overlay
 * panel cleanly covers them at the same corner — see .fab-stack
 * below for the full reasoning.
 * ============================================================ */

/* ----- Brand (top-left identity) ----------------------------------- */

#brand {
    position: fixed;
    top: calc(12px + var(--safe-top));
    left: calc(12px + env(safe-area-inset-left, 0));
    z-index: 4;
    max-width: min(60vw, 240px);
    pointer-events: none;
    display: flex;
    align-items: center;
}

#brand-img {
    max-width: 200px;
    max-height: 48px;
    width: auto;
    height: auto;
    opacity: 0.95;
    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.35));
}

/* Logo auto-invert in dark mode. JS adds .invert-dark to #brand-img
 * at boot iff CONFIG.invertLogoDark !== false (default true).
 * invert(1) hue-rotate(180deg) flips lightness without flipping
 * hue, so coloured logo elements stay the right hue rather than
 * going green→magenta etc. For monochrome logos this is equivalent
 * to plain invert. The drop-shadow is preserved (filter chain
 * reapplies it after the invert). Curators with logos that look
 * bad inverted set invert_logo_dark: false in YAML to opt out.
 */
[data-color-scheme="dark"] #brand-img.invert-dark {
    filter: invert(1) hue-rotate(180deg)
            drop-shadow(0 1px 3px rgba(0, 0, 0, 0.35));
}

/* Title span — visible only when no image is present (build-time
 * stripping removes the <img> when neither logo nor icon is
 * configured). When the img IS present, hide the span via :has()
 * so screen readers still get it as a fallback if the image
 * fails to load (alt text covers the SR case anyway). */
#brand-title {
    font-size: 16px;
    font-weight: 700;
    color: var(--sheet-text);
    text-shadow: 0 0 6px rgba(255, 255, 255, 0.85);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

#brand:has(#brand-img) #brand-title {
    /* Visually hidden but kept in the DOM for screen readers. The
     * <img alt> already names the brand for SR; this is defensive in
     * case the image never loads. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ----- Floating button stacks (split: top-right + bottom-right) --- */
/* Two corner-anchored vertical stacks. Top-right holds Locate
 * (corner) + Options. Bottom-right holds Search alone, where a
 * one-handed thumb naturally rests. Same 12 px corner inset as the
 * brand element on the left.
 *
 * z-index sits BELOW any open overlay (search z=6, options z=7) so
 * the overlay panel and its close-X cleanly cover the FABs at the
 * same corner — no visual overlap at the close button. Trade-off:
 * cross-FAB switching (tap the other FAB while one overlay is open)
 * doesn't work because the FAB is hidden behind the overlay; rider
 * closes the current overlay first, then taps the other FAB.
 *
 * Below the About modal (z-index 9 / 10) so About remains true-modal. */
.fab-stack {
    position: fixed;
    right: calc(12px + env(safe-area-inset-right, 0));
    z-index: 5;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.fab-stack-top {
    top: calc(12px + var(--safe-top));
}

.fab-stack-bottom {
    bottom: calc(12px + var(--safe-bottom));
}

/* Hide FAB stacks when ANY overlay is open. Without this, the
 * top-right stack stays visible through the search-overlay dim
 * (the dim is only 32% opaque) — the FABs would look interactive
 * but tapping them dismisses the overlay instead of activating
 * the FAB, which is confusing. Hiding them clarifies the modal
 * relationship: while an overlay is up, the FABs are not the
 * active control surface; close the overlay to get them back. */
body:has(.search-overlay.is-open) .fab-stack,
body:has(.options-overlay.is-open) .fab-stack {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease-out;
}

.fab {
    width: 48px;
    height: 48px;
    border-radius: 24px;
    /* FAB chrome adapts to the colour scheme via the same sheet
     * tokens used by the overlays and chip — on a dark basemap, a
     * dark FAB with a light icon reads as part of the dark UI
     * rather than a bright white spot. */
    background: var(--sheet-bg);
    border: 1.5px solid var(--sheet-border);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
    color: var(--sheet-text);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    transition: background 0.12s, transform 0.06s;
    -webkit-tap-highlight-color: transparent;
    /* Positioning context for ::after pseudo-elements (e.g. the
     * locate-state-disabled red strike). Without this, abs-positioned
     * descendants resolve against the .fab-stack ancestor and the
     * strike lands centred on the whole stack, not on the FAB. */
    position: relative;
}

.fab:hover {
    /* --pill-bg is "subtly different from sheet bg" — light grey on
     * light mode (darker than sheet), darker grey on dark mode
     * (lighter than sheet). Replacing the bg outright instead of
     * overlaying because --sheet-hover is translucent and would
     * make the FAB nearly transparent against the map. */
    background: var(--pill-bg);
}

.fab:active {
    transform: scale(0.96);
}

/* FAB labels — first-visit-per-map discoverability cue (see
 * setupFabLabels in app.js). Each FAB temporarily renders a pill
 * label to its left explaining what the icon does. After the rider
 * dismisses (any FAB tap or 15 s auto-timeout), an LS flag
 * (mtb.fabsLabeled) suppresses the labels on subsequent visits to
 * this map.
 *
 * Anchored to the right edge of the FAB so it floats out into the
 * map area on the left. Pointer-events: none so taps still register
 * on the FAB underneath the label, not on the label itself. The
 * .fab is position: relative (set above) so the label's absolute
 * positioning resolves against it. */
.fab-label {
    position: absolute;
    right: calc(100% + 8px);
    top: 50%;
    /* Tucked toward the FAB by default — slide-in animation moves
     * it to translateX(0) (final resting position) on reveal, slide-
     * out returns it here on dismissal. The 8px offset is small
     * enough to be visually subtle but enough to read as "moving". */
    transform: translateY(-50%) translateX(8px);
    background: var(--sheet-bg);
    color: var(--sheet-text);
    border: 1.5px solid var(--sheet-border);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
    border-radius: 16px;
    padding: 6px 12px;
    font-size: 13px;
    font-weight: 500;
    line-height: 1;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.22s ease-out, transform 0.22s ease-out;
}

body.fabs-labeled .fab-label {
    opacity: 1;
    transform: translateY(-50%) translateX(0);
}

@media (prefers-reduced-motion: reduce) {
    .fab-label {
        transition: none;
    }
}

.fab:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Pressed state — when the corresponding overlay is open. Solid
 * accent fill + white icon so it's obvious which button triggered
 * the visible overlay. */
.fab[aria-pressed="true"]:not(#toggle-locate) {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

@media (prefers-reduced-motion: reduce) {
    .fab {
        transition: none;
    }
    .fab:active {
        transform: none;
    }
}

/* ----- Locate FAB state machine ----------------------------------
 * Mirrors MapLibre's native GeolocateControl FSM. The state classes
 * (locate-state-*) are stamped onto #toggle-locate by app.js. Color
 * on the button cascades into the SVG (.locate-icon, fill=currentColor)
 * via inheritance.
 *
 * Tracking / error / disabled states use semantic colours that read
 * fine on both light and dark FAB backgrounds (cyan, salmon, red,
 * mid-grey). The idle "default, not yet tracking" colour adapts to
 * the colour scheme via --sheet-text — dark in light mode, light in
 * dark mode — otherwise the icon would disappear into a dark FAB
 * and read as disabled. */

@keyframes mtb-locate-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(1turn); }
}

#toggle-locate.locate-state-idle {
    color: var(--sheet-text);
}

#toggle-locate.locate-state-waiting .locate-icon {
    animation: mtb-locate-spin 2s linear infinite;
    transform-origin: center;
}

#toggle-locate.locate-state-active {
    color: #33b5e5;
}

#toggle-locate.locate-state-background {
    color: #33b5e5;
}
#toggle-locate.locate-state-background .locate-dot {
    display: none;
}

#toggle-locate.locate-state-active-error {
    color: #e58978;
}

#toggle-locate.locate-state-background-error {
    color: #e54e33;
}
#toggle-locate.locate-state-background-error .locate-dot {
    display: none;
}

#toggle-locate.locate-state-disabled {
    color: #999;
}
#toggle-locate.locate-state-disabled::after {
    /* Red strikethrough across the button when geolocation has been
     * permission-denied. */
    content: "";
    position: absolute;
    top: 50%;
    left: 6px;
    right: 6px;
    height: 2px;
    background: #d33;
    transform: translateY(-50%) rotate(-45deg);
    pointer-events: none;
}

/* ----- Search overlay (outer backdrop + inner panel) -------------
 *
 * Outer .search-overlay: full-viewport layer. Acts as the dim
 * backdrop AND the click-outside dismissal target. The dim fades
 * in/out on .is-open. Pointer-events stay auto so the backdrop
 * area receives clicks (which the JS interprets as dismissal).
 *
 * Inner .search-overlay-panel: the visible chrome. On mobile a
 * half-sheet anchored to the bottom; on desktop a centred card
 * floating above the bottom edge with margin from all four sides.
 * Slides up from the bottom on .is-open. */

.search-overlay {
    position: fixed;
    inset: 0;
    z-index: 6;
    background: rgba(0, 0, 0, 0);  /* transparent → dim on .is-open */
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    pointer-events: none;
    transition: background 0.22s ease-out;
}

.search-overlay.is-open {
    background: rgba(0, 0, 0, 0.32);
    pointer-events: auto;
}

.search-overlay[hidden] {
    display: none !important;
}

.search-overlay-panel {
    /* dvh (dynamic viewport height) so the height shrinks when the
     * mobile keyboard opens. Combined with `interactive-widget=
     * resizes-content` in the viewport meta, this keeps the panel
     * fully visible above the keyboard instead of being half-hidden. */
    height: var(--search-overlay-height, min(60dvh, 480px));
    background: var(--sheet-bg);
    border-top: 1px solid var(--sheet-border);
    border-top-left-radius: 14px;
    border-top-right-radius: 14px;
    box-shadow: 0 -2px 12px var(--sheet-shadow);
    display: flex;
    flex-direction: column;
    padding-bottom: var(--safe-bottom);
    transform: translateY(100%);
    transition: transform 0.22s ease-out;
    overflow: hidden;
}

.search-overlay.is-open .search-overlay-panel {
    transform: translateY(0);
}

.search-overlay-header {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px;
    border-bottom: 1px solid var(--sheet-divider);
    flex-shrink: 0;
}

/* Wrapper around the input so the clear button can be absolutely
 * positioned over the input's right edge — Google/Apple-style inline
 * clear affordance. The wrapper takes the input's flex slot in the
 * header row. */
.search-input-wrap {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
}

.search-overlay-input {
    flex: 1 1 auto;
    min-width: 0;
    height: 38px;
    /* Right padding reserves space for the absolutely-positioned
     * clear button so typed text doesn't slide under it. */
    padding: 0 38px 0 12px;
    border: 1px solid var(--sheet-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--sheet-text);
    font: inherit;
    font-size: 15px;
    -webkit-appearance: none;
    appearance: none;
}

.search-overlay-input:focus {
    outline: 2px solid var(--accent);
    outline-offset: 0;
    border-color: var(--accent);
}

/* Suppress the browser's native search clear button — we render our
 * own so the affordance looks/behaves identically across browsers
 * and respects the same accent/focus styling as the rest of the UI. */
.search-overlay-input::-webkit-search-cancel-button,
.search-overlay-input::-webkit-search-decoration {
    -webkit-appearance: none;
    appearance: none;
    display: none;
}

/* Inline clear button — sits over the input's right edge. Hidden via
 * the .hidden utility class when the input is empty (toggled in JS).
 * 30×30 hit area is comfortable for touch without looking heavy. */
.search-clear-btn {
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    width: 30px;
    height: 30px;
    padding: 0;
    background: transparent;
    border: none;
    border-radius: 50%;
    color: var(--sheet-text-muted);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.12s, color 0.12s;
}

.search-clear-btn:hover {
    background: var(--sheet-hover);
    color: var(--sheet-text);
}

.search-clear-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.search-overlay-cancel {
    flex: 0 0 auto;
    background: none;
    border: none;
    color: var(--accent);
    font: inherit;
    font-size: 15px;
    font-weight: 500;
    cursor: pointer;
    padding: 8px 4px;
}

.search-overlay-filters {
    display: flex;
    gap: 6px;
    padding: 8px 12px;
    border-bottom: 1px solid var(--sheet-divider);
    overflow-x: auto;
    scrollbar-width: none;
    flex-shrink: 0;
}

.search-overlay-filters::-webkit-scrollbar {
    display: none;
}

.search-filter-chip {
    flex: 0 0 auto;
    background: var(--pill-bg);
    color: var(--sheet-text);
    border: 1px solid transparent;
    border-radius: 999px;
    padding: 5px 12px;
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.search-filter-chip[aria-selected="true"] {
    background: var(--accent);
    color: white;
}

.search-overlay-results {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 4px 0;
}

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

/* ----- Options overlay (outer backdrop + inner panel) -----------
 *
 * Same outer/inner structure as the Search overlay so dimming and
 * dismissal behave identically. Mobile: panel fills the viewport
 * (the dim is hidden behind it). Desktop: panel is a centred card
 * with the dim visible around it. Slides up from the bottom on
 * mobile, fades in on desktop. */

.options-overlay {
    position: fixed;
    inset: 0;
    z-index: 7;
    background: rgba(0, 0, 0, 0);  /* transparent → dim on .is-open */
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    pointer-events: none;
    transition: background 0.22s ease-out;
}

.options-overlay.is-open {
    background: rgba(0, 0, 0, 0.45);
    pointer-events: auto;
}

.options-overlay[hidden] {
    display: none !important;
}

.options-overlay-panel {
    /* Mobile: fills the viewport. Desktop overrides below cap the
     * width / height and centre the panel. */
    flex: 1 1 auto;
    background: var(--sheet-bg);
    display: flex;
    flex-direction: column;
    padding-top: var(--safe-top);
    padding-bottom: var(--safe-bottom);
    transform: translateY(100%);
    transition: transform 0.22s ease-out;
    overflow: hidden;
}

.options-overlay.is-open .options-overlay-panel {
    transform: translateY(0);
}

.options-overlay-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
    border-bottom: 1px solid var(--sheet-divider);
    flex-shrink: 0;
}

.options-overlay-title {
    font-size: 18px;
    font-weight: 600;
    color: var(--sheet-text);
    margin: 0;
}

/* Shared close-button style for both overlays. mdi:close icon
 * inside a 36×36 hit target — generous enough for thumb taps on
 * mobile (Apple HIG suggests 44px minimum but the icon's visible
 * size + adjacent header padding gives the same effective tap
 * area as a 44px button). Subtle hover background so the button
 * reads as interactive without being a heavy chrome element. */
.overlay-close-btn {
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    background: transparent;
    border: none;
    border-radius: 8px;
    color: var(--sheet-text-muted);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.12s, color 0.12s;
}

.overlay-close-btn:hover {
    background: var(--sheet-hover);
    color: var(--sheet-text);
}

.overlay-close-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.options-overlay-body {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
}

@media (prefers-reduced-motion: reduce) {
    .search-overlay,
    .options-overlay,
    .search-overlay-panel,
    .options-overlay-panel {
        transition: none;
    }
}

/* ----- Season + difficulty/feature off-state hollowing ------------- */

/* Season swatch background — summer (default) is a forest green
 * matching the warm growing-season palette; winter (.is-winter
 * class toggled by app.js) is a cold cyan-leaning teal-blue that
 * reads as glacier / ice rather than generic "blue", and is
 * distinctly cooler than the parking blue (#2980b9) and water
 * blue (#3498db) it shares the chrome with. */
.opt-toggle-row.season-toggle-row .season-swatch {
    background: #2e7d32;
    color: white;
}
.opt-toggle-row.season-toggle-row .season-swatch.is-winter {
    background: #0e7490;
}

/* Off-state hollowing for swatches with ::before fills. Same visual
 * as the row's aria-pressed=false slash treatment: transparent fill
 * + grey border so the toggled-off swatch reads as "outline only." */
.opt-toggle-row[aria-pressed="false"] .feature-swatch::before {
    background: transparent;
    border: 1.5px solid #9aa0a6;
    box-shadow: none;
}
.opt-toggle-row[aria-pressed="false"] .difficulty-swatch::before {
    background: transparent;
    border-color: #9aa0a6;
}

/* MapLibre attribution lives at bottom-left (added to the map via
 * map.addControl(..., "bottom-left") in app.js). Respect safe-area-
 * inset-bottom for the iOS home-bar area. Bottom-LEFT is currently
 * empty (brand is top-left, FAB stack is bottom-right); attribution
 * fits there cleanly without competing with any other chrome.
 *
 * Inset matches the brand element (top-left, 12 px) and the FAB
 * stack (bottom-right, 12 px) so all three corner elements sit the
 * same distance from their respective corners. The inner chip's
 * default 0 0 10 10 margin (set by MapLibre's own stylesheet) is
 * flattened so the visible chip edge IS the 12 px inset rather
 * than 12 + 10. */
.maplibregl-ctrl-bottom-left {
    bottom: calc(12px + var(--safe-bottom)) !important;
    left: calc(12px + env(safe-area-inset-left, 0)) !important;
}

.maplibregl-ctrl-bottom-left .maplibregl-ctrl {
    margin: 0 !important;
}

/* ============================================================
 * Desktop overrides for the floating overlays
 *
 * Mobile-first: at < 768px the search overlay is a full-width
 * half-sheet anchored to the bottom, and the options overlay is
 * full-screen. Both make sense on a phone.
 *
 * Desktop has plenty of horizontal space — full-width overlays
 * leave huge whitespace gutters around what's actually a narrow
 * column of content. Constrain to readable widths and centre.
 * ============================================================ */

@media (min-width: 768px) {
    /* Both overlays: centre the .justify-content axis so the panel
     * lands centred horizontally. (Vertical alignment is set per
     * overlay — search stays bottom-anchored, options stays
     * vertically-centred.) */
    .search-overlay,
    .options-overlay {
        align-items: center;
    }

    /* Both overlay panels target ~440px wide on desktop — narrow
     * enough that the label-on-left / control-on-right rows feel
     * "filled" (no awkward whitespace between the label and the
     * pill), wide enough to show full route names and meaningful
     * stats. Same width for both gives the two surfaces visual
     * symmetry when the user toggles between them. */
    .search-overlay-panel,
    .options-overlay-panel {
        width: min(440px, calc(100vw - 48px));
        border: 1px solid var(--sheet-border);
        border-radius: 14px;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.22);
    }

    /* Search panel — anchored ~24px above the bottom. */
    .search-overlay-panel {
        height: min(60dvh, 540px);
        margin-bottom: 24px;
    }

    /* Options panel — vertically-centred card. Override
     * justify-content on the overlay so it doesn't get pushed to
     * the bottom. */
    .options-overlay {
        justify-content: center;
    }
    .options-overlay-panel {
        flex: 0 1 auto;
        max-height: min(80dvh, 680px);
        /* Drop safe-area padding — panel doesn't touch the edges
         * so notches don't intersect it. */
        padding-top: 0;
        padding-bottom: 0;
    }
}

/* POI persistent highlight — rendered as MapLibre circle layers
 * (sandwich pattern: dark outer stroke + bright yellow inner stroke,
 * see ensurePoiHighlightLayers in app.js). No CSS needed; the
 * stroke colors and widths live on the layers themselves so they
 * stay aligned with the in-map geometry across pan/zoom. */

/* ============================================================
 * POI search rows (Phase 3 POI search)
 *
 * POI finder rows reuse .finder-row + .finder-row-name + .finder-row-meta
 * from the existing route/trail rows. The left-side swatch is a
 * .layer-swatch with the per-type modifier class (.parking-swatch,
 * .toilet-swatch, etc.) so visual style matches the on-map marker.
 * Sized down a touch from the Options-overlay 28px to fit the row's
 * height. .finder-row-meta carries the type label ("parking",
 * "toilets") so the rider can disambiguate similarly-named items.
 * ============================================================ */

.finder-row-poi-swatch {
    /* Reuse the .layer-swatch base styling but size it for the
     * finder row. 22px keeps the 14px-wide SVG / text glyph
     * comfortably centered. */
    width: 22px;
    height: 22px;
    flex: 0 0 auto;
    position: relative;  /* for ::before / ::after pseudo-content */
    overflow: visible;
    font-size: 11px;
}

/* Meta label sits right-aligned next to the name. Use auto-margin
 * to push it to the row's right edge — same visual rhythm as the
 * route row's stats span. Inherits font/colour from .finder-row-meta. */
.finder-row-poi .finder-row-meta {
    flex: 0 0 auto;
    margin-left: auto;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 10px;
    font-weight: 600;
}
