/* ─────────────────────────────────────────────────────────────────────────
   MOTO NAV — Rally roadbook aesthetic
   Dark, high-contrast UI optimized for outdoor / through-visor visibility.
   Amber accents, monospaced numerics, generous hit targets.
   ───────────────────────────────────────────────────────────────────────── */

:root {
  --bg-deep:      #0a0a0a;
  --bg-panel:     #141414;
  --bg-elev:      #1c1c1c;
  --bg-elev-hi:   #262626;
  /* Borders and secondary text lifted one Tailwind grey step for
     higher outdoor / through-visor legibility. Old values are kept
     as a comment for reference in case something looks too bright. */
  --line:         #404040;   /* was #2a2a2a (gray-750ish) → gray-700 */
  --line-hi:      #525252;   /* was #3a3a3a → gray-600 */
  --text:         #f5f5f4;
  --text-dim:     #d4d4d4;   /* was #a3a3a3 → gray-300, contrast 14:1 on bg-panel */
  --text-faint:   #a3a3a3;   /* was #525252 → gray-400, contrast 8:1 on bg-panel */
  --amber:        #f59e0b;
  --amber-hi:     #fbbf24;
  --amber-dim:    #78350f;
  --green:        #84cc16;
  --red:          #ef4444;
  --route-direct: #fbbf24;
  --route-adv:    #f97316;
  /* Foreground color used on top of amber accents (button text, modal
     titlebar text, marker borders). Stays dark across all themes so the
     amber stays readable regardless of the page background. */
  --on-amber:     #0a0a0a;

  --font-display: 'Space Grotesk', system-ui, sans-serif;
  --font-mono:    'JetBrains Mono', ui-monospace, monospace;

  --radius:       4px;
  --radius-lg:    8px;
  --shadow:       0 10px 30px rgba(0,0,0,0.6), 0 2px 8px rgba(0,0,0,0.4);
}

/* ─────────────────────────────────────────────────────────────────────────
   Light theme — same rally-roadbook layout, inverted palette. Amber stays
   the signature accent; page background flips to off-white with a clear
   information hierarchy in stone-gray text. Map style is unchanged and
   remains dark, which works well as a "screen" inside the light chrome.
   Activated by adding `theme-light` to the <html> element.
   ───────────────────────────────────────────────────────────────────────── */
:root.theme-light {
  --bg-deep:      #fafaf9;
  --bg-panel:     #ffffff;
  --bg-elev:      #f5f5f4;
  --bg-elev-hi:   #e7e5e4;
  --line:         #e7e5e4;
  --line-hi:      #a8a29e;
  --text:         #1c1917;
  --text-dim:     #57534e;
  /* Lifted one stone step (was #a8a29e ≈ stone-400; contrast on white
     was only 2.7:1 — failed AA for body text). Now stone-500 at ~5:1
     so faint captions / metadata stay readable in sunlight too. */
  --text-faint:   #78716c;
  --amber:        #f59e0b;
  --amber-hi:     #d97706;
  --amber-dim:    #fde68a;
  --green:        #65a30d;
  --red:          #dc2626;
  --route-direct: #d97706;
  --route-adv:    #b45309;
  --on-amber:     #1c1917;
  --shadow:       0 4px 14px rgba(15,12,10,0.08), 0 1px 4px rgba(15,12,10,0.04);
}

* { box-sizing: border-box; }
/* Following PizzaTool's pattern: html/body in normal flow with no
   position: fixed. `min-height: 100dvh` lets the body genuinely fill the
   physical screen on iOS PWA (where the dvh unit DOES expand into the
   safe-area band even though `position: fixed` containers don't).
   `overflow: hidden` on body keeps the app feel — no rubber-band scroll —
   without putting position: fixed on the root. */
html, body {
  margin: 0;
  padding: 0;
}
html {
  background: var(--bg-deep);
  /* Disable scrolling entirely — Moto Nav is map-pan, not page-pan. The
     map element handles its own gestures via MapLibre. overscroll-behavior
     also kills the iOS rubber-band / pull-to-refresh effect. */
  overflow: hidden;
  overscroll-behavior: none;
  height: 100%;
}
body {
  width: 100%;
  /* Layered fallback. iOS PWA initial-paint dvh is buggy short; --app-h
     comes from the inline script reading screen.height (physical screen)
     so it stays honest. */
  min-height: 100vh;
  min-height: 100dvh;
  min-height: var(--app-h, 100dvh);
  overflow: hidden;
  overscroll-behavior: none;
  /* touch-action: none on body blocks the browser's default touch
     handling (drag-scroll, pinch-zoom-on-page). MapLibre attaches its
     own touch listeners on its canvas and isn't affected. */
  touch-action: none;
  font-family: var(--font-display);
  background: var(--bg-deep);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  /* App-style: no text caret on stray clicks, no iOS grey tap-flash.
     Selectable elements (search inputs, the beta-gate code input, the
     address-picker input, the settings address pickers, etc.) opt
     back in via the input/textarea override below. */
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
/* Re-enable selection + the I-beam caret on actual text-entry surfaces
   so the rider can still type, select, and paste into them. */
input, textarea, [contenteditable="true"] {
  -webkit-user-select: text;
  -moz-user-select: text;
  -ms-user-select: text;
  user-select: text;
}

button, input { font-family: inherit; color: inherit; }

/* `position: relative` (NOT fixed) — this is the key change for iOS PWA.
   Children with position: absolute reference #app's box, which is a
   normal-flow block sized by `min-height: 100dvh`. In iOS PWA standalone
   mode, `100dvh` resolves to the FULL physical screen (including the
   home-indicator band), while `position: fixed` uses a smaller initial
   containing block. So absolute children of #app reach the true bottom;
   fixed children would not. */
#app {
  position: relative;
  width: 100%;
  min-height: 100vh;
  min-height: 100dvh;
  min-height: var(--app-h, 100dvh);
}

/* ───── Map ───── */
/* `position: absolute` relative to #app (which is min-height: 100dvh and
   so reaches the full physical screen on iOS PWA). inset: 0 fills #app
   edge-to-edge — including the bottom safe-area band that the old
   `position: fixed` setup couldn't reach. */
#map {
  position: absolute;
  inset: 0;
  background: #0a0a0a;
  z-index: 0;
}

/* Top-edge blur strip — iOS "glass" behind the topbar chrome. Sits
   between the map (z 0) and the topbar (z 10). Height covers the
   safe-area inset (notch / Dynamic Island band) + the 40 px FAB row
   + a soft fade region below.
   The mask gradient makes the blur "physically" present from the top
   edge down to ~70 % of the strip, then fades to nothing so the
   transition into the unblurred map below doesn't show a hard line.
   pointer-events: none so map pans pass straight through. */
.map-top-blur {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: calc(100px + env(safe-area-inset-top, 0px));
  z-index: 1;
  pointer-events: none;
  backdrop-filter: blur(10px) saturate(120%);
  -webkit-backdrop-filter: blur(10px) saturate(120%);
  /* Soft gradient mask: opaque (= blur applied) up top, fading to
     transparent at the bottom of the strip. Both `mask-image` and the
     -webkit- prefix so Safari renders it. */
  mask-image: linear-gradient(to bottom,
    rgba(0,0,0,1) 0%,
    rgba(0,0,0,1) 40%,
    rgba(0,0,0,0) 100%);
  -webkit-mask-image: linear-gradient(to bottom,
    rgba(0,0,0,1) 0%,
    rgba(0,0,0,1) 40%,
    rgba(0,0,0,0) 100%);
}
/* Strip stays visible in ride mode too — the pause / theme-toggle /
   recenter FABs along the top still benefit from the glass backing,
   and the consistent silhouette across both modes feels more
   intentional than a strip that pops in and out. */

/* Recolor MapLibre attribution to fit theme */
.maplibregl-ctrl-attrib {
  background: rgba(20, 20, 20, 0.7) !important;
  color: var(--text-dim) !important;
}

/* OSM attribution overlay. Tiny credit pinned to the top-right of
   the map, slotted just under the recenter FAB so it reads as a
   small footnote rather than a hero element. Held at 0.4 opacity
   (60% transparent per design call) so it doesn't compete with
   the actual UI; lifts to full opacity on hover for legibility.
   Hidden in ride mode to declutter the HUD — the Settings → About
   footer still carries the credit when riding. */
.map-attribution {
  position: fixed;
  right: calc(16px + env(safe-area-inset-right, 0px));
  top: calc(64px + env(safe-area-inset-top, 0px));
  z-index: 5;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  text-decoration: none;
  padding: 2px 6px;
  background: rgba(20, 20, 20, 0.55);
  border-radius: 3px;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  opacity: 0.4;
  transition: opacity 200ms, color 120ms;
  pointer-events: auto;
}
.map-attribution:hover { opacity: 1; color: var(--amber); }
html.theme-light .map-attribution {
  background: rgba(255, 255, 255, 0.75);
  color: var(--text-dim);
}
/* Hide while the rider is in ride mode — at speed they don't need
   chrome on the map, and the Settings footer still carries the
   credit. Restored automatically on exitRide. */
body.ride-active .map-attribution { display: none; }
.maplibregl-ctrl-attrib a { color: var(--amber) !important; }

/* Desktop: move the attribution to the bottom-right corner — the
   canonical position for OSM credit on map apps. Mobile keeps the
   top-right placement (under the topbar) because the bottom corners
   are needed for the planner panel + FABs at narrow widths. Same
   gate as the desktop topbar wordmark + FAB row (#43): pointer +
   hover excludes phone-landscape, so the override only kicks in on
   a real desktop. */
@media (min-width: 801px) and (min-height: 501px) and (hover: hover) and (pointer: fine) {
  .map-attribution {
    top: auto;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    right: calc(16px + env(safe-area-inset-right, 0px));
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   Top bar
   ───────────────────────────────────────────────────────────────────────── */
.topbar {
  position: absolute;
  /* Push the topbar inward by the device's safe-area insets so UI sits
     clear of the iOS notch / Dynamic Island, while the map itself still
     extends edge-to-edge underneath (since #map uses inset: 0). */
  top:   calc(16px + env(safe-area-inset-top, 0px));
  left:  calc(16px + env(safe-area-inset-left, 0px));
  right: calc(16px + env(safe-area-inset-right, 0px));
  display: grid;
  /* Columns: burger | theme-toggle | search (1fr) | recenter. */
  grid-template-columns: auto auto 1fr auto;
  gap: 12px;
  align-items: center;
  z-index: 10;
  pointer-events: none;
  /* Smooth fade when entering / exiting ride mode (body.ride-active). */
  transition: opacity 350ms ease;
}
/* Pin each topbar child to its template column explicitly. Without
   these, when the theme toggle is `display: none` (mobile planning
   mode) the remaining items auto-flow LEFT — search lands in column
   2 (auto) instead of column 3 (1fr), and recenter inherits the 1fr
   expansion it doesn't need. Pinning keeps search anchored to the
   1fr column regardless of which siblings are visible. The ride-mode
   override `body.ride-active .theme-toggle { grid-column: 1 }` has
   higher specificity, so it still wins when active. */
.topbar > .brand-wrap   { grid-column: 1; }
.topbar > .theme-toggle { grid-column: 2; }
.topbar > .search       { grid-column: 3; }
.topbar > #recenter     { grid-column: 4; }
/* Desktop-only action clusters. On mobile the .topbar-actions
   containers are hidden via display: none (see the @media block
   below) so the 4-column mobile grid is unaffected. The desktop
   media query rewrites grid-template-columns to accommodate them. */
.topbar > .topbar-actions { display: none; }

/* Brand button content swap — burger on mobile, motorsport wordmark
   on desktop. Both elements live inside #brand-button at all times;
   CSS just toggles which is visible. Default state (mobile) shows
   burger, hides wordmark; the desktop media query inverts it. */
.brand-burger   { display: block; }
.brand-wordmark { display: none; }

/* Theme toggle (standalone button) — show the sun in dark theme
   (tap → light), show the moon in light theme (tap → dark).
   Same icon-swap logic applies to the in-menu version (.menu-theme-icon)
   below — both selectors live in this block so they stay in sync. */
.theme-toggle .icon-sun,
.menu-theme-icon .icon-sun { display: block; }
.theme-toggle .icon-moon,
.menu-theme-icon .icon-moon { display: none; }
html.theme-light .theme-toggle .icon-sun,
html.theme-light .menu-theme-icon .icon-sun { display: none; }
html.theme-light .theme-toggle .icon-moon,
html.theme-light .menu-theme-icon .icon-moon { display: block; }

/* Menu theme item — label spans switch in lock-step with the icons.
   "Light mode" is shown when currently dark (tap → light); "Dark mode"
   when currently light. */
.menu-theme-label--light { display: inline; }
.menu-theme-label--dark  { display: none; }
html.theme-light .menu-theme-label--light { display: none; }
html.theme-light .menu-theme-label--dark  { display: inline; }
/* Menu theme item is only shown on mobile — desktop keeps the
   standalone topbar button. Compound selector `.menu-item.menu-theme`
   beats the generic `.menu-item { display: flex }` declared later in
   the file, otherwise the rule below loses on source order. */
.menu-item.menu-theme { display: none; }
/* Icon goes BEFORE the label. The generic `.menu-item .menu-icon` rule
   below hides all menu icons (placeholder for future styling) and
   pushes them to the right via `order: 2`. We un-hide it just for the
   theme item and reset order / margin so the sun/moon sits to the
   left of the text. */
.menu-item.menu-theme .menu-icon {
  display: inline-flex;
  align-items: center;
  order: 0;
  margin-left: 0;
  width: auto;
  color: var(--text-dim);
}
@media (max-width: 800px) {
  .menu-item.menu-theme { display: flex; }
  /* On mobile + planning mode, hide the standalone topbar theme
     toggle so the in-menu item is the only theme control. In ride
     mode we still want the standalone (it's the only menu we can
     reach, since the burger is faded). */
  body:not(.ride-active) .theme-toggle { display: none; }
}

/* ─────────────────────────────────────────────────────────────────────
   Desktop topbar: expose menu actions as inline icon buttons.
   Mobile keeps the burger-menu pattern (handled above + by hiding
   .topbar-actions in the base rules). On desktop:
     ─ #brand-button swaps the burger glyph for the motorsport SPORR
       wordmark (visual identity only — dropdown is suppressed)
     ─ #brand-menu is force-hidden so a stray click on the brand
       doesn't open a panel whose entries are duplicated inline
     ─ .topbar-actions-left  (Load · Save · Import) shows between
       theme-toggle and the search input
     ─ .topbar-actions-right (Settings · Feedback · Sign out) shows
       to the right of recenter
   Grid columns are rewritten to accommodate all six new pills while
   the search keeps its 1fr expansion in the middle.
   ───────────────────────────────────────────────────────────────────── */
/* `hover: hover` + `pointer: fine` filter out touch devices — even
   when an iPhone in landscape is wider than 801 px, Safari reports
   `hover: none, pointer: coarse` and stays in the mobile burger
   layout. iPad with a real trackpad reports hover/fine and gets the
   desktop layout, which is the intended behaviour.
   `min-height: 501 px` also gates the desktop layout — a short
   desktop window (≤ 500 px tall) collapses to the mobile-style
   burger row (burger + search + recenter), with everything else
   reachable via the burger dropdown. Mirrors the landscape-phone
   collapse so a chrome window dragged short for testing, or a
   ChromeOS clamshell in a side-by-side window split, picks up the
   same compact layout. */
@media (min-width: 801px) and (min-height: 501px) and (hover: hover) and (pointer: fine) {
  .topbar {
    grid-template-columns:
      auto       /* brand wordmark        */
      auto       /* theme toggle          */
      auto       /* load / save / import  */
      1fr        /* search                */
      auto       /* recenter              */
      auto;      /* settings / feedback / logout */
    column-gap: 12px;
  }
  .topbar > .brand-wrap            { grid-column: 1; }
  .topbar > .theme-toggle          { grid-column: 2; }
  .topbar > .topbar-actions-left   { grid-column: 3; display: inline-flex; gap: 8px; }
  .topbar > .search                { grid-column: 4; }
  .topbar > #recenter              { grid-column: 5; }
  .topbar > .topbar-actions-right  { grid-column: 6; display: inline-flex; gap: 8px; }

  /* Swap the brand button's content. Wordmark inside the same pill,
     so the row height + alignment are unchanged. */
  .brand-burger   { display: none; }
  .brand-wordmark { display: inline-flex; align-items: center; }

  /* Hide the burger dropdown entirely on desktop — all entries are
     now exposed as icon buttons, so the dropdown is redundant. The
     click handler in app.js still toggles aria-expanded but the
     panel never paints. */
  #brand-menu { display: none !important; }

  /* Brand button presentation when showing the wordmark. On desktop
     the pill chrome (background, border, shadow) is stripped — the
     wordmark floats as pure brand chrome over the map, with its own
     text-shadow giving it depth instead of relying on a card. The
     base .brand rule pins width: 44 px (burger square); override to
     auto so the wordmark gets its natural width. */
  .brand-wrap .brand {
    width: auto;
    min-width: 0;
    padding: 0 8px;
    background: transparent;
    border: none;
    box-shadow: none;
    font-family: var(--font-display);
    font-weight: 700;
    letter-spacing: 0.06em;
    line-height: 1;
    color: var(--text);
    /* No dropdown, no destination — the brand pill is pure visual
       identity on desktop. Strip the interactive affordances the
       base .brand class carries (pointer cursor, hover recolour,
       aria-expanded amber outline) so it reads as a logo, not a
       button. Focus ring stays via the browser default for keyboard
       a11y; tabbing past it does nothing harmful. */
    cursor: default;
    transition: none;
  }
  .brand-wrap .brand:hover,
  .brand-wrap .brand[aria-expanded="true"],
  .brand-wrap .brand:active {
    background: transparent;
    border: none;
    box-shadow: none;
    color: var(--text);
  }

  /* Drop-shadow on the desktop action buttons (Load · Save · Import ·
     Settings · Feedback · Sign out). The pill chrome stays the same
     panel colour + 1 px border; the shadow lifts the cluster off the
     map underneath so it reads as one floating control strip rather
     than a transparent overlay. Same shadow recipe as the rest of
     the floating UI surfaces. */
  .topbar > .topbar-actions .topbar-fab {
    box-shadow: var(--shadow);
  }
}

/* Labels on the desktop topbar action buttons (Load / Save / Import /
   Settings / Feedback / Sign out). Hidden by default at every
   breakpoint; the wide-desktop @media block (≥ 1601 px) widens the
   pill, shows the label inline next to the icon, and adds a small
   gap. Below 1601 px the buttons stay icon-only with title +
   aria-label carrying the same text. */
.topbar-fab-label {
  display: none;
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

@media (min-width: 1601px) and (hover: hover) and (pointer: fine) {
  .topbar-actions .topbar-fab {
    width: auto;
    padding: 0 14px;
    gap: 8px;
  }
  .topbar-actions .topbar-fab-label {
    display: inline;
  }
  /* Sign out stays icon-only regardless of viewport width. It's the
     destructive action of the cluster and shouldn't carry the same
     visual weight as the everyday route I/O / settings buttons.
     Re-pin the square geometry the base .topbar-fab rule defines
     and force the label back to display:none even inside this wide-
     viewport block. */
  #topbar-logout {
    width: 44px;
    padding: 0;
  }
  #topbar-logout .topbar-fab-label {
    display: none;
  }
}

/* Motorsport wordmark inside the brand button — Russo One isn't
   loaded in the app shell, so we use the existing Space Grotesk at
   heavy weight with the same -6° skew + amber RR pair to evoke the
   landing-page mark without a second web font request. Bumped from
   16 → 22 px now that the pill chrome is stripped on desktop — the
   wordmark is the only visual weight in that slot and needs to read
   as the brand, not a generic label. The text-shadow lifts it off
   the map underneath so it doesn't dissolve into a busy raster
   background. */
.brand-wordmark {
  /* Motorsport variant — Russo One with a -6° skew, identical to
     the landing page hero + beta gate. Was previously rendering in
     Space Grotesk 700 with a generic letter-spacing, which read as
     a bold label rather than the brand mark. Russo One is already
     loaded via the same Google Fonts link as the rest of the app
     so there's no extra request. font-weight: 400 because Russo One
     only ships a single weight; bumping that to 700 falls back to
     the synthetic-bold the browser invents, which looks chunky. */
  font-family: 'Russo One', var(--font-display), sans-serif;
  font-weight: 400;
  letter-spacing: 0.01em;
  font-size: 22px;
  line-height: 1;
  transform: skewX(-6deg);
  text-transform: uppercase;
  text-shadow:
    0 2px 6px rgba(0, 0, 0, 0.45),
    0 1px 2px rgba(0, 0, 0, 0.30);
}
.brand-wordmark-rr {
  color: var(--amber);
}

.topbar > * { pointer-events: auto; }

/* Ride mode: fade out everything in the topbar EXCEPT the theme toggle.
   The rider should still be able to flip dark↔light on the fly (helpful
   when going through a tunnel or moving from sun to shade). The fade is
   applied per-child rather than to the whole .topbar element so the
   theme toggle isn't dragged along to opacity 0. Pointer events on the
   faded children are dropped so invisible search/menu/recenter pills
   don't catch stray taps. */
.topbar > * {
  transition: opacity 350ms ease;
}
body.ride-active .topbar > *:not(.theme-toggle) {
  opacity: 0;
  pointer-events: none;
}

.brand-wrap { position: relative; }

/* Shared height for everything in the topbar so brand, search and recenter
   all sit as one consistent bar. Padding is horizontal-only since the
   fixed height defines the vertical dimension. */
.topbar > * > .brand,
.topbar .brand,
.topbar .search input,
.topbar-fab {
  height: 44px;
  box-sizing: border-box;
}

/* Burger-menu button. Square icon-only button matching the recenter FAB
   on the right so the topbar reads as two balanced controls flanking
   the search field. Height comes from the shared topbar-row selector
   above (44 px desktop, 40 px mobile). */
.brand {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  padding: 0;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--text-dim);
  cursor: pointer;
  box-shadow: var(--shadow);
  font-family: inherit;
  transition: color 200ms, border-color 200ms, box-shadow 200ms;
}
.brand:hover { color: var(--text); border-color: var(--line-hi); }
.brand[aria-expanded="true"] {
  background: var(--bg-elev);
  border-color: var(--amber);
  color: var(--amber);
}

/* Dropdown menu anchored under the brand button. Only displayed when the
   hidden attribute is removed by the toggle handler. */
.brand-menu[hidden] { display: none; }
.brand-menu {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  min-width: 200px;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
  z-index: 20;
}
.menu-item {
  background: transparent;
  border: none;
  color: var(--text);
  padding: 10px 12px;
  text-align: left;
  cursor: pointer;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: inherit;
  /* Force left-alignment even for .file-btn-styled labels — without this the
     Import GPX label gets centered by the legacy .file-btn rule. */
  justify-content: flex-start;
  width: 100%;
  box-sizing: border-box;
}
.menu-item:hover:not(:disabled) { background: var(--bg-elev-hi); }
.menu-item:disabled { opacity: 0.4; cursor: not-allowed; }
.menu-item .menu-icon {
  /* SVG icons sit BEFORE the label, vertically centred with it.
     The icons themselves are 16 × 16 with stroke="currentColor" so
     they pick up the menu-item's text colour (dim by default, full
     contrast on hover via the parent rule's color: var(--text)). */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  color: var(--text-dim);
}
.menu-item:hover:not(:disabled) .menu-icon { color: var(--text); }
.menu-item:disabled .menu-icon { color: var(--text-faint); }
.menu-sep { height: 1px; background: var(--line); margin: 4px 2px; }

/* Live GPS status row at the top of the brand menu. Dot turns green when
   a fix is active, red on error, neutral grey while waiting. */
.menu-status {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--text-dim);
}
.menu-status-dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--text-faint);
  transition: background 200ms, box-shadow 200ms;
  flex: 0 0 auto;
}
.menu-status.active .menu-status-dot {
  background: var(--green);
  box-shadow: 0 0 6px var(--green);
}
.menu-status.error .menu-status-dot {
  background: var(--red);
  box-shadow: 0 0 6px var(--red);
}
.menu-status-text { white-space: nowrap; }

/* Footer of the burger menu: small sporr wordmark on the left,
   build-version chip on the right. Non-interactive — meant to be
   ignorable chrome you only notice when you go looking for it. */
.menu-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 6px 14px 4px;
  gap: 12px;
  pointer-events: none;
  user-select: none;
}
.menu-foot-brand {
  /* Motorsport wordmark — Russo One, all-caps, slight racing skew. The
     HTML ships SPORR in caps, so no text-transform needed; the −6°
     skewX gives it the "in motion" lean used across the rest of the
     identity. Tracking opens a touch since Russo One's caps are
     dense at small sizes. */
  font-family: 'Russo One', var(--font-display), sans-serif;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--text-dim);
  line-height: 1;
  display: inline-block;
  transform: skewX(-6deg);
}
.menu-foot-rr { color: var(--amber); }
/* The version chip itself — small mono, faint colour. Populated by
   app.js from import.meta.url's ?v= query so it always tracks the
   current cache-bust version. */
.menu-version {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--text-faint);
}

.search { position: relative; }
.search input {
  width: 100%;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0 16px;
  /* iOS Safari auto-zooms the whole page whenever an input is focused and
     its font-size is below 16px. Keeping it at 16px here disables that
     behaviour without needing a viewport user-scalable=no override. */
  font-size: 16px;
  color: var(--text);
  outline: none;
  box-shadow: var(--shadow);
  transition: border-color 0.15s;
}
.search input:focus { border-color: var(--amber); }
.search input::placeholder { color: var(--text-faint); }

/* Recolor Chrome/Edge's built-in <input type="search"> clear button to
   match our neutral chrome. The user-agent default is system-blue,
   which reads as a foreign accent against the amber-on-grey palette.
   Applies globally to every type="search" field — currently the topbar
   planner search and the Home/Work address picker in Settings.
   Inlined SVG data URI because :pseudo elements can't read CSS vars. */
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  margin-left: 8px;
  cursor: pointer;
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23a3a3a3' stroke-width='2' stroke-linecap='round'><line x1='4' y1='4' x2='12' y2='12'/><line x1='12' y1='4' x2='4' y2='12'/></svg>") center / contain no-repeat;
  opacity: 0.85;
}
input[type="search"]::-webkit-search-cancel-button:hover { opacity: 1; }

.search-results {
  position: absolute;
  top: calc(100% + 6px); left: 0; right: 0;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  list-style: none;
  margin: 0; padding: 4px;
  max-height: 320px;
  overflow-y: auto;
  box-shadow: var(--shadow);
}
.search-results li {
  padding: 10px 12px;
  cursor: pointer;
  border-radius: 2px;
  font-size: 14px;
  color: var(--text-dim);
  line-height: 1.4;
  display: flex;
  align-items: center;
  gap: 10px;
}
.search-results li:hover, .search-results li.active {
  background: var(--bg-elev);
  color: var(--text);
}
.search-results li small { display: block; color: var(--text-faint); font-size: 11px; margin-top: 2px; }
.search-results .result-info { flex: 1; min-width: 0; }
.search-results .result-name { display: block; }
.search-results .result-info small { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-results .result-add {
  flex: 0 0 auto;
  width: 32px; height: 32px;
  background: var(--bg-elev);
  color: var(--amber);
  border: 1px solid var(--line);
  border-radius: 50%;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  transition: background 120ms;
}
.search-results li:hover .result-add,
.search-results li.active .result-add { background: var(--amber); color: var(--on-amber); border-color: var(--amber); }
/* Favourite variant — used by the Settings → Favourites picker.
   Matches the planner search results' `.result-favourite` look: grey
   outline at rest, amber-tinted outline ring on hover (NOT inverted
   amber fill). Same visual vocabulary as the star buttons that sit on
   live search results / recents, so adding from either surface feels
   like the same affordance. */
.search-results .result-add--favourite {
  background: transparent;
  color: var(--text-faint);
}
.search-results .result-add--favourite > svg {
  width: 16px; height: 16px; display: block;
}
.search-results li:hover .result-add--favourite,
.search-results li.active .result-add--favourite,
.search-results .result-add--favourite:hover {
  background: rgba(245, 158, 11, 0.10);
  color: var(--amber);
  border-color: var(--amber);
}
.search-results .result-add--favourite:active {
  background: rgba(245, 158, 11, 0.18);
  color: var(--amber);
  border-color: var(--amber);
}
/* Section heading inside the search-results dropdown — e.g. the
   "Recent destinations" caption above the first recent row. Visually
   a label, not a row: smaller, dimmer, uppercase, no hover. */
.search-results .result-section {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding: 10px 12px 4px;
  cursor: default;
}
.search-results .result-section:hover {
  background: transparent;
  color: var(--text-faint);
}

/* Home / Work shortcuts row pinned above "Recent destinations" when
   the search input is empty. Two side-by-side cells in a 1fr 1fr
   grid; cells are squared, borderless, grey (text-dim) so they read
   as quiet utility chrome and don't compete with the amber accents
   reserved for active route + active state. When a place isn't
   saved the cell becomes "Set home" / "Set work" (faint italic) and
   the tap deep-links into the Settings picker for that key.
   See setupSearch() in app.js for the rendering. */
.search-shortcuts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  padding: 4px;
  margin: 0 0 4px;
  list-style: none;
}
.search-shortcut {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 5px 10px;
  background: transparent;
  border: none;
  border-radius: 8px;
  color: var(--text-dim);
  cursor: pointer;
  font-family: inherit;
  transition: background 120ms, color 120ms;
  min-width: 0;
}
.search-shortcut:hover {
  background: var(--bg-elev);
  color: var(--text);
}
.search-shortcut-icon {
  color: var(--text-dim);
  display: inline-flex;
  flex-shrink: 0;
}
.search-shortcut:hover .search-shortcut-icon { color: var(--text); }
.search-shortcut-label {
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Unset state — no Home/Work saved yet. Reads as visually disabled
   (faint, dimmed) but is still tappable: the click opens Settings
   straight into the Home/Work picker for that key, so a tester can
   set the address from one place rather than digging through menus.
   No italic — the look is "greyed out", not "placeholder text". */
.search-shortcut.unset { opacity: 0.55; }
.search-shortcut.unset .search-shortcut-label,
.search-shortcut.unset .search-shortcut-icon { color: var(--text-faint); }
.search-shortcut.unset:hover { opacity: 1; background: var(--bg-elev); }
.search-shortcut.unset:hover .search-shortcut-label,
.search-shortcut.unset:hover .search-shortcut-icon { color: var(--text-dim); }
/* Per-row delete button on recent destinations. Same circular shape
   as `.result-add` but transparent / faint by default so it doesn't
   compete with the "Recent" pill or the row's primary tap target.
   Red tint on hover to make the destructive nature obvious. Click
   handler in app.js stops propagation so removing doesn't also pick. */
.search-results .result-remove {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  background: transparent;
  color: var(--text-faint);
  border: 1px solid var(--line);
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: background 120ms, color 120ms, border-color 120ms;
}
/* Hover state gated on real hover capability — on touch devices iOS
   Safari "sticks" :hover to the element under the touch point after
   a tap, which meant tapping the trash to delete a recent left a
   red highlight on the NEXT row's trash (because rows shifted up
   after the re-render). With (hover: hover) the rule only applies
   to mice / trackpads, so phone taps no longer paint a phantom
   hover. Active state still gives a visible red press feedback. */
@media (hover: hover) {
  .search-results .result-remove:hover {
    color: var(--red);
    border-color: var(--red);
    background: rgba(220, 38, 38, 0.10);
  }
}
.search-results .result-remove:active {
  color: var(--red);
  border-color: var(--red);
  background: rgba(220, 38, 38, 0.18);
}

/* Favourite-toggle button on a result row. Same 32 px circle as the
   "+" used to be — sits in the same slot, so on live search rows the
   star fully replaces the old "Add as waypoint" plus button. On
   recents rows the star + trash sit side by side (star to the left
   of trash). Outline glyph when not yet favourited; filled amber when
   already in the user's favourites list. */
.search-results .result-favourite {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  background: transparent;
  color: var(--text-faint);
  border: 1px solid var(--line);
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: background 120ms, color 120ms, border-color 120ms;
}
.search-results .result-favourite > svg {
  width: 16px; height: 16px; display: block;
}
@media (hover: hover) {
  .search-results .result-favourite:hover {
    color: var(--amber);
    border-color: var(--amber);
    background: rgba(245, 158, 11, 0.10);
  }
}
.search-results .result-favourite:active {
  color: var(--amber);
  border-color: var(--amber);
  background: rgba(245, 158, 11, 0.18);
}
/* Filled state — currently a favourite. Amber glyph stands out
   against the dim row background so the user can scan the recents
   dropdown and spot what's already saved. */
.search-results .result-favourite.on {
  color: var(--amber);
  border-color: rgba(245, 158, 11, 0.45);
  background: rgba(245, 158, 11, 0.08);
}
@media (hover: hover) {
  .search-results .result-favourite.on:hover {
    color: var(--amber);
    border-color: var(--amber);
    background: rgba(245, 158, 11, 0.18);
  }
}

.status {
  display: flex; align-items: center; gap: 8px;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  padding: 10px 14px;
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  box-shadow: var(--shadow);
}
.status-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--text-faint);
  transition: background 0.3s, box-shadow 0.3s;
}
.status.active .status-dot {
  background: var(--green);
  box-shadow: 0 0 8px var(--green);
}
.status.error .status-dot { background: var(--red); box-shadow: 0 0 8px var(--red); }

/* ─────────────────────────────────────────────────────────────────────────
   Bottom panel
   ───────────────────────────────────────────────────────────────────────── */
.panel {
  /* `position: absolute` inside #app. Because #app uses min-height: 100dvh,
     its bottom edge is the actual physical screen bottom on iOS PWA, so
     bottom: 0 (plus a safe-area inset) lands where we want. */
  position: absolute;
  /* Inset by safe-area on iOS so the panel sits above the home indicator. */
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  left:   calc(16px + env(safe-area-inset-left, 0px));
  /* See html.android-pwa override at the bottom of this rule's
     siblings — Android PWA standalone mode needs a floor on the
     bottom inset because Chrome reports safe-area-inset-bottom as 0
     for many gesture-nav phones, letting the system nav bar overlap
     the Start ride button. */
  width: 440px;
  max-height: calc(100dvh - 96px);
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  z-index: 5;
}

.panel-section { padding: 14px 16px; border-bottom: 1px solid var(--line); }
.panel-section:last-child { border-bottom: none; }

/* ─────────────────────────────────────────────────────────────────────────
   Android PWA — bottom-inset floor
   ─────────────────────────────────────────────────────────────────────────
   When Sporr is installed as a PWA on Android ("Add to home screen")
   and launched in standalone mode, Chrome reports
   safe-area-inset-bottom as 0 on many devices even with
   viewport-fit=cover. The system nav bar (gesture pill ~24 px,
   3-button nav ~48 px) then overlaps anything anchored to the bottom
   of the viewport — most visibly the Start ride button.
   The `android-pwa` class is set in app.js (isAndroidStandalone() in
   boot()). iOS NEVER gets this class — iPhone behaviour is unchanged.
   We use `max(env(...), 48px)` so devices that DO report the inset
   honestly still get the OS-reported value (48 px is just a floor for
   the broken case — covers 3-button nav, well above gesture-pill
   needs).
   Affected elements: `.panel` (planner, owns Start ride), `.nav-hud`
   (ride HUD, owns the pause + recenter pills + data card). The
   landscape ride-card media query has its own override further down. */
html.android-pwa .panel {
  bottom: calc(16px + max(env(safe-area-inset-bottom, 0px), 48px));
}
html.android-pwa .nav-hud {
  bottom: calc(16px + max(env(safe-area-inset-bottom, 0px), 48px));
}

.panel-label {
  display: flex; justify-content: space-between; align-items: baseline;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 10px;
}
.panel-hint { color: var(--text-faint); font-size: 9px; text-transform: none; letter-spacing: 0.05em; }

.slider-ticks span {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  white-space: nowrap;
}

/* Solid pill that sits just above the planner. Visible while there are no
   user waypoints; fades out when the first one is added.
   --panel-h is updated from JS (ResizeObserver on .panel) so the offset
   tracks the planner's real height regardless of viewport or content. */
.map-hint {
  position: absolute;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.04em;
  white-space: nowrap;
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  transition: opacity 350ms ease;
  box-shadow: var(--shadow);

  /* Desktop default: panel sits at left:16, width 440, anchored 16px from
     bottom (plus iOS safe-area). Centre the hint over the panel's
     horizontal middle and 10 px above its top edge. */
  bottom: calc(16px + env(safe-area-inset-bottom, 0px) + var(--panel-h, 320px) + 10px);
  left:   calc(16px + env(safe-area-inset-left,   0px) + 220px);
  transform: translateX(-50%);
}
.map-hint.visible { opacity: 1; }
@media (max-width: 800px) {
  /* Mobile: panel is full-width at the bottom; centre on viewport and
     float 10 px above its top edge. */
  .map-hint {
    bottom: calc(var(--panel-h, 50dvh) + 10px);
    left: 50%;
  }
}

/* Recenter button when placed in the topbar (replaces the old GPS status
   pill). Reuses the .fab base style but adds glow rings that mirror the
   GPS state classes we used to put on the status pill. */
.topbar-fab {
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--text-dim);
  font-size: 18px;
  cursor: pointer;
  width: 44px;
  padding: 0;
  transition: color 200ms, border-color 200ms, box-shadow 200ms;
}
.topbar-fab:hover { color: var(--text); border-color: var(--line-hi); }
/* Pressed/active state — used by the recenter button when planner-
   mode "follow my GPS" is on. Amber border + amber icon so the
   toggle reads as engaged at a glance. */
.topbar-fab[aria-pressed="true"] {
  color: var(--amber);
  border-color: var(--amber);
  box-shadow: 0 0 0 1px var(--amber);
}

/* Header row for the waypoints section — toggle + Clear sit side-by-side
   on the left, count on the right. */
.waypoints-header {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.waypoint-count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 700;
  color: var(--text-faint);
  letter-spacing: 0.05em;
  white-space: nowrap;
}

/* Waypoint list */
/* Waypoint list: shows up to 3 rows before scrolling. Each row is
   ≈ 22 (marker) + 16 (vertical padding) + 1 (dashed border) = 39 px, so
   3 rows = 117 px. We use 132 px to be safe across browsers/fonts so the
   scrollbar truly never appears at 3 waypoints. */
.waypoint-list {
  list-style: none;
  margin: 0;
  padding: 0;
  /* Was 132 px (≈ 3 rows). Trimmed to ~2 rows + a small peek of the
     next so longer routes scroll without dominating the planner
     panel. Each row is ~44 px tall (22 px marker + 8 px top/bottom
     padding + dashed divider). */
  max-height: 88px;
  overflow-y: auto;
  /* Firefox */
  scrollbar-width: thin;
  scrollbar-color: var(--line-hi) transparent;
}
/* WebKit (Chrome, Safari, Edge) */
.waypoint-list::-webkit-scrollbar { width: 6px; }
.waypoint-list::-webkit-scrollbar-track { background: transparent; }
.waypoint-list::-webkit-scrollbar-thumb {
  background: var(--line-hi);
  border-radius: 3px;
}
.waypoint-list::-webkit-scrollbar-thumb:hover { background: var(--amber); }
.waypoint-list li {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 0;
  font-size: 15px;
  color: var(--text-dim);
  border-bottom: 1px dashed var(--line);
}
.waypoint-list li:last-child { border-bottom: none; }
.waypoint-list .wp-marker {
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  background: var(--amber);
  color: var(--on-amber);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 11px;
  border-radius: 50%;
  flex-shrink: 0;
}
.waypoint-list .wp-marker.start { background: var(--green); }
.waypoint-list .wp-marker.end   { background: var(--red); color: white; }
/* Destination pill flips green once the route is computed + fresh.
   `.go` is added by renderWaypoints() in the same conditions that drive
   the map's destination pin colour — keeps the two depictions in sync. */
.waypoint-list .wp-marker.end.go {
  background: #22c55e;
  color: #0a0a0a;
}
/* Finish-flag glyph inside the planner waypoint-list's end marker.
   Smaller scale than the map version (22 px circle vs 28 px) and a
   slightly smaller right-shift to keep the pole visually centred. */
.wp-marker-flag {
  width: 11px;
  height: 11px;
  display: block;
  transform: translateX(2px);
}
.wp-marker-flag line {
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
}
.wp-marker-flag path {
  fill: currentColor;
}
.waypoint-list .wp-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.waypoint-list .wp-remove {
  background: none; border: none; color: var(--text-faint);
  cursor: pointer; padding: 4px 6px;
  line-height: 1;
  border-radius: 2px;
  display: inline-flex; align-items: center; justify-content: center;
}
.waypoint-list .wp-remove > svg {
  width: 16px; height: 16px; display: block;
}
.waypoint-list .wp-remove:hover { color: var(--red); background: var(--bg-elev); }
/* Star toggle sits LEFT of the trash icon on each waypoint row.
   Outline by default, filled amber when this waypoint is in the
   user's favourites. Same hit-area as the trash button so the two
   read as a pair. */
.waypoint-list .wp-favourite {
  background: none; border: none; color: var(--text-faint);
  cursor: pointer; padding: 4px 6px;
  line-height: 1;
  border-radius: 2px;
  display: inline-flex; align-items: center; justify-content: center;
}
.waypoint-list .wp-favourite > svg {
  width: 16px; height: 16px; display: block;
}
/* Neutral grey instead of amber — the star already reads as "saved"
   when filled; using the brand accent here over-emphasised it next to
   the other quieter row controls. */
.waypoint-list .wp-favourite:hover { color: var(--text); background: var(--bg-elev); }
.waypoint-list .wp-favourite.on { color: var(--text-dim); }
.waypoint-empty { color: var(--text-faint); font-style: italic; padding: 12px 0 !important; border: none !important; }

/* Collapsed-summary row that replaces the per-waypoint rows when there are
   more than 2 waypoints. Clicking it expands the list. Also reused as the
   header in expanded state. */
.waypoints-collapsed {
  justify-content: space-between !important;
  cursor: pointer;
  color: var(--text) !important;
  font-weight: 500;
  letter-spacing: 0.04em;
}
.waypoints-collapsed:hover { color: var(--amber) !important; }
.waypoints-collapsed .wp-count { font-family: var(--font-mono); font-size: 12px; }
.waypoints-collapsed .wp-chevron { font-size: 14px; color: var(--text-dim); }

.waypoint-actions { display: flex; gap: 8px; margin-top: 10px; }

/* Mode toggle */
.mode-toggle {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.mode-btn {
  background: var(--bg-elev);
  border: none;
  padding: 14px 8px;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
  color: var(--text-dim);
}
.mode-btn:hover { background: var(--bg-elev-hi); color: var(--text); }
.mode-btn.active {
  background: var(--amber);
  color: var(--on-amber);
}
/* Holds the inline SVG glyph for each nav mode. Sized so the SVG
   has the same optical weight as the previous text character (~22 px),
   and uses currentColor for stroke/fill so it picks up the button's
   active-amber state automatically. */
.mode-icon {
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 1;
}
.mode-icon svg { width: 100%; height: 100%; display: block; }
.mode-title { font-size: 13px; font-weight: 600; letter-spacing: 0.05em; }
.mode-sub { font-size: 10px; opacity: 0.7; font-family: var(--font-mono); letter-spacing: 0.05em; }

/* Slider */
/* Animated collapse/expand. Direct mode hides the slider section
   entirely; switching to Adventure or Roundtrip eases it back in (and
   eases it back out when switching to Direct). max-height is generous
   so the actual content height fits comfortably; padding / opacity /
   border collapse together for a clean accordion feel. */
.extra {
  overflow: hidden;
  max-height: 220px;
  opacity: 1;
  transition: max-height 280ms ease,
              opacity 200ms ease,
              padding-top 280ms ease,
              padding-bottom 280ms ease,
              border-bottom-width 280ms ease;
}
.extra[data-disabled="true"] {
  max-height: 0;
  opacity: 0;
  padding-top: 0;
  padding-bottom: 0;
  border-bottom-width: 0;
  pointer-events: none;
}
.slider-wrap { display: flex; align-items: center; gap: 12px; }
/* Slider + its tick labels share the same width column, separate from the
   numeric value/unit on the right. Ensures the +0…+50 labels align with the
   slider track and not with the "km" counter. */
.slider-track-col { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 2px; }
.slider-track-col input[type="range"] { width: 100%; }
input[type="range"] {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 4px;
  /* Track fill up to --fill-pct (set from JS in setExtraKm) drawn with
     diagonal 45° amber stripes; the remainder is the dim background.
     Top layer is a window: transparent below fill-pct lets the stripes
     beneath show, opaque dim above fill-pct hides them. */
  background:
    linear-gradient(
      to right,
      transparent 0 var(--fill-pct, 0%),
      var(--bg-elev) var(--fill-pct, 0%) 100%
    ),
    repeating-linear-gradient(
      45deg,
      var(--amber) 0 4px,
      var(--amber-dim) 4px 8px
    );
  border-radius: 2px;
  outline: none;
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px; height: 20px;
  background: var(--amber);
  border: 2px solid var(--on-amber);
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 0 0 1px var(--amber);
}
input[type="range"]::-moz-range-thumb {
  width: 20px; height: 20px;
  background: var(--amber);
  border: 2px solid var(--on-amber);
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 0 0 1px var(--amber);
}
.slider-value {
  font-family: var(--font-mono);
  font-size: 25px;             /* number is 40% larger than before */
  font-weight: 600;
  color: var(--amber);
  min-width: 60px;
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1;
}
.slider-value .unit {
  color: var(--text-faint);
  font-size: 10px;
  margin-left: 0;
  margin-top: 4px;
  text-transform: uppercase;
  letter-spacing: 0.15em;
}
.slider-ticks {
  /* Labels are positioned absolutely at their real percentage along the
     track. Padding accounts for the thumb's half-width so the 0% and 100%
     ends line up with the thumb's actual travel range. */
  position: relative;
  height: 14px;
  margin-top: 6px;
  padding: 0 10px;
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--text-faint);
  letter-spacing: 0.05em;
}

/* Summary */
.summary-stats[hidden] { display: none; }
.summary-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-bottom: 14px;
}
.summary-stats:has(#stat-stretch-wrap:not([hidden])) {
  grid-template-columns: 1fr 1fr 1fr;
}
.stat {
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-left: 3px solid var(--amber);
  padding: 8px 12px;
  border-radius: 2px;
}
.stat-label {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.15em;
  color: var(--text-faint);
  text-transform: uppercase;
}
.stat-value {
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.2;
  margin-top: 2px;
}
.stat-value .unit { font-size: 11px; color: var(--text-faint); margin-left: 3px; }

/* Buttons */
.btn-primary, .btn-secondary, .btn-ghost {
  font-family: var(--font-display);
  font-weight: 600;
  border: none;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, transform 0.05s;
  letter-spacing: 0.04em;
  border-radius: var(--radius);
}
.btn-primary:active, .btn-secondary:active, .btn-ghost:active { transform: translateY(1px); }

.btn-primary {
  width: 100%;
  background: var(--amber);
  color: var(--on-amber);
  padding: 14px;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.btn-primary:hover { background: var(--amber-hi); }
.btn-primary:disabled { background: var(--bg-elev); color: var(--text-faint); cursor: not-allowed; }
.btn-primary.loading {
  background: var(--amber-dim);
  color: var(--amber);
  cursor: progress;
}

/* Start-ride button uses green to signal "go" once a route is ready. The
   :disabled rule above still wins for greyed-out state, so the green only
   appears when the button is clickable. */
#start-ride:not(:disabled) { background: #22c55e; color: #0a0a0a; }
#start-ride:not(:disabled):hover { background: #16a34a; }
/* Bottom-right corner rounded harder so the button echoes the phone
   screen's bottom-right curve — mirrors the matching `.btn-trash`
   bottom-left treatment so the action-row reads as a single "bottom
   bezel" pair. */
#start-ride {
  border-radius: var(--radius) var(--radius) 28px var(--radius);
}

.btn-secondary {
  background: var(--bg-elev);
  color: var(--text-dim);
  padding: 10px 12px;
  font-size: 12px;
  border: 1px solid var(--line);
}
.btn-secondary:hover:not(:disabled) { background: var(--bg-elev-hi); color: var(--text); border-color: var(--line-hi); }
.btn-secondary:disabled { opacity: 0.4; cursor: not-allowed; }

.btn-ghost {
  background: transparent;
  color: var(--text-dim);
  padding: 6px 10px;
  font-size: 11px;
  border: 1px solid var(--line);
}
.btn-ghost:hover:not(:disabled) { color: var(--text); border-color: var(--line-hi); }
.btn-ghost:disabled { opacity: 0.4; cursor: not-allowed; }

/* Themed confirm dialog — used for destructive actions like "clear all
   waypoints" that we don't want to fire from a single accidental tap. */
.modal-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.modal-overlay[hidden] { display: none; }

/* ─────────────────────────────────────────────────────────────────────────
   Beta access gate
   Lives inside a `.modal-overlay` (so it inherits the centring + dim +
   backdrop-blur) but ports the V1 splash design from
   beta-gate-mockups.html — wordmark / "Private Beta" label / headline /
   subtitle / form / footnote, with an amber glow contained inside the
   card so it keeps the "you've arrived somewhere" mood. */
.beta-gate-card {
  position: relative;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  /* Extra bottom padding reserves room for the two absolute-positioned
     corner elements (language toggle on the right, "don't have one"
     footnote on the left) so they sit clear of the form above. */
  padding: 36px 28px 64px;
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 28px;
  overflow: hidden;
}
/* Diagonal amber glow inside the card — top-left + bottom-right corners
   pick up a faint accent without touching the dim overlay outside. */
.beta-gate-card::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at 30% 0%,    rgba(245, 158, 11, 0.10), transparent 55%),
    radial-gradient(circle at 80% 100%,  rgba(245, 158, 11, 0.06), transparent 55%);
}
.beta-gate-card > * { position: relative; z-index: 1; }

/* Sporr wordmark — motorsport variant. Russo One all-caps with a
   −6° racing skew; doubled-R still carries the amber accent. The
   HTML ships SPORR in caps so the styling needs no text-transform.
   Display: inline-block so the skew has a containing box to act on
   without dragging neighbouring content with it. */
.beta-gate-brand {
  font-family: 'Russo One', var(--font-display), sans-serif;
  /* 70 px ≈ 44 × 1.6 — the wordmark is the headline element of the
     gate now, so it earns the extra weight. */
  font-size: 70px;
  font-weight: 400;
  letter-spacing: 0.01em;
  color: var(--text);
  line-height: 1;
  display: inline-block;
  transform: skewX(-6deg);
}
.beta-gate-rr { color: var(--amber); }
/* Wrap around the wordmark — kept as an inline-flex container in
   case we want to slot something next to the wordmark again later.
   Currently only holds the wordmark itself. */
.beta-gate-brand-wrap {
  display: inline-flex;
  align-items: flex-end;
  gap: 8px;
}
/* Legacy divider — kept as a no-op alias in case any older saved
   markup is still cached on a returning user's browser. Safe to
   remove once we're confident the new HTML has rolled out. */
.beta-gate-divider { color: var(--amber); margin: 0 4px; }

/* Wordmark + tagline + divider + version pill share a column container
   so they read as one tight stacked header — same composition as the
   landing-page hero. The card's main 28-px gap stays for the wordmark
   block ↔ form spacing; inside the header we use a smaller gap. */
.beta-gate-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

/* Tagline — mirrors .hero-sub on sporr-landing.html. The brand-term
   "Adventure Rider" reads as an all-caps italic label rather than a
   sentence — the casing comes from CSS so the underlying translation
   keys stay in title case. Letter-spacing widens the caps so they
   read as the genre tag they are, not a shouty headline. */
.beta-gate-sub {
  font-size: clamp(13px, 1.9vw, 15px);
  color: #d4d4d4;
  font-weight: 400;
  font-style: italic;
  line-height: 1.35;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: center;
  margin: 0;
  /* Tighten the gap to the wordmark — the header's 16-px gap is a
     touch loose between a heavy wordmark and a slim tagline. */
  margin-top: -4px;
}

/* Thin separator — identical to .hero-divider on the landing page. */
.beta-gate-divider {
  width: 84px;
  height: 1px;
  background: var(--line);
}

/* Status pill — visually identical to .hero-pill on sporr-landing.html.
   Holds the i18n'd "Private beta" label, a middle-dot separator, and
   the dynamic version string populated by setupVersionLabel(). */
.beta-gate-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 16px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 4px;
}
/* Middle dot — opacity-dimmed so it reads as a quiet separator and not
   a third equally-weighted token. */
.beta-gate-pill-sep { opacity: 0.55; }
/* Defensive: if the version span ends up empty (dev case, no ?v= on
   the script URL), hide the dot too so the pill doesn't trail with
   "PRIVATE BETA · ". :has support is Safari 15.4+ / Chrome 105+,
   which covers the beta cohort. */
.beta-gate-pill:has(#beta-gate-version:empty) .beta-gate-pill-sep,
.beta-gate-pill:has(#beta-gate-version:empty) #beta-gate-version {
  display: none;
}

.beta-gate-headline {
  font-size: 19px;
  font-weight: 700;
  color: var(--text);
  margin: 0;
  text-align: center;
  max-width: 280px;
  line-height: 1.3;
}
.beta-gate-sub {
  font-size: 13px;
  color: var(--text-dim);
  margin: -20px 0 0;
  text-align: center;
  max-width: 280px;
  line-height: 1.5;
}

.beta-gate-form {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.beta-gate-text {
  width: 100%;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0 14px;
  height: 48px;
  color: var(--text);
  font-family: var(--font-mono);
  /* 16 px font prevents iOS Safari from auto-zooming on focus. */
  font-size: 16px;
  letter-spacing: 0.18em;
  text-align: center;
  text-transform: uppercase;
  outline: none;
  transition: border-color 0.15s;
}
.beta-gate-text::placeholder {
  color: var(--text-faint);
  letter-spacing: 0.12em;
  text-transform: none;
}
.beta-gate-text:focus { border-color: var(--amber); }
.beta-gate-text.error { border-color: var(--red); }
/* Brief horizontal shake when the code is rejected — re-triggered by
   removing + re-adding the class with a forced reflow in between. */
@keyframes beta-gate-shake {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-4px); }
  40%, 80% { transform: translateX(4px); }
}
.beta-gate-text.shake { animation: beta-gate-shake 220ms ease; }

.beta-gate-btn {
  width: 100%;
  height: 48px;
  background: var(--amber);
  color: var(--on-amber);
  border: none;
  border-radius: var(--radius);
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.15s;
}
.beta-gate-btn:hover { background: var(--amber-hi); }

.beta-gate-error {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.05em;
  color: var(--red);
  text-align: center;
  min-height: 14px;
}

.beta-gate-footnote {
  /* Anchored to the bottom-left corner of the gate card so the
     wordmark + form get the centre line uncluttered. The language
     toggle (.beta-gate-lang below) sits in the matching bottom-right
     corner; the max-width clamp leaves space between them on the
     narrow 360-px card so they never collide. */
  position: absolute;
  left: 18px;
  bottom: 18px;
  font-size: 10px;
  color: var(--text-faint);
  text-align: left;
  line-height: 1.4;
  margin: 0;
  max-width: calc(100% - 130px);
}
.beta-gate-footnote a {
  color: var(--amber);
  text-decoration: none;
}

/* Stack the gate card and the install hint vertically. The base
   .modal-overlay is a centred flex row; we just flip direction and
   add a gap so the hint reads as a footnote below the card. */
.beta-gate-overlay {
  flex-direction: column;
  gap: 16px;
  /* Smooth gate dismissal — two-speed fade. The dim background +
     backdrop-blur ease away over the full 800 ms so the map reveal
     feels gradual; the modal card + its chrome (language toggle,
     install hint) wipe out faster (350 ms) so the user isn't staring
     at a translucent ghost of the card while the dim is still
     lingering. JS adds .fading-out then waits GATE_FADE_MS (matching
     the slower dim transition) before flipping `hidden`. */
  transition:
    backdrop-filter 800ms ease,
    -webkit-backdrop-filter 800ms ease,
    background-color 800ms ease;
}
.beta-gate-overlay.fading-out {
  background-color: rgba(0, 0, 0, 0);
  backdrop-filter: blur(0);
  -webkit-backdrop-filter: blur(0);
  /* Don't let the fading layer eat taps meant for the planner / map
     behind it; pointer-events flip the moment the fade starts. */
  pointer-events: none;
}
/* Faster modal-card fade — wraps the card itself plus the absolute-
   positioned language toggle and install hint that sit alongside it
   inside the overlay. All three vanish together in 350 ms while the
   dim layer behind them keeps unwinding for another 450 ms. */
.beta-gate-card,
.beta-gate-lang,
.beta-gate-install {
  transition: opacity 350ms ease;
}
.beta-gate-overlay.fading-out .beta-gate-card,
.beta-gate-overlay.fading-out .beta-gate-lang,
.beta-gate-overlay.fading-out .beta-gate-install {
  opacity: 0;
}

/* Language toggle anchored inside the gate card's bottom-right corner
   (moved here from top-right — it was crowding the wordmark above).
   Mirrors the design of the toggle on sporr-landing.html but scaled
   ≈ 20 % smaller (36 → 29 px height, 11 → 9 px text, 14 → 11 px
   glyph, 8 → 6 px gap) so it sits comfortably inside the card chrome. */
.beta-gate-lang {
  position: absolute;
  bottom: 14px;
  right:  14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 29px;
  padding: 0 13px;
  font-family: var(--font-display);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--text);
  background: transparent;
  /* Whisper-thin outline — dropped from var(--line-hi) (#3a3a3a, very
     visible) to a 10% white wash so the chip reads as a quiet
     control next to the wordmark, not a competing element. Hover
     fades up to full amber for a clear affordance. */
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 4px;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.beta-gate-lang:hover { border-color: var(--amber); color: var(--amber); }
.beta-gate-lang svg { width: 11px; height: 11px; }

/* Install hint panel — V1b in beta-gate-mockups.html. Visually slimmer
   chrome than the gate card so the gate stays the primary focus.
   Amber phone-icon, bold title, mono eyebrow, chevron right. The
   whole row is a button — tap dispatches to the captured
   beforeinstallprompt (Android) or the iOS instructions modal. */
.beta-gate-install {
  width: 100%;
  max-width: 360px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 12px 16px;
  display: flex;
  align-items: center;
  gap: 14px;
  color: var(--text-dim);
  font-family: var(--font-display);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  -webkit-tap-highlight-color: transparent;
  text-align: left;
}
.beta-gate-install[hidden] { display: none; }
html.theme-light .beta-gate-install {
  background: rgba(255, 255, 255, 0.55);
}
.beta-gate-install:hover {
  border-color: var(--amber);
  background: rgba(245, 158, 11, 0.08);
}
.beta-gate-install:focus-visible {
  outline: none;
  border-color: var(--amber);
}
.beta-gate-install-icon {
  flex: 0 0 auto;
  color: var(--amber);
  display: flex;
  align-items: center;
  justify-content: center;
}
.beta-gate-install-icon svg { width: 28px; height: 28px; }
.beta-gate-install-text {
  flex: 1;
  min-width: 0;
  line-height: 1.3;
  display: flex;
  flex-direction: column;
}
.beta-gate-install-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: 0.02em;
}
.beta-gate-install-sub {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-top: 2px;
}
.beta-gate-install-chev {
  flex: 0 0 auto;
  color: var(--text-faint);
  display: flex;
  align-items: center;
}
.beta-gate-install-chev svg { width: 16px; height: 16px; }

/* iOS install-instructions modal. Reuses the generic .modal /
   .modal-titlebar / .modal-body chrome, just adds typography for
   the ordered list of steps and the inline share-icon callout. */
.install-modal { max-width: 360px; }
.install-modal-lede {
  margin: 0 0 14px 0;
  font-size: 13px;
  color: var(--text-dim);
  line-height: 1.5;
}
.install-modal-steps {
  margin: 0 0 16px 0;
  padding-left: 22px;
  font-size: 13px;
  color: var(--text);
  line-height: 1.55;
}
.install-modal-steps li { margin-bottom: 8px; }
.install-modal-steps li:last-child { margin-bottom: 0; }
.install-modal-share-inline {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  vertical-align: text-bottom;
  margin: 0 2px;
  color: var(--amber);
}
.install-modal-share-inline svg { width: 18px; height: 18px; }
.modal {
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  max-width: 380px;
  width: 100%;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
  font-family: var(--font-display);
  overflow: hidden;
}
/* Amber title bar at the top of the dialog — Windows-window-style. */
.modal-titlebar {
  background: var(--amber);
  color: var(--on-amber);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 14px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-size: 12px;
}
.modal-title { line-height: 1; }
.modal-close {
  background: none;
  border: none;
  color: var(--on-amber);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 2px;
  font-family: inherit;
}
.modal-close:hover { background: rgba(0, 0, 0, 0.18); }
.modal-body {
  padding: 20px 22px 18px;
}
.modal-message {
  color: var(--text);
  font-size: 15px;
  line-height: 1.45;
  margin-bottom: 20px;
}
.modal-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}
/* Text input inside the prompt dialog. Same chrome family as the search
   bar; 16px font-size to keep iOS from zooming the page when the input
   receives focus. */
.modal-input {
  display: block;
  width: 100%;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 14px;
  font-family: var(--font-display);
  font-size: 16px;
  color: var(--text);
  outline: none;
  margin-bottom: 16px;
  transition: border-color 0.15s;
}
.modal-input:focus { border-color: var(--amber); }
.modal-input::placeholder { color: var(--text-faint); }
.modal-actions .btn-primary,
.modal-actions .btn-secondary {
  width: auto;
  padding: 10px 18px;
}
.btn-primary.modal-danger {
  background: var(--amber);   /* same orange as the title bar */
  color: var(--on-amber);
}
.btn-primary.modal-danger:hover { background: var(--amber-hi); }

/* Saved-routes list inside the Load dialog. Each row: name + meta on the
   left, ✕ delete button on the right. Clicking the row (anywhere except
   the delete button) loads that route. */
.saved-routes-list {
  list-style: none; margin: 0; padding: 0;
  max-height: 320px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-hi) transparent;
}
.saved-routes-list::-webkit-scrollbar { width: 6px; }
.saved-routes-list::-webkit-scrollbar-track { background: transparent; }
.saved-routes-list::-webkit-scrollbar-thumb { background: var(--line-hi); border-radius: 3px; }
.saved-routes-list::-webkit-scrollbar-thumb:hover { background: var(--amber); }

.saved-routes-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 4px;
  border-bottom: 1px dashed var(--line);
  cursor: pointer;
}
.saved-routes-list li:last-child { border-bottom: none; }
.saved-routes-list li:hover { background: var(--bg-elev); }

.saved-route-info {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; gap: 2px;
}
.saved-route-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.saved-route-meta {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
}

.saved-route-delete {
  background: none;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px 10px;
  font-size: 18px;
  line-height: 1;
  border-radius: 3px;
}
.saved-route-delete:hover { color: var(--amber); background: var(--bg-elev-hi); }

.saved-routes-empty {
  color: var(--text-faint);
  text-align: center;
  padding: 28px 0;
  font-style: italic;
}

/* ─────────────────────────────────────────────────────────────────────
   Settings — V1 iOS-style grouped list. Modal on desktop, fullscreen
   on mobile (≤ 800 px). See settings-mockups.html for the source design.
   ───────────────────────────────────────────────────────────────────── */
.settings-modal {
  max-width: 480px;
  width: 100%;
  max-height: 86vh;
  display: flex;
  flex-direction: column;
}
.settings-body {
  flex: 1 1 auto;     /* fill the column height inside .settings-modal so
                         the bg-deep background reaches the bottom of the
                         viewport even on screens taller than the content */
  overflow-y: auto;
  padding: 18px 18px 28px;
  background: var(--bg-deep);
  scrollbar-width: thin;
  scrollbar-color: var(--line-hi) transparent;
}
.settings-body::-webkit-scrollbar { width: 6px; }
.settings-body::-webkit-scrollbar-thumb { background: var(--line-hi); border-radius: 3px; }

/* Back chevron in the titlebar (left side) — replaces the close X on
   the right. Spacer keeps the modal-title visually centred. */
.settings-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px; height: 28px;
  padding: 0;
  border-radius: 50%;
}
.settings-titlebar-spacer { width: 28px; height: 28px; display: inline-block; }

/* Section header above each group of rows. Mono uppercase, faint. */
.settings-section-label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.22em;
  color: var(--text-faint);
  text-transform: uppercase;
  margin: 18px 4px 8px;
}
.settings-section-label:first-child { margin-top: 4px; }

/* Group — rounded card holding stacked rows. Rows share a left/right
   inset + a bottom border between them; corners are rounded by the
   parent so the rows fall into a single pill-like shape. */
.settings-group {
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  margin-bottom: 6px;
}
.settings-group--disabled { opacity: 0.55; pointer-events: none; }

/* One settings row. Layout: icon · label-and-sub · value/control · chev. */
.settings-v1-row {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 56px;
  padding: 10px 14px;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--line);
  width: 100%;
  color: var(--text);
  font-family: inherit;
  text-align: left;
}
.settings-v1-row:last-child { border-bottom: none; }
.settings-v1-row--button { cursor: pointer; }
.settings-v1-row--button:hover:not([aria-disabled="true"]) { background: var(--bg-elev); }
.settings-v1-row[aria-disabled="true"] { cursor: not-allowed; }

.settings-v1-icon {
  /* Outer slot keeps the 32 × 32 footprint so every row aligns its
     label-and-controls column the same. Background + border-radius
     stripped so the glyph floats free on the row background — cleaner
     against the grouped-card look. SVG size bumped to 22 px (from the
     inline 18 px attribute) so the un-boxed icon carries the visual
     weight it lost when the chip went away. */
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-dim);
  flex-shrink: 0;
}
.settings-v1-icon svg { width: 22px; height: 22px; }
.settings-v1-row[aria-disabled="true"] .settings-v1-icon { color: var(--text-faint); }
/* Destructive variant — used for "Reset app". Label + icon both red
   so the row signals "this is the dangerous one" even before the
   confirm dialog appears. Chev stays neutral grey so it doesn't
   double up on the warning. */
.settings-v1-row--danger .settings-v1-label,
.settings-v1-row--danger .settings-v1-icon { color: var(--red); }
.settings-v1-row--danger:hover:not([aria-disabled="true"]) {
  background: rgba(239, 68, 68, 0.08);
}

.settings-v1-text {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; gap: 1px;
}
.settings-v1-label { font-size: 14px; color: var(--text); }
.settings-v1-sub   { font-size: 11px; color: var(--text-faint); }
.settings-v1-value {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-dim);
  letter-spacing: 0.06em;
}
.settings-v1-chev {
  color: var(--text-faint);
  flex-shrink: 0;
  display: inline-flex; align-items: center;
}

/* Amber-tinted badge used for the beta code chip on the About row. */
.settings-v1-badge {
  display: inline-flex; align-items: center;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--amber);
  background: rgba(245, 158, 11, 0.12);
  padding: 4px 8px;
  border-radius: 4px;
}

/* Inline native <select> rendered on the right side of a row. Compact,
   transparent background, amber chevron on focus. All <select>s in
   Settings share the same locked width so Theme / Language / Default
   mode line up in a single right-aligned column instead of jagging
   out to whatever fits each row's longest option. */
.settings-v1-select {
  background: var(--bg-elev);
  border: 1px solid var(--line);
  color: var(--text);
  padding: 6px 10px;
  border-radius: 6px;
  font-family: inherit;
  font-size: 13px;
  width: 160px;
  text-align: right;
  cursor: pointer;
}
.settings-v1-select:focus { outline: none; border-color: var(--amber); }
.settings-v1-select:disabled { opacity: 0.5; cursor: not-allowed; }

/* iOS-style sliding toggle — used for the (disabled) ride-mode placeholders.
   Wire `.on` for the on state, `.disabled` for greyed-out. */
.settings-v1-toggle {
  flex-shrink: 0;
  width: 44px; height: 26px;
  background: var(--bg-elev-hi);
  border-radius: 999px;
  position: relative;
  cursor: pointer;
  transition: background 0.15s;
}
.settings-v1-toggle::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 22px; height: 22px;
  background: var(--text);
  border-radius: 50%;
  transition: left 0.15s;
}
.settings-v1-toggle.on { background: var(--amber); }
.settings-v1-toggle.on::after { left: 20px; background: var(--on-amber); }
.settings-v1-toggle.disabled { opacity: 0.5; cursor: not-allowed; }

/* Settings footer — quiet attribution + copyright line at the very
   bottom of the page. Sits below the About group. Mono mid-grey,
   tiny — reads as legal chrome rather than another section. Links
   are amber (matches the rest of the app's link styling). */
.settings-footer {
  margin-top: 22px;
  padding: 0 6px 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  line-height: 1.6;
  text-align: center;
}
.settings-footer p { margin: 0 0 6px; }
.settings-footer p:last-child { margin-bottom: 0; opacity: 0.75; }
/* Inline wordmark inside the last footer paragraph — display font +
   lowercase + amber `rr`. Bumped a few pixels above the 10 px legal
   chrome around it so it reads as the wordmark rather than disappearing
   into the copyright line. */
.settings-footer-brand {
  /* Motorsport wordmark — Russo One, all-caps, slight racing skew.
     See .menu-foot-brand for full rationale. */
  font-family: 'Russo One', var(--font-display), sans-serif;
  font-size: 14px;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--text-dim);
  text-transform: none;
  vertical-align: 0;
  display: inline-block;
  transform: skewX(-6deg);
}
.settings-footer-rr { color: var(--amber); }
.settings-footer a {
  color: var(--text-dim);
  text-decoration: underline;
  text-decoration-color: var(--line-hi);
  text-underline-offset: 2px;
}
.settings-footer a:hover { color: var(--amber); text-decoration-color: var(--amber); }

/* Big red-outline sign-out button at the bottom. Pulls the eye away
   from the rest of the page so it's hard to hit by accident. */
.settings-signout {
  width: 100%;
  margin-top: 20px;
  padding: 14px;
  background: transparent;
  border: 1px solid var(--red);
  color: var(--red);
  border-radius: 6px;
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.settings-signout:hover { background: var(--red); color: var(--bg-deep); }

/* ─ Settings sub-view: address picker for Home / Work ─────────────────
   When the user taps a Home/Work row, the main grouped list is hidden
   and this picker takes over. The titlebar's title swaps from
   "Settings" → "Set home" / "Set work" and the back chevron returns to
   the main view instead of closing the modal. Wiring in openSettings().
   Reuses the existing .search + .search-results styles via inherited
   classes so the input/dropdown match the planning view exactly. */
.settings-view-picker { display: flex; flex-direction: column; gap: 14px; }
/* User-agent [hidden] sets display:none, but our explicit display:flex
   on .settings-view-picker wins on specificity — so when JS sets the
   hidden attribute the picker stays visible (showing through under the
   main view). Re-instate the hide explicitly. Same rule covers
   .settings-view-main and the privacy view for symmetry. */
.settings-view[hidden] { display: none; }

/* Privacy sub-view — long-form prose. Settings modal already gives us
   scrolling + padding, so the body just needs typography. Calmer
   weights than the rest of Settings (which is dense rows) so it reads
   as documentation rather than UI. */
.settings-privacy-body {
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-dim);
}
.settings-privacy-body p {
  margin: 0 0 12px 0;
}
.settings-privacy-body p:last-child { margin-bottom: 0; }
.settings-privacy-body h3 {
  margin: 18px 0 8px 0;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text);
}
.settings-privacy-body h3:first-child { margin-top: 0; }
.settings-privacy-body ul {
  margin: 0 0 12px 0;
  padding-left: 20px;
}
.settings-privacy-body li {
  margin-bottom: 4px;
}
.settings-privacy-body li:last-child { margin-bottom: 0; }
.settings-privacy-body strong { color: var(--text); }
.settings-privacy-body code {
  font-family: var(--font-mono);
  font-size: 12px;
  background: var(--bg-elev);
  padding: 1px 5px;
  border-radius: 3px;
  color: var(--text);
}
.settings-privacy-aside {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--text-faint);
  text-transform: none;
  margin-left: 6px;
}
.settings-picker-search { position: relative; padding-top: 4px; }
/* Match the planning view's search field — same height + font-size so
   the iOS auto-zoom-on-focus quirk stays suppressed and the input
   doesn't read as thin compared to the rest of the settings rows. */
.settings-picker-input {
  width: 100%;
  height: 48px;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0 16px;
  font-family: inherit;
  font-size: 16px;
  color: var(--text);
  outline: none;
  box-shadow: var(--shadow);
  transition: border-color 0.15s;
}
.settings-picker-input:focus { border-color: var(--amber); }
.settings-picker-input::placeholder { color: var(--text-faint); }
.settings-picker-results {
  /* Inherits .search-results layout from the main planning search,
     but in this context we want the results to flow inside the modal
     body rather than absolutely overlay something — override the
     positioning so the list pushes the Clear button down naturally. */
  position: static;
  margin-top: 8px;
  border-radius: 8px;
  max-height: 60vh;
}
.settings-picker-clear {
  align-self: flex-start;
  background: transparent;
  border: 1px solid var(--line);
  color: var(--text-dim);
  padding: 10px 14px;
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  border-radius: 6px;
  cursor: pointer;
  margin-top: 4px;
}
.settings-picker-clear:hover { color: var(--red); border-color: var(--red); }

/* Favourites sub-view — same chrome as the main settings list (a
   .settings-group full of rows) but the list is dynamically rendered
   from state.favourites and each row carries its own delete button.
   The "Add favourite" row sits below the list as a separate group so
   it visually reads as an action, not another saved entry. */
.settings-view-favourites { display: flex; flex-direction: column; gap: 12px; }
.settings-favourites-list {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}
.settings-favourites-list:empty { display: none; }
.settings-fav-row {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 56px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  color: var(--text);
}
.settings-fav-row:last-child { border-bottom: none; }
.settings-fav-row .settings-v1-icon { color: var(--amber); }
.settings-fav-delete {
  background: none;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 6px 8px;
  line-height: 1;
  border-radius: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.settings-fav-delete:hover { color: var(--red); background: var(--bg-elev); }
.settings-favourites-add {
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: 10px;
}
.settings-favourites-add:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.settings-favourites-empty {
  font-size: 13px;
  color: var(--text-faint);
  font-style: italic;
  padding: 4px 14px;
}

/* Fullscreen on mobile. Overrides the modal-overlay's centred-card
   layout so the settings page takes the whole viewport — feels like
   a native settings page rather than a popup. */
@media (max-width: 800px) {
  /* Lift the .modal-overlay padding so the modal can fill edge-to-edge. */
  #settings-modal-overlay { padding: 0; }
  .settings-modal {
    max-width: 100vw;
    width: 100vw;
    height: 100vh;
    max-height: 100vh;
    border-radius: 0;
    border-left: none;
    border-right: none;
    border-top: none;
  }
  /* Push the amber titlebar's content below the iPhone notch / dynamic
     island while letting the amber background extend up into that area.
     iOS-standard look: the coloured bar fills the safe-area band so the
     status text + clock sit on amber, the back chevron + title sit
     below the cutout. */
  .settings-modal .modal-titlebar {
    padding-top: calc(8px + env(safe-area-inset-top, 0px));
  }
  .settings-body {
    /* Account for safe-area insets so the sign-out button isn't
       hidden behind the home indicator on iPhones. */
    padding-bottom: calc(28px + env(safe-area-inset-bottom, 0px));
  }
}

/* Toggle variant — when aria-pressed="true" the button shows an amber dot
   and amber border to indicate the option is active. */
.btn-ghost.toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.btn-ghost .toggle-dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--text-faint);
  transition: background 120ms, box-shadow 120ms;
}
.btn-ghost[aria-pressed="true"] { border-color: var(--amber); color: var(--amber); }
/* Hover state must NOT override the amber active treatment. Without this
   rule, the generic `.btn-ghost:hover:not(:disabled)` selector (which
   sets border to var(--line-hi) + text to var(--text)) wins by
   specificity, and on touch devices the hover "sticks" after a tap —
   so toggling off then back on visually clears the amber stroke
   even though aria-pressed is correctly "true". Specificity here is
   (0,4,0) vs. (0,3,0) on the generic hover, so this wins. */
.btn-ghost[aria-pressed="true"]:hover:not(:disabled) {
  border-color: var(--amber);
  color: var(--amber);
}
.btn-ghost[aria-pressed="true"] .toggle-dot {
  background: var(--amber);
  box-shadow: 0 0 6px var(--amber);
}

.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 8px; }
/* Action row: trash + share buttons on the left, Start filling the
   rest. Both icon buttons are `auto` width so they stay icon-sized;
   Start gets a `1fr` so it visually carries most of the weight (it's
   the primary action once a route has auto-computed). */
.action-row { display: grid; grid-template-columns: auto auto 1fr; gap: 8px; }

/* Action-row buttons are 40 % taller than the standard `.btn-primary`
   so the bottom-of-panel action bar reads as the obvious next step.
   Padding bump from 14 → 22 px lifts the effective height from ~42 px
   to ~62 px while keeping the text vertically centred. Trash button
   stretches to match via grid `align-items: stretch`. Scoped to
   `.action-row` so modal Save / Cancel buttons keep their natural size.
   Label font also bumped from 14 → 20 px (≈40 % bigger) so the text
   has more presence in the taller button. */
.action-row .btn-primary {
  padding: 16px 22px;
  font-size: 20px;
}
/* Trash icon scaled 40 % up (20 → 28 px) to match the label-bump on
   the sibling Start button. The SVG inside the button is targeted via
   descendant selector; the inline width/height attributes are
   overridden by these CSS values. */
.btn-trash svg {
  width: 28px;
  height: 28px;
}

/* Trash button — inherits the Clear-route click handler (auto-compute
   removed the need for a Compute button, so this slot is repurposed
   for the destructive "wipe all" action). Square-ish, dim by default,
   red-tinted on hover so the destructive nature is clear. */
.btn-trash {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 50 % wider than the original 48 px so the button has more thumb
     surface and reads as a meaningful action rather than a glyph. */
  width: 72px;
  padding: 0;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  /* Bottom-left corner rounded harder than the rest so the button's
     outer corner echoes the phone screen's bottom-left curve. The
     other three corners stay at the standard small radius. */
  border-radius: var(--radius) var(--radius) var(--radius) 28px;
  color: var(--text-dim);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.05s;
}
.btn-trash:hover:not(:disabled) {
  color: var(--red);
  border-color: var(--red);
  background: var(--bg-elev-hi);
}
.btn-trash:active:not(:disabled) { transform: translateY(1px); }
.btn-trash:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

/* Share button — same chrome as the trash button (icon size, width,
   border, default colour) but a positive amber accent on hover
   instead of destructive red, and no special corner curve (it sits
   mid-row, not against the phone's bottom-left corner). Inherits the
   28-px SVG sizing via the descendant rule below. */
.btn-share {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  padding: 0;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--text-dim);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.05s;
}
.btn-share svg { width: 28px; height: 28px; }
.btn-share:hover:not(:disabled) {
  color: var(--amber);
  border-color: var(--amber);
  background: var(--bg-elev-hi);
}
.btn-share:active:not(:disabled) { transform: translateY(1px); }
.btn-share:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.btn-row > * { padding: 8px; font-size: 11px; }
.file-btn { cursor: pointer; text-align: center; display: flex; align-items: center; justify-content: center; }

.btn-icon {
  background: none; border: none;
  color: var(--text-dim);
  cursor: pointer;
  padding: 4px 8px;
  font-size: 16px;
}
.btn-icon:hover { color: var(--text); }

/* ─────────────────────────────────────────────────────────────────────────
   Speed-warning frame
   Thick red border + inner red glow welded to the viewport edges,
   shown whenever the rider is exceeding the posted speed limit
   (eventually — currently always-on in ride mode for desktop testing).
   Pointer-events disabled so the frame never blocks UI underneath.
   Animated pulse so the warning reads as urgent without being a
   static border that the eye stops registering after a few seconds.
   ───────────────────────────────────────────────────────────────────────── */
.speed-warning {
  position: fixed;
  inset: 0;
  pointer-events: none;
  /* Sits just above the map (z 0) but BELOW every floating UI
     element — HUD card (z 6), topbar (z 10), road-name pill (z 11),
     pause/end-ride FABs. The red frame bleeds in from the screen
     edges and the UI controls paint over it cleanly, so a
     speeding warning never obscures the data the rider needs. */
  z-index: 4;
  opacity: 0;
  transition: opacity 350ms ease;
  box-sizing: border-box;
  border: 8px solid #dc2626;
  /* Stacked inset + outer shadows — inset spreads the glow inward
     across the viewport edges (visible past the 8 px border line),
     outer adds a slight bloom around the border for the LCD-bleed
     look. Both use red-600 with descending alphas so the falloff
     reads as a real light source rather than a flat rectangle. */
  box-shadow:
    inset 0 0 80px rgba(220, 38, 38, 0.55),
    inset 0 0 24px rgba(220, 38, 38, 0.85),
    0 0 24px rgba(220, 38, 38, 0.45);
  animation: speed-warning-pulse 1.4s ease-in-out infinite;
}
/* Body gets the .speeding class from updateSpeedLimit() whenever the
   rider's reported speed exceeds the current road's posted limit AND
   we actually have limit data from the vector tiles. Combining with
   .ride-active so the frame can never appear in planner mode. */
body.ride-active.speeding .speed-warning {
  opacity: 1;
}
@keyframes speed-warning-pulse {
  0%, 100% {
    box-shadow:
      inset 0 0 60px rgba(220, 38, 38, 0.45),
      inset 0 0 18px rgba(220, 38, 38, 0.75),
      0 0 18px rgba(220, 38, 38, 0.35);
  }
  50% {
    box-shadow:
      inset 0 0 100px rgba(220, 38, 38, 0.65),
      inset 0 0 28px rgba(220, 38, 38, 0.95),
      0 0 30px rgba(220, 38, 38, 0.55);
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   FAB
   ───────────────────────────────────────────────────────────────────────── */
.fab-stack {
  position: absolute;
  right: calc(16px + env(safe-area-inset-right, 0px));
  top:   calc(80px + env(safe-area-inset-top, 0px));
  display: flex; flex-direction: column; gap: 8px;
  z-index: 5;
}
.fab {
  width: 44px; height: 44px;
  border-radius: var(--radius);
  background: var(--bg-panel);
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 18px;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: background 0.15s, color 0.15s;
}
.fab:hover:not(:disabled) { background: var(--bg-elev); color: var(--amber); }
.fab:disabled { opacity: 0.4; cursor: not-allowed; }

/* ─────────────────────────────────────────────────────────────────────────
   Ride / navigation HUD
   Replaces the planner once Start ride is pressed. Compact bottom panel
   with the off-route hint up top and the Distance / Time stat blocks
   beneath. The planner is fully hidden via body.ride-active in JS.
   ───────────────────────────────────────────────────────────────────────── */
.nav-hud[hidden] { display: none; }
/* No surrounding panel chrome — the HUD is just a layout container that
   positions individual floating elements (exit button, off-route pill,
   stat boxes) over the map, the same way the topbar floats the brand
   menu / search / recenter buttons as separate pills. Positioned inside
   #app (which is min-height: 100dvh) so the bottom reaches the actual
   screen bottom on iOS PWA. */
.nav-hud {
  position: absolute;
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  left:   calc(16px + env(safe-area-inset-left, 0px));
  width: 440px;
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  z-index: 6;
  pointer-events: none;     /* clicks pass through to the map between pills */
}
.nav-hud > * { pointer-events: auto; }
/* (Variant-G card replaces the two stat boxes — styles defined below.) */
/* Pause-ride button — anchored top-right in ride mode so it balances
   the theme toggle which lives on the left. (Theme toggle is the
   leftmost topbar grid item; in ride mode it's promoted from column
   2 to column 1 so the theme + pause pair flank the map cleanly.)
   44 × 44 desktop / 40 × 40 mobile, same panel chrome as the other
   floating controls. */
.nav-exit-fab {
  position: absolute;
  top:   calc(16px + env(safe-area-inset-top,   0px));
  right: calc(16px + env(safe-area-inset-right, 0px));
  height: 44px;
  width: 44px;
  font-size: 18px;
  line-height: 1;
  z-index: 11;
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}
/* In ride mode the theme toggle takes the leftmost topbar slot
   (where the burger lives in planning). Burger is faded to opacity 0,
   so the theme visually occupies the corner. Both `grid-column` AND
   `grid-row` need to be pinned — without `grid-row: 1` the grid
   auto-flow drops a second row beneath the burger and the theme
   ends up pushed down. With both set, the two children share the
   same cell and stack visually; only the theme is visible. */
body.ride-active .theme-toggle {
  grid-column: 1;
  grid-row: 1;
}
body.ride-active .nav-exit-fab {
  opacity: 1;
  pointer-events: auto;
}

/* Road / street name pill (R3 style from info-mockups.html).
   Floating uppercase mono text with an amber underline — no panel
   chrome, drop-shadow keeps it legible on both dark + light maps.
   Centered at the top of the viewport, sized just to the text. Updated
   each ride-mode GPS tick by updateRoadNamePill() in app.js. Default
   state is opacity 0; gaining the `.visible` class fades it in over
   250 ms when a name is available. Hidden in planning mode (the
   class is never applied outside ride mode). */
.road-name-pill {
  position: absolute;
  top: calc(22px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text);
  text-align: center;
  padding: 2px 4px 6px;
  border-bottom: 2px solid var(--amber);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7));
  /* Leave room for pause (left ~60 px) + theme toggle (left ~108 px)
     and the recenter slot on the right — the pill ellipsises if its
     text is too long for the remaining middle strip. */
  max-width: calc(100vw - 220px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  /* Above the topbar (z-index 10) so it isn't covered by the faded-
     out search field, but below modals (z-index 100). */
  z-index: 11;
  opacity: 0;
  transition: opacity 250ms ease;
}
.road-name-pill.visible { opacity: 1; }
html.theme-light .road-name-pill {
  filter: drop-shadow(0 1px 2px rgba(255, 255, 255, 0.7));
}

/* ─────────────────────────────────────────────────────────────────────
   Rider-direction orb (ride mode)
   ─────────────────────────────────────────────────────────────────────
   Blue dot + glow visually identical to the GPS marker (.map-marker.gps)
   — same #38bdf8 fill, same box-shadow ring + halo — so it reads as a
   "second copy" of the rider's marker docked at the edge of the screen
   when they've panned the camera away. Anchored at viewport (0, 0);
   updateRiderDirectionChevron() in app.js writes a transform each
   frame that places it on the inset viewport edge along the line
   pointing toward the rider's projected screen position. Visual cue
   only — pointer-events: none, no tap target. The element is hidden
   the moment the auto-recenter flyTo lands.

   Box-shadow rather than filter: blur — the latter creates a
   permanent GPU compositing layer that breaks WebGL map rendering on
   some browser/GPU combinations (we learned this the hard way). */
.rider-direction-chevron {
  position: absolute;
  top: 0;
  left: 0;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  /* Transparent core — only the box-shadow halos paint, so the orb
     reads as a soft blue glow at the edge without a visible disc.
     Two layered shadows: a tight bright inner radius + a wider diffuse
     outer one. Same #38bdf8 hue as the GPS marker's glow, lower alpha
     so the absence of a bright core doesn't feel harsh. Dimensions +
     shadow radii are 3× the original 20-px / 14-px / 36-px values. */
  background: transparent;
  box-shadow:
    0 0  42px 12px rgba(56, 189, 248, 0.78),
    0 0 108px 42px rgba(56, 189, 248, 0.32);
  pointer-events: none;
  /* Above the map (0) and its top-blur strip (1) but below all UI
     chrome — topbar (10), road-name pill (11), HUD (5), modals (100).
     The glow is meant to be an ambient cue at the edge, not a sticker
     on top of the interface, so chrome painting over it reinforces the
     "light from outside" feel. Frosted UI surfaces with backdrop-filter
     will let some of the glow shine through them, which looks right. */
  z-index: 2;
  transition: opacity 200ms ease;
  opacity: 0;
}
.rider-direction-chevron.visible { opacity: 1; }

/* ─────────────────────────────────────────────────────────────────────
   Weather-at-destination toast
   ─────────────────────────────────────────────────────────────────────
   Fires once per ride at the halfway point. Three states driven by
   class toggles from showWeatherToast() in app.js:
     • base:                opacity 0, hidden
     • .visible:            opacity 1 — fade-in transition (800 ms)
     • .visible.full:       progress fill animates to 100% (10 s linear)
     • .fading-out:         opacity 0 — fade-out transition (800 ms)
   Total visible time = 800 + 10000 + 800 = 11.6 s. */
.weather-toast {
  position: fixed;
  /* 16 px horizontal inset matches the topbar's left/right padding,
     so the toast's edges line up with the pause + theme-toggle FABs
     on the left and the recenter FAB on the right. */
  left: calc(16px + env(safe-area-inset-left, 0px));
  right: calc(16px + env(safe-area-inset-right, 0px));
  /* 72 px ≈ topbar inset (16) + FAB height (40) + matching 16 px gap
     below — same rhythm as the rest of the topbar chrome. */
  top: calc(72px + env(safe-area-inset-top, 0px));
  /* Fully opaque panel-grey base + a slim sky-blue tint at the top-
     left corner layered on top. Two backgrounds stacked: gradient
     above (with a transparent endpoint, so the blue fades into
     nothing), solid panel below — no map showing through. */
  background-color: var(--bg-panel);
  background-image: linear-gradient(135deg, rgba(56,189,248,0.14), transparent 65%);
  border: 1px solid var(--line);
  border-radius: 10px;
  /* Padding + inner sizes chosen so the toast matches the ride-mode
     data card height almost exactly — easier to spot at a glance and
     reads as the card's "sibling" floating above. */
  padding: 18px 22px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  box-shadow: var(--shadow);
  z-index: 14;
  opacity: 0;
  transition: opacity 800ms ease;
  pointer-events: none;
  /* isolate so the ::before progress fill sits behind the text via
     z-index without leaking out of the rounded corners. */
  overflow: hidden;
  isolation: isolate;
}
.weather-toast[hidden] { display: none; }
/* Toast is interactive only while it's actually visible — tap-to-
   dismiss handler lives on the element itself. During fade-in/-out
   we leave pointer-events: none from the base rule so a half-faded
   toast can't intercept a tap meant for the map underneath. */
.weather-toast.visible { opacity: 1; pointer-events: auto; cursor: pointer; }
.weather-toast.fading-out { opacity: 0; }
/* Light-mode override — the default --bg-panel (#fff) reads as
   blown-out white against the bright map tiles. Drop to the elevated-
   grey step so the toast still separates from the page but doesn't
   glare. Dark mode keeps --bg-panel since the deep neutral already
   reads soft. */
html.theme-light .weather-toast { background-color: var(--bg-elev); }

/* Progress-fill background — animates from 0% → 100% over 10 s once
   the `.full` class is set (which happens AFTER the fade-in lands). */
.weather-toast::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 0%;
  background: rgba(56, 189, 248, 0.18);
  /* Must stay in sync with WEATHER_PROGRESS_MS in app.js. */
  transition: width 20000ms linear;
  z-index: 0;
  pointer-events: none;
}
.weather-toast.full::before { width: 100%; }

/* Keep the eyebrow / row content above the fill. */
.weather-eyebrow, .weather-row { position: relative; z-index: 1; }

/* Eyebrow spans the full width as a centred header — same role as the
   "WEATHER AT DESTINATION" label that opens the strip on the road. */
.weather-eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-faint);
  text-align: center;
}

/* Glyph + temp clustered toward the middle of the toast rather than
   pinned to opposite edges — reads as one weather block rather than
   two corner items. */
.weather-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 28px;
}
.weather-temp {
  font-size: 64px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--text);
}
.weather-glyph { width: 88px; height: 88px; flex: 0 0 auto; }
.weather-glyph svg { width: 100%; height: 100%; }

/* Landscape ride mode — the original full-width stretch reaches all
   the way across the screen on a phone in landscape, which collides
   with the road-name pill (centered) and the recenter FAB on the
   right. Cap at 600 px and let the toast hug the LEFT edge of the
   visible band so the right half of the screen stays clear. The
   600 px cap is wide enough that the glyph + temp + eyebrow all
   read at their portrait sizes; no internal layout change needed.
   `@media (orientation: landscape) and (max-height: 500px)` mirrors
   the same gate the rest of the ride-mode landscape overrides use,
   so this only kicks in on a phone in landscape (not a desktop
   landscape window). */
@media (orientation: landscape) and (max-height: 500px) {
  .weather-toast {
    right: auto;
    width: 600px;
    max-width: calc(100vw
                    - env(safe-area-inset-left, 0px)
                    - env(safe-area-inset-right, 0px)
                    - 32px);
  }
}
@media (max-width: 800px) {
  .nav-exit-fab {
    height: 40px;
    width: 40px;
    font-size: 16px;
  }
}
/* Wrapper that fuses the off-route banner + data card into a single
   rounded shape. Owns the rounded corners + shadow; the card itself
   loses its own rounded corners (see .nav-hud .nav-card override below). */
.nav-card-wrap {
  position: relative;
  /* Match the planner panel exactly — same 8 px (var(--radius-lg))
     all four corners + same 1 px border. The inner .nav-card drops
     its own border so the wrap is the sole authority on the
     silhouette; this also stops the card's stroke from clipping
     awkwardly against the wrap's rounded edge. The end-ride green
     CTA (inset:0) and any off-route banner welded above the card
     inherit the same clip via `overflow: hidden`. */
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: border-color 120ms ease;
}
/* Tap-flash relocated from the inner card to the wrap so the amber
   accent rides the rounded silhouette cleanly. :has() is supported in
   all the targets we care about (Safari 15.4+, Chrome 105+). */
.nav-card-wrap:has(.nav-card:active),
.nav-card-wrap:focus-within {
  border-color: var(--amber);
}

/* End-ride morph CTA. Replaces the data card + any visible off-route
   banner when the rider arrives at the final waypoint. Green block
   (matches the completed-route line + the Start ride button green)
   with a tap-to-end hint. Position: absolute inset 0 → the CTA fills
   the EXACT footprint of the data card underneath (which stays in
   the layout via visibility:hidden so the wrap height doesn't jump
   when the CTA appears). The wrap's overflow:hidden clips both the
   CTA and the sweep animation to the rounded panel shape. See B5 in
   end-ride-mockups.html for the source design. */
.nav-end-cta {
  position: absolute;
  inset: 0;
  background: #22c55e;
  color: #052e16;
  border: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 14px 18px;
  cursor: pointer;
  font-family: inherit;
  transition: background 120ms;
}
/* Thumbs-up inlined next to the "Good ride." headline. Sized in em
   so the icon tracks the headline's font-size exactly — no separate
   tuning when the headline scale changes (accessibility text-size,
   future variant headlines). currentColor inherits the headline's
   dark-green so the icon visually belongs to the text rather than
   reading as decoration. flex-shrink: 0 stops the thumb from
   collapsing when the headline + thumb lockup runs out of width. */
.nav-end-cta-thumbs {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  color: currentColor;
}
.nav-end-cta:hover { background: #16a34a; }
.nav-end-cta[hidden] { display: none; }
/* Type scale on the End ride CTA — bumped ~30 % over the original
   so the green panel reads with the same visual weight as a hero
   metric on the data card. Eyebrow 10 → 13, headline 22 → 29, tap
   hint 11 → 14, chevron SVG 14 → 18 px so the icon stays in
   proportion with the new label size. */
.nav-end-cta-eyebrow {
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  opacity: 0.85;
}
.nav-end-cta-headline {
  font-size: 29px;
  font-weight: 700;
  letter-spacing: 0.04em;
  /* Inline-flex so the headline text + thumbs-up sit as a single
     centred lockup. align-items: center keeps the thumb visually
     aligned with the cap-height of the heavy headline — baseline
     alignment is unreliable with SVGs (no natural baseline). */
  display: inline-flex;
  align-items: center;
  gap: 12px;
}
.nav-end-cta-tap {
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  opacity: 0.95;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.nav-end-cta-tap svg { width: 18px; height: 18px; }

/* When the wrap is in the "arrived" state, leave the data card in the
   layout but invisible (so the wrap keeps its data-card height) and
   collapse the off-route banner entirely. The CTA above sits over the
   card via position:absolute. */
.nav-card-wrap.arrived .nav-card     { visibility: hidden; pointer-events: none; }
.nav-card-wrap.arrived .nav-offroute { display: none; }

/* Arrival animation — sweep a clip-path from left to right over 500 ms
   so the green CTA fills in like a progress-bar reaching the end of
   its track. ease-out makes the leading edge decelerate as it covers
   the right half, which reads as "settling into place". */
.nav-card-wrap.arrived .nav-end-cta {
  /* 2-second sweep — long enough that the green fill reads as a proper
     finishing beat, not a quick fade. ease-out decelerates the leading
     edge into the right side so the slowdown lands as "settling into
     place" rather than feeling like a hung animation. */
  animation: nav-arrive-sweep 2000ms ease-out forwards;
}
@keyframes nav-arrive-sweep {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* Off-route banner — big red attention strip attached to the top of
   the data card. Collapsed (height 0, opacity 0) by default; gaining
   the `visible` class grows it open via max-height transition. The
   .nav-hud's bottom-anchor in landscape (and the planner-hidden
   nav-hud in portrait being bottom-anchored too) means the wrapper
   visually grows UPWARD when the banner appears. */
.nav-offroute {
  background: #dc2626;
  color: #ffffff;
  /* Animation: max-height + opacity. Both transition together so the
     banner fades in slightly behind the slide so the rider's eye
     latches onto the motion. */
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  transition: max-height 280ms ease-out, opacity 200ms ease-out;
}
.nav-offroute.visible {
  max-height: 140px;  /* generous cap above the natural ~88 px height */
  opacity: 1;
}
.nav-offroute-inner {
  padding: 14px 20px;
  display: flex;
  align-items: center;
  gap: 16px;
}
.nav-offroute-arrow {
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  /* Rotated to point back to the route. The JS sets
     --recover-rotation on :root, same as the previous pill design. */
  transform: rotate(var(--recover-rotation, 0deg));
  transition: transform 220ms ease;
}
.nav-offroute-arrow path { fill: #ffffff; }

/* Reload spinner — visible only while the reroute API call is in
   flight (.rerouting-active on the banner). Same 52 px footprint as
   the recovery arrow it replaces, so the row height doesn't jump
   when the icon swaps. Spins at one revolution per second, clockwise
   (positive rotation = clockwise in SVG coords). */
.nav-offroute-reload {
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  color: #ffffff;
  display: none;
}
@keyframes nav-offroute-reload-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* Big rerouting headline — same height as the .nav-offroute-distance
   it replaces so the banner stays the same physical size. Mono caps
   with a touch of letter-spacing so REROUTING reads with authority,
   not as a generic system label. */
.nav-offroute-headline {
  font-family: var(--font-mono);
  font-size: 24px;
  font-weight: 700;
  letter-spacing: 0.16em;
  color: #ffffff;
  line-height: 1;
  text-transform: uppercase;
  display: none;
}

/* Rerouting-active state: hide the countdown stack + arrow, show the
   spinner + big headline. Toggled by maybeRefreshBridge() in app.js
   the moment the rider crosses the 100 m reroute threshold AND
   there isn't already a bridge polyline on the map. */
.nav-offroute.rerouting-active .nav-offroute-arrow,
.nav-offroute.rerouting-active .nav-offroute-text {
  display: none;
}
.nav-offroute.rerouting-active .nav-offroute-reload {
  display: block;
  animation: nav-offroute-reload-spin 1s linear infinite;
}
.nav-offroute.rerouting-active .nav-offroute-headline {
  display: block;
}
.nav-offroute-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
  line-height: 1;
  min-width: 0;
}
.nav-offroute-distance {
  font-family: var(--font-mono);
  font-size: 28px;
  font-weight: 700;
  color: #ffffff;
  line-height: 1;
}
.nav-offroute-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.22em;
  color: #ffffff;
  text-transform: uppercase;
  opacity: 0.92;
  line-height: 1;
}
/* Nav-mode HUD card (variant G from nav-hud-mockups.html). One panel with
   amber-accented left border. Top row: oversized hero metric + small
   unit label. Middle: dashed-line footer showing two secondary metrics
   (one with an amber clock-time when ETA lands in a footer slot).
   Bottom: thin amber progress bar.
   The card is tappable — tapping cycles which metric sits in the hero
   slot. See cycleMetricRotation() in app.js. */
.nav-hud .nav-card {
  /* Background + border + border-radius all live on .nav-card-wrap
     now (matches the planner panel). The card itself is just the
     padded content area. */
  /* position: relative anchors absolutely-positioned descendants
     (.nav-speedlimit) to the CARD instead of the wrap. Without this
     the speed-limit sign rides on the wrap, and the wrap visibly grows
     upward when the off-route banner expands — so the sign jumps up
     by ~88 px the moment the rider drifts off route. Anchoring to the
     card keeps the sign welded to the card's top-right corner. */
  position: relative;
  padding: 18px 22px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: auto;
  cursor: pointer;
  /* Touch / tap polish: kill the iOS grey flash, block accidental text
     selection from a long-press, keep focus rings off the rim of the
     card itself (slot contents still get them when relevant). */
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  outline: none;
  transition: transform 120ms ease;
}
.nav-hud .nav-card:active {
  /* Subtle press feedback — brief shrink. The amber border flash now
     lives on .nav-card-wrap via :has(.nav-card:active) so it follows
     the rounded silhouette cleanly. */
  transform: scale(0.985);
}
.nav-card-hero {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  /* Reserve room on the right for the speed-limit sign (56-px diameter
     + 16-px gap). The space stays reserved even when no sign is shown
     so the hero doesn't reflow each time the rider turns onto a road
     with/without a maxspeed tag. */
  padding-right: 72px;
}

/* Speed-limit sign — Danish road-sign style. Absolutely positioned in
   the top-right of the data card. Hidden via [hidden] when the road
   under the rider has no maxspeed data; otherwise the inner div's
   textContent is the km/h value (e.g. "80"). */
.nav-speedlimit {
  position: absolute;
  top: 14px;
  right: 16px;
  z-index: 2;
  pointer-events: none;     /* taps still hit the card behind */
}
.nav-speedlimit[hidden] { display: none; }
.nav-speedlimit-sign {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: #ffffff;
  border: 6px solid #d40d2c;     /* Danish road-sign red */
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 800;
  color: #0a0a0a;
  letter-spacing: -0.02em;
  /* Subtle lift so the sign reads as a sticker on top of the card. */
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.28);
}
.nav-hero-value {
  font-family: var(--font-mono);
  font-size: 56px;
  font-weight: 700;
  color: var(--text);
  line-height: 1;
  white-space: nowrap;
}
.nav-hero-value .nav-hero-unit {
  /* Double the previous 16 px — the rider's most-used unit (km / min /
     ETA) reads from further away now. Uppercased via text-transform so
     the JS still ships bare 'km'/'min'/'ETA' strings; the same rule
     also catches 'm' (metres) when within 2.5 km of the destination. */
  font-size: 32px;
  font-weight: 700;
  color: var(--text-faint);
  margin-left: 8px;
  text-transform: uppercase;
}

.nav-hero-label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  color: var(--text-faint);
  text-transform: uppercase;
}
.nav-card-footer {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 14px;
  padding-top: 10px;
  border-top: 1px dashed var(--line);
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-dim);
}
.nav-card-footer .nav-eta { color: var(--amber); }
/* Progress bar row: thin amber track with a big amber arrow riding the
   leading edge and a grey finish flag at the right end. Sits 4 px below
   the footer text (parent .nav-card gap is 10 px; -6 px margin trims it). */
.nav-card-progress {
  position: relative;
  margin-top: -6px;
  height: 24px;
  display: flex;
  align-items: center;
}
.nav-card-progress-track {
  flex: 1;
  height: 4px;
  background: var(--bg-elev-hi);
  border-radius: 999px;
  overflow: hidden;
}
.nav-card-progress-fill {
  height: 100%;
  background: var(--amber);
  width: 0%;
  transition: width 400ms ease;
}
.nav-card-progress-arrow {
  position: absolute;
  left: 0%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 24px;
  height: 24px;
  pointer-events: none;
  transition: left 400ms ease;
}
.nav-card-progress-arrow path {
  fill: var(--amber);
  stroke: var(--bg-panel);
  stroke-width: 1;
  stroke-linejoin: round;
}
.nav-card-progress-flag {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translate(-2px, -50%);
  width: 14px;
  height: 14px;
  pointer-events: none;
}
.nav-card-progress-flag line {
  stroke: var(--text-dim);
  stroke-width: 1.4;
  stroke-linecap: round;
}
.nav-card-progress-flag path {
  fill: var(--text-dim);
}

/* Hide the planner while ride mode is active. The brand menu and search
   stay visible so the user can still interact with the app. */
body.ride-active .panel { display: none; }
body.ride-active .map-hint { display: none; }

@media (max-width: 800px) {
  /* No background, no edge-to-edge chrome — pills float inset from the
     screen edges like the topbar. Bottom inset accounts for the iOS home
     indicator. */
  .nav-hud {
    left: 16px;
    right: 16px;
    width: auto;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   Toast
   ───────────────────────────────────────────────────────────────────────── */
/* Toast — one element, five flavours. Driven by a `kind` class added by
   the toast() helper in app.js. Default kind is neutral panel+amber-
   border for backwards compatibility; specific kinds (error / warning /
   success / major / loading) override with solid-fill treatments
   matching E1 / E3 / I1 / E2 / I2 in `info-mockups.html`. */
/* `display: flex` on .toast below would otherwise beat the user-agent
   `[hidden] { display: none }` rule by specificity — the result was
   toasts that never disappeared after their auto-hide timer fired.
   Explicit override here pins display: none on the hidden state. */
.toast[hidden] { display: none; }
.toast {
  position: absolute;
  /* 80 px nominal gap below the viewport top, then add the safe-area
     inset so on iOS the toast clears the notch / dynamic-island band
     AND clears the topbar (which is itself pushed down by the same
     inset). Without `env(...)` the toast sat ON TOP of the search bar
     in PWA portrait. */
  top: calc(80px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-panel);
  border: 1px solid var(--amber);
  color: var(--text);
  padding: 10px 16px;
  border-radius: var(--radius);
  font-size: 13px;
  z-index: 20;
  box-shadow: var(--shadow);
  animation: toast-in 0.2s ease-out;
  display: flex;
  align-items: center;
  gap: 10px;
  /* Base cap bumped 30 % (360 → 470 px) so the two-line major banner
     reads as a clear top-of-viewport announcement rather than a
     squeezed pill. On mobile this lifts the cap above the viewport
     width on most devices, so the per-screen `calc(100vw - 16px)`
     override below takes over and gives near-edge-to-edge width. */
  max-width: min(470px, calc(100vw - 32px));
}
/* Mobile: match the topbar's exact width — the strip running from the
   burger menu on the left to the recenter button on the right. The
   topbar uses `left: 16+safe-left, right: 16+safe-right`, so the
   toast subtracts the same 32 px (16 px each side) plus the safe-area
   insets. Result: toast aligns flush with the topbar's outer edges. */
@media (max-width: 800px) {
  .toast {
    box-sizing: border-box;
    width: calc(100vw
                - 32px
                - env(safe-area-inset-left, 0px)
                - env(safe-area-inset-right, 0px));
    max-width: none;
  }
}
.toast .toast-icon { flex-shrink: 0; display: block; }

/* E1 — error: white on solid red. */
.toast.error {
  background: #dc2626;
  border-color: #dc2626;
  color: #ffffff;
}
.toast.error .toast-icon { color: #ffffff; }

/* E3 — warning: very dark grey on solid amber. */
.toast.warning {
  background: #f59e0b;
  border-color: #f59e0b;
  color: #1c1917;
}
.toast.warning .toast-icon { color: #1c1917; }

/* I1 — success: white on solid green. */
.toast.success {
  background: #16a34a;
  border-color: #16a34a;
  color: #ffffff;
}
.toast.success .toast-icon { color: #ffffff; }

/* E2 — major banner: white on solid red, taller, two-line layout with
   uppercase mono title above a regular body line. Used for hard
   blockers like "Route failed". The .toast-text wrapper holds the
   stacked title + body so the icon stays vertically centred next to
   the whole block. */
.toast.major {
  background: #dc2626;
  border-color: #dc2626;
  color: #ffffff;
  padding: 14px 18px;
  gap: 14px;
  align-items: center;
}
.toast.major .toast-icon { color: #ffffff; }
/* Larger warning glyph for the major banner — double the standard
   toast-icon size so it carries the right visual weight against the
   two-line title + body block beside it. */
.toast.major .toast-icon svg {
  width: 36px;
  height: 36px;
}
.toast.major .toast-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1.25;
  min-width: 0;
}
.toast.major .toast-title {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  opacity: 0.92;
}
.toast.major .toast-body {
  font-size: 14px;
  font-weight: 500;
}

/* major-success — same big two-line banner as .toast.major but in
   success-green. Used for positive outcomes that deserve more visual
   weight than the small green pill (e.g. GPX imported, with mode +
   total distance shown beneath the title). Inherits all layout from
   .toast.major above (gap, padding, icon-size, two-line text rules).
   Only the colours and the icon-tint differ. */
.toast.major-success {
  background: #16a34a;
  border-color: #16a34a;
  color: #ffffff;
  padding: 14px 18px;
  gap: 14px;
  align-items: center;
}
.toast.major-success .toast-icon { color: #ffffff; }
.toast.major-success .toast-icon svg {
  width: 36px;
  height: 36px;
}
.toast.major-success .toast-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1.25;
  min-width: 0;
}
.toast.major-success .toast-title {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  opacity: 0.92;
}
.toast.major-success .toast-body {
  font-size: 14px;
  font-weight: 500;
}

/* I2 — loading: amber left-accent + spinner. Stays open until another
   toast replaces it or hideToast() is called. No animation easing
   from toast-in interferes with the spinner's rotation. */
.toast.loading {
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-left: 3px solid var(--amber);
  color: var(--text);
}
.toast.loading .toast-spinner {
  width: 14px;
  height: 14px;
  border: 2px solid var(--bg-elev-hi);
  border-top-color: var(--amber);
  border-radius: 50%;
  animation: toast-spin 0.8s linear infinite;
  flex-shrink: 0;
}
@keyframes toast-spin {
  to { transform: rotate(360deg); }
}

@keyframes toast-in { from { transform: translate(-50%, -10px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } }

/* ─────────────────────────────────────────────────────────────────────────
   Map markers (custom)
   ───────────────────────────────────────────────────────────────────────── */
.map-marker {
  width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  background: var(--amber);
  color: var(--on-amber);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 12px;
  border-radius: 50%;
  border: 2px solid var(--on-amber);
  box-shadow: 0 0 0 2px var(--amber), 0 4px 10px rgba(0,0,0,0.5);
  cursor: grab;
}
.map-marker.start { background: var(--green); box-shadow: 0 0 0 2px var(--green), 0 4px 10px rgba(0,0,0,0.5); }
.map-marker.end   { background: var(--red); color: white; box-shadow: 0 0 0 2px var(--red), 0 4px 10px rgba(0,0,0,0.5); }
/* Finish-flag icon inside the destination marker — same shape as the
   flag at the right end of the ride-mode progress bar. Uses currentColor
   so it picks up the marker's white (red pin) or near-black (green pin)
   foreground automatically. */
.map-marker-flag {
  width: 14px;
  height: 14px;
  display: block;
  /* Shift right 3 px so the pole sits closer to the marker's centre —
     otherwise the pole-on-left + banner-on-right shape reads as off to
     the left in a circular pin. */
  transform: translateX(3px);
}
/* End marker showing a home / work glyph instead of the finish flag
   (destination matches a saved place). Sizes the inline SVG to fit
   the 28-px pin neatly; currentColor inherits the marker's white
   foreground (red pin) or near-black foreground (green .go pin). */
.map-marker.end.end-saved > svg {
  width: 16px;
  height: 16px;
  display: block;
}
.map-marker-flag line {
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
}
.map-marker-flag path {
  fill: currentColor;
}
/* Destination marker turns Start-ride green once a route is computed,
   signaling "route ready, go". Reverts to red when the route is cleared. */
.map-marker.end.go {
  background: #22c55e;
  color: #0a0a0a;
  box-shadow: 0 0 0 2px #22c55e, 0 4px 10px rgba(0,0,0,0.5);
}
/* Ride mode: waypoint markers the rider has crossed get tinted the
   same Tailwind green-500 as the completed-line layer, so a passed
   stop visually "merges" into the green trail rather than sticking
   out as an amber / red pin. updatePassedWaypoints() in app.js
   toggles the `.passed` class on every GPS tick (driven from
   setCompletedOnMap, which already runs on every tick to grow the
   completed line). The selector list explicitly covers each base
   role (.start / .end / .end.go / default) so this rule wins on
   specificity AND source order. */
.map-marker.passed,
.map-marker.start.passed,
.map-marker.end.passed,
.map-marker.end.go.passed {
  background: #22c55e;
  color: #0a0a0a;
  box-shadow: 0 0 0 2px #22c55e, 0 4px 10px rgba(0,0,0,0.5);
}

/* Stacked distance/time label floating above the destination marker
   on the map once a route is computed. Solid Start-ride green with a
   small diamond pointer underneath anchoring it to the green pin. */
.map-destination-label {
  position: relative;
  display: inline-block;
  /* width: max-content prevents the label from stretching to the width
     of MapLibre's marker wrapper when the inner block divs would
     otherwise make it block-level. */
  width: max-content;
  background: #22c55e;
  border: 1px solid #22c55e;
  box-shadow: 0 4px 14px rgba(34, 197, 94, 0.25), 0 2px 8px rgba(0, 0, 0, 0.3);
  padding: 8px 14px;
  border-radius: 4px;
  font-family: var(--font-mono);
  white-space: nowrap;
  text-align: left;
  color: #0a0a0a;
  pointer-events: none;
  /* Fade-in only. We intentionally do NOT transition `transform` —
     MapLibre 4 applies its own transform inline on the marker element
     to keep it pinned to lat/lon as the camera moves; transitioning
     `transform` would animate every one of those tweaks, making the
     label dance around during pan/zoom. */
  opacity: 0;
  transition: opacity 220ms ease;
}
.map-destination-label.visible {
  opacity: 1;
}
.map-destination-label::after {
  content: '';
  position: absolute;
  bottom: -5px;
  left: 50%;
  transform: translateX(-50%) rotate(45deg);
  width: 10px;
  height: 10px;
  background: #22c55e;
}
.map-destination-label .num {
  font-size: 18px;
  font-weight: 700;
  color: #0a0a0a;
  line-height: 1.05;
}
.map-destination-label .num .unit {
  font-size: 11px;
  color: rgba(10, 10, 10, 0.6);
  margin-left: 2px;
}
.map-destination-label .time {
  font-size: 11px;
  color: rgba(10, 10, 10, 0.7);
  letter-spacing: 0.05em;
  margin-top: 2px;
}
.map-marker.gps {
  background: #38bdf8;
  box-shadow: 0 0 0 2px #38bdf8, 0 0 20px #38bdf8;
  /* Sized to match the waypoint markers (28 px) so the rider's
     position reads with the same visual weight as the destinations
     it sits alongside, AND so a home/work glyph has room to embed
     inside it when the rider is near a saved place. */
  width: 28px; height: 28px;
  border-width: 3px;
  /* Flex-centre the inner icon slot — empty most of the time, filled
     with the home or work glyph by updateGpsMarkerIcon() when the
     rider's current position is within range of a saved place. */
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--on-amber);
  /* Touches pass straight through to the map canvas — the GPS marker
     is never directly interactive (it follows GPS), and intercepting
     touches here breaks panning when the user's finger reflexively
     lands on the rider's position. Especially important in ride mode
     where the marker grows to a 66 px arrow that's right under the
     rider's thumb. */
  pointer-events: none;
}
.map-marker.gps > svg {
  /* Home/work glyph inlined inside the dot when near a saved place.
     Fits comfortably inside the 28 px - 6 px (border) = 22 px inner
     area, with breathing room. currentColor inherits the dot's
     foreground (dark) so the icon contrasts cleanly against the
     blue. */
  width: 14px;
  height: 14px;
  display: block;
}

/* Standalone home / work pins on the map. Rendered only when the
   rider's current GPS is NOT within range of the corresponding saved
   place — once the rider arrives at home or work, the standalone
   pin hides and its glyph appears inside the GPS dot instead.
   Quiet dark chip with the same 28 px footprint as waypoint markers
   so they sit visually as personal landmarks, not destinations. */
.saved-place-marker {
  width: 28px;
  height: 28px;
  background: rgba(20, 20, 20, 0.88);
  border: 1.5px solid rgba(255, 255, 255, 0.22);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Hardcoded near-white: the chip stays dark in both themes so the
     pin reads as a quiet landmark on a varied map. var(--text) flips
     to near-black in light mode and would make the icon disappear
     against the dark background — anchor the foreground instead. */
  color: #f5f5f4;
  /* Clickable — tapping a home / work pin adds a waypoint at that
     place's coordinates (snapped to the nearest road). Click handler
     lives on the element itself in updateSavedPlaceMarkers() in
     app.js so taps don't bubble to the map's add-waypoint handler. */
  pointer-events: auto;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
  /* IMPORTANT: do NOT transition `transform` here. MapLibre repositions
     every Marker by writing transform: translate3d(...) on every map
     frame; a transform transition would turn each of those positional
     updates into an animation, causing the pin to visibly lag / slide
     behind the map as it pans + zooms. Border-color is safe to ease. */
  transition: border-color 120ms ease;
}
.saved-place-marker:hover { border-color: var(--amber); }
/* Active-state visual feedback uses filter instead of transform so we
   don't reintroduce the transform-during-pan lag described above. */
.saved-place-marker:active { filter: brightness(0.85); }
/* Light mode: invert the chip — light fill with a dark icon so the
   pin reads naturally against the bright light-mode map style. */
html.theme-light .saved-place-marker {
  background: rgba(245, 245, 244, 0.92);
  border-color: rgba(20, 20, 20, 0.22);
  color: #1c1917;
}
.saved-place-marker > svg {
  width: 16px;
  height: 16px;
  display: block;
}
/* Favourite-destination variant — same chip + same neutral foreground
   as the home / work pins. The icon itself (outline star) is what
   distinguishes it; nothing else changes. Slight opacity drop so a
   screen with several favourites doesn't compete with home/work. */
.saved-place-marker.favourite {
  opacity: 0.85;
}
.saved-place-marker.favourite:hover {
  opacity: 1;
}
/* Ride mode: dot becomes a big blue arrow pointing in the direction of
   travel. Rotation is driven by the MapLibre Marker rotation API (with
   rotationAlignment: 'map') so the arrow's geometry stays locked to the
   true compass heading; combined with bearing-locked camera follow, the
   arrow always appears to point "up" on screen = forward. */
.map-marker.gps.arrow {
  background: transparent;
  box-shadow: none;
  border: none;
  /* 50% bigger than the original 44 px chevron — at speed the arrow
     needs to be readable in a glance through gloves + sunlight. */
  width: 66px;
  height: 66px;
  cursor: default;
  /* Ride mode: the rider's position takes precedence over the
     destination + waypoint pins. MapLibre stacks markers by DOM
     order (waypoints get re-created on every renderWaypoints() pass,
     which can drop them above the GPS marker that was added once at
     boot), so we lift z-index here to override that ordering and
     keep the blue arrow always visible. Planner-mode .map-marker.gps
     stays at the default z-index so waypoint pins remain tappable
     on top of the dot. */
  z-index: 5;
}
.map-marker.gps.arrow svg {
  display: block;
  width: 100%;
  height: 100%;
  filter: drop-shadow(0 0 8px rgba(56, 189, 248, 0.7));
}

/* ─────────────────────────────────────────────────────────────────────────
   Responsive: stack panels on narrow screens
   ───────────────────────────────────────────────────────────────────────── */
@media (max-width: 800px) {
  /* Burger + theme toggle on the left; recenter on the right. Search
     fills the gap. Same 4-column layout as desktop, just a tighter gap. */
  .topbar { grid-template-columns: auto auto 1fr auto; gap: 8px; }
  /* Compact heights on mobile but still uniform across all three items. */
  .topbar .brand,
  .topbar .search input,
  .topbar-fab { height: 40px; }
  .topbar-fab { width: 40px; font-size: 16px; }
  .brand { width: 40px; }
  .brand-menu { min-width: 220px; }
  /* Bottom-sheet style: edge-to-edge, anchored to the bottom of the screen,
     rounded only on the top corners. Safe-area moved to inner padding so the
     panel background touches the home-indicator strip. */
  .panel {
    left: 0;
    right: 0;
    /* Now that --app-h gives us the real visual viewport height,
       bottom: 0 actually reaches the physical screen edge. A small
       padding-bottom (8px) leaves a hair of clearance so the Compute
       button isn't sitting right on the home-indicator pill. */
    bottom: 0;
    padding-bottom: 8px;
    width: auto;
    max-height: 50dvh;
    border-radius: 16px 16px 0 0;
    border-left: none;
    border-right: none;
    border-bottom: none;
    box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
  }
  .fab-stack {
    top: auto;
    bottom: calc(50dvh + 28px + env(safe-area-inset-bottom, 0px));
  }
  /* Compact mode buttons: hide the sub-caption and halve the vertical
     padding so the row takes less of the precious mobile panel real estate. */
  .mode-btn { padding: 7px 8px; gap: 2px; }
  .mode-sub { display: none; }
}

/* ─────────────────────────────────────────────────────────────────────────
   Short landscape viewport (phone in landscape OR a desktop window
   that's wider than tall and ≤ 500 px tall):
   The portrait mobile rules above turn the planner into a bottom-sheet
   capped at 50dvh, which on a ~390-px-tall landscape phone leaves only
   ~195 px for the planner and squashes the map into a thin strip at
   the top. Switch the planner to a side-sheet that fills the left half
   of the viewport, drop the standalone topbar theme toggle (it moves
   into the burger dropdown), and re-anchor the ride HUD.
   The desktop @media above is gated on `min-height: 501 px`, so a
   wide-and-short desktop window falls through to these rules too — the
   collapsed-topbar + left-side-sheet treatment is the right answer for
   any landscape viewport that's short, regardless of device.
   ───────────────────────────────────────────────────────────────────────── */
@media (orientation: landscape) and (max-height: 500px) {
  /* Topbar — anchored to the LEFT HALF of the viewport, sitting
     visually INSIDE the planner card (see .panel rule below which
     extends the card chrome to the top of the screen). Only three
     items are visible in the row: BURGER + SEARCH + RECENTER.
     Everything else (theme, load route, save route, import GPX,
     settings, feedback, sign out) lives in the burger dropdown.
     `right: calc(50vw + 16px)` stops the topbar at the right edge
     of the left half + a 16 px inset so the recenter button
     doesn't crowd the card's right border.
     The desktop-only action clusters (.topbar-actions-left +
     -right) are already hidden on phone landscape by the base
     `.topbar > .topbar-actions { display: none }` rule — the
     desktop layout is gated on `hover: hover` + `pointer: fine`
     which a phone in landscape doesn't satisfy, so the clusters
     never render here. The only override we need below is to
     suppress the standalone theme toggle. */
  .topbar {
    left:  calc(16px + env(safe-area-inset-left, 0px));
    right: calc(50vw + 16px);
  }
  /* Theme toggle suppressed on landscape phone in planner mode —
     theme stays reachable via the burger menu's Appearance row
     (which `.menu-item.menu-theme` un-hides on the line below).
     Ride mode keeps the standalone toggle since the burger menu
     fades during a ride. Mirrors the same pattern as the
     `@media (max-width: 800px)` block for portrait phone. */
  body:not(.ride-active) .theme-toggle { display: none; }
  .menu-item.menu-theme { display: flex; }

  /* Belt-and-suspenders for the topbar-inside-the-card layout:
     whenever the topbar sits inside the planner card (this @media
     gate), force the inline action clusters off and the burger
     dropdown back on — `!important` so they win regardless of
     any desktop @media leakage. The desktop @media above is
     already gated on `min-height: 501 px`, so in practice these
     don't override anything — but they make the collapse robust
     against future tweaks to that gate (e.g. if we ever loosen
     `(hover: hover) and (pointer: fine)` for touch laptops).
     Without this, a regression in those gates would silently
     leave a tall, wide dropdown plus inline FAB clusters fighting
     for space in a topbar that's only half a viewport wide. */
  .topbar > .topbar-actions-left,
  .topbar > .topbar-actions-right { display: none !important; }
  .brand-burger   { display: inline-flex !important; }
  .brand-wordmark { display: none        !important; }
  /* Burger dropdown must be reachable — desktop @media normally
     force-hides #brand-menu with `display: none !important`. We
     override only the OPEN state (no [hidden] attribute) so the
     usual JS-driven `[hidden]` toggle still hides the panel when
     closed. Higher specificity than `#brand-menu` alone, and
     same `!important` weight, so this wins when the menu is open. */
  #brand-menu[hidden]      { display: none !important; }
  #brand-menu:not([hidden]) { display: flex !important; }
  /* Ride mode: planner is hidden and the nav-hud now spans the full
     bottom edge (not a left side-sheet anymore), so nothing claims
     the left strip. Snap the topbar back to the viewport's left
     edge so the theme toggle — which is the only visible topbar
     element in ride mode and pinned to column 1 — lands in the
     top-left corner instead of floating around the middle of the
     screen. All four edges use the same 8 px breathing-room as the
     data card and pause FAB so theme / pause / nav-hud share an
     identical inset frame around the screen. */
  body.ride-active .topbar {
    top:   calc(8px + env(safe-area-inset-top, 0px));
    left:  calc(8px + env(safe-area-inset-left, 0px));
    right: calc(8px + env(safe-area-inset-right, 0px));
  }
  /* Pause FAB (top-right) — match the same 8 px inset so theme +
     pause + the data card share one uniform frame around the
     screen edges in landscape ride mode. Base rule is 16 px which
     looks oversized once everything else is at 8 px. */
  body.ride-active .nav-exit-fab {
    top:   calc(8px + env(safe-area-inset-top, 0px));
    right: calc(8px + env(safe-area-inset-right, 0px));
  }

  /* Road-name pill — re-centre across the VISIBLE band (between the
     left + right safe-area insets) rather than the geometric
     viewport. On a phone in landscape the notch / dynamic-island
     pushes safe-area-inset-left up to ~40 px while inset-right
     stays at 0; without this shift the pill sits half a notch left
     of where the rider perceives "centre". The translateX(-50%)
     keeps the pill's own width centred on the shifted anchor.
     Widen the max-width allowance too — the topbar in ride mode
     only has the theme toggle visible (top-left), so we only need
     to clear ~80 px on the left + nothing on the right, vs. the
     base 220 px reservation. */
  .road-name-pill {
    left: calc(50% + (env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px)) / 2);
    max-width: calc(100vw
                    - env(safe-area-inset-left, 0px)
                    - env(safe-area-inset-right, 0px)
                    - 120px);
  }

  /* Compact mode buttons — same treatment as portrait mobile. The
     narrower side-sheet has even less horizontal room for the row,
     so dropping the sub-caption and tightening padding is a must. */
  .mode-btn { padding: 7px 8px; gap: 2px; }
  .mode-sub { display: none; }

  /* Waypoint list — fills the vertical space left between the
     mode/extra sections above and the summary action row below.
     Flexbox sizing replaces the portrait 88 px max-height cap.
     The .panel is already a flex column; here we make the
     waypoints section grow to fill remaining height, and the
     list inside it grow to fill the section's remaining height
     (after the "Use my location as start" header). The list is
     the only scrolling region so the panel itself drops its
     own overflow; otherwise we'd get nested scroll containers
     that fight each other on iOS.
     `min-height: 0` on the flex items is required for them to
     shrink below their intrinsic content height — without it
     the list overflows its parent and the panel's hidden
     overflow clips the bottom rows out of view. */
  .panel { overflow: hidden; }
  .panel-section.waypoints {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  .waypoint-list {
    flex: 1 1 auto;
    max-height: none;
    min-height: 0;
  }

  .panel {
    /* Fills the left half of the viewport (between the device's
       notch on the left and the 50 % vertical line). No floating
       chrome — the card is a fixed left side-panel, not a popup.
       The map fills the right half. The topbar sits visually
       inside the card at the top (see .topbar override above).
       - top: 0 + safe-area-inset-top → chrome extends behind the
         iOS status bar / Dynamic Island band so the card reads as
         a single edge-to-edge surface.
       - bottom: 0 → reaches the bottom of the viewport (the inner
         padding-bottom handles the home-indicator inset).
       - left: env(safe-area-inset-left) → the chrome respects the
         landscape notch on the left edge. Without this the card
         background extended under the notch, and the first column
         of section content (which sits at +16 px from the chrome's
         left edge via .panel-section padding) was nudged behind
         the notch on Dynamic-Island / hardware-cutout phones.
       - right: 50vw → divider stays exactly at the viewport's
         50 % line regardless of how wide the notch reports. Map
         keeps its full right half.
       - With this anchoring, the leftmost edge of the card's
         content (panel-section padding: 16 px) aligns with the
         topbar's burger button (also at safe-left + 16 px).
       - padding-top: 60 px + safe-top → first section clears the
         topbar overlay (44 px topbar + 16 px breathing room).
       - No border-radius, no shadow, no margin — the only visual
         separator from the map is a 1 px right border. */
    top:    0;
    bottom: 0;
    /* Card chrome extends to the viewport's left edge so nothing
       of the map shows behind the notch / Dynamic Island. The
       chrome paints in two zones via a hard-stop gradient:
       - 0 → safe-left:  SOLID BLACK (the notch / cutout band)
       - safe-left → end: normal --bg-panel
       The black band keeps the notch reading as "device chrome"
       instead of a coloured slab of the planner card pushed under
       the hardware cutout. Content (panel-section) is shifted
       right by `padding-left: env(safe-area-inset-left)` so the
       leftmost item still lands at safe-left + 16 px, matching
       the topbar burger position. */
    left:   0;
    right:  50vw;
    width:  auto;
    max-height: none;
    padding-top:    calc(60px + env(safe-area-inset-top, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    padding-left:   env(safe-area-inset-left, 0px);
    background:
      linear-gradient(
        to right,
        #000 0,
        #000 env(safe-area-inset-left, 0px),
        var(--bg-panel) env(safe-area-inset-left, 0px),
        var(--bg-panel) 100%
      );
    border: none;
    border-right: 1px solid var(--line);
    border-radius: 0;
    box-shadow: none;
    overflow-y: auto;
  }
  /* The "Tap the map to add a destination" hint is anchored to the
     planner's top edge — in landscape that lands awkwardly to the
     right of a 340-px side-sheet. Drop it. */
  .map-hint { display: none; }
  /* Ride HUD: move the data card into the freed-up left side-sheet
     slot (same footprint the planner had during planning). Keeps the
     km / time / progress info glanceable alongside the map instead of
     squashed into a sliver at the bottom of a short landscape view —
     and the previous bottom-center version was getting clipped on
     some phones. The card itself is content-sized; the container just
     defines the visible bounding box. Off-route pill (when shown)
     stacks above the card via the existing flex-column. */
  .nav-hud {
    /* Anchored bottom edge in landscape, spanning the full visible
       width minus an 8 px breathing-room on each side (on top of the
       device safe-area insets). Going edge-to-edge lets the card
       split into a two-column layout: big hero metric on the left
       half, secondary stats + progress bar on the right half.
       Previous version capped width at min(400px, 50vw) which left
       the card hugging just the left corner; now the wrap stretches
       so the right column lands far from the rider's eye when the
       phone is mounted near the bars, but the data is still readable
       at speed. */
    top:    auto;
    bottom: calc(8px + env(safe-area-inset-bottom, 0px));
    left:   calc(8px + env(safe-area-inset-left, 0px));
    right:  calc(8px + env(safe-area-inset-right, 0px));
    width:  auto;
    transform: none;
  }
  /* Internal grid: hero on the left half (full height), with the
     footer + progress bar stacked on the right half. */
  .nav-hud .nav-card {
    /* Pad slightly tighter than desktop to suit the short row. */
    padding: 14px 20px;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto auto;
    column-gap: 24px;
    row-gap: 6px;
  }
  .nav-hud .nav-card-hero {
    /* Left half — spans both rows so the big number reads as a
       single block. Centred vertically so it lines up with the
       midpoint of the right-side stack rather than sitting flush
       with the footer row's baseline. */
    grid-row: 1 / span 2;
    grid-column: 1;
    align-self: center;
  }
  .nav-hud .nav-card-footer {
    grid-row: 1;
    grid-column: 2;
    /* Drop the dashed border that lives on the footer's top edge in
       portrait. In landscape the footer sits next to the hero (not
       below it) and the dashed line reads as a stray separator
       floating above the progress bar in the right column. */
    border-top: none;
    padding-top: 0;
  }
  .nav-hud .nav-card-progress { grid-row: 2; grid-column: 2; }
  /* Hero font drops a touch so a value + unit fits half a phone
     screen comfortably — but stays bigger than the portrait-mobile
     44 px since the left half is now wider than the old 50vw cap. */
  .nav-hud .nav-hero-value { font-size: 48px; }
  /* Speed-limit sign — move it OFF the top-right corner of the card
     (where it was sitting on top of the progress bar in the right
     half) onto the right edge of the LEFT half. The card uses
     `grid-template-columns: 1fr 1fr` with `column-gap: 24px`, so
     the gap sits centred on the card's 50% line — meaning the
     left column ends at `50% - 12px`. Anchoring the sign's right
     edge there with `right: calc(50% + 12px)` lands it cleanly
     between the hero number and the progress column with no
     overlap. Vertically centred so it tracks the centred hero
     instead of floating at the card's top edge. */
  .nav-hud .nav-speedlimit {
    top: 50%;
    right: calc(50% + 12px);
    transform: translateY(-50%);
  }
}
