/* ===== CSS Variables ===== */
:root {
  --bg-primary: #0f1923;
  --bg-secondary: #1a2632;
  --bg-tertiary: #243342;
  --bg-card: #1e2d3d;
  --text-primary: #e8edf2;
  --text-secondary: #a3b5c4;
  --text-muted: #8ba0b2;
  --accent: #00c853;
  --accent-hover: #00e676;
  --accent-dim: rgba(0, 200, 83, 0.15);
  --danger: #ff5252;
  --warning: #ffab40;
  --info: #40c4ff;
  /* Wild-camping legality — must match Region.WILD_STATUS[*].colour in
     js/region-config.js. Used by the country choropleth, the sidebar legend
     and the "can I sleep here?" line in campsite popups. */
  --wild-allowed: #00c853;
  --wild-tolerated: #9ccc65;
  --wild-restricted: #ffab40;
  --wild-prohibited: #ff5252;
  --sidebar-width: 380px;
  --mobile-header-height: calc(56px + env(safe-area-inset-top, 0px)); /* fullscreen display: bar background reaches the true screen top; content pads below the camera cutout */
  --radius: 8px;
  --radius-sm: 4px;
  --shadow: 0 4px 24px rgba(0,0,0,0.4);
  --transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

/* ===== Reset ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--bg-primary);
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
}

/* ===== Login Screen ===== */
#login-screen {
  position: fixed; inset: 0;
  background: var(--bg-primary);
  display: flex; flex-direction: column;
  align-items: center; justify-content: flex-start;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 20px 12px;
  z-index: 10001;
  transition: opacity 0.4s ease;
}
#login-screen.fade-out { opacity: 0; pointer-events: none; }
/* Radio off with a session on the device: no login card, from the first paint
   (the head script in index.html sets the class; Auth._admitOffline opens the app). */
html.offline-session #login-screen { display: none; }
/* Stored session: the card stays hidden while it is checked (see index.html). */
html.stored-session #login-screen { visibility: hidden; }
/* Store app, before Auth has run: the static sign-up words name the website's card trial. */
html.store-app-loading #login-request-toggle { visibility: hidden; }
/* The credential fields sit in a real <form> so password managers will save
   and fill them. .login-card is a centred flex column, so without this the
   form would shrink to its content and the width:100% inputs with it. */
#login-form { width: 100%; display: flex; flex-direction: column; align-items: center; }
.login-card {
  display: flex; flex-direction: column; align-items: center;
  padding: 32px 28px;
  background: var(--bg-secondary);
  border-radius: 16px;
  border: 1px solid var(--border, rgba(255,255,255,0.06));
  width: min(360px, 90vw);
  margin-block: auto;
  flex-shrink: 0;
}
.login-input {
  width: 100%; padding: 10px 12px;
  border-radius: var(--radius-sm, 6px);
  border: 1px solid var(--border, rgba(255,255,255,0.1));
  background: var(--bg-tertiary); color: var(--text-primary);
  font-size: 0.88rem; font-family: inherit;
  margin-bottom: 8px; outline: none;
}
.login-input:focus { border-color: var(--accent); }

.password-wrap { position: relative; width: 100%; }
.password-wrap .login-input { padding-right: 52px; }
.password-toggle {
  position: absolute; right: 10px; top: 10px;
  background: none; border: 0; color: var(--text-muted, #999);
  font: inherit; font-size: max(0.72rem, var(--text-floor, 0px)); font-weight: 600;
  cursor: pointer; padding: 0;
}

.login-error {
  font-size: max(0.75rem, var(--text-floor, 0px)); color: #ff5252;
  margin-bottom: 8px; display: none;
  text-align: center;
}
.login-btn {
  width: 100%; padding: 10px; font-size: 0.88rem;
  border-radius: var(--radius-sm, 6px);
  cursor: pointer; margin-bottom: 6px;
}
/* "Message us if you can't sign in" (js/signin-help.js): an outlined button,
   not grey text, so a person stuck on the card sees it. While its form is
   open every other part of the card is hidden; Back shows them again. */
#signin-help-btn {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin: 14px 0 0; text-decoration: none; font-size: 0.8rem;
  text-align: center; padding: 10px 8px;
  background: none; color: var(--text-primary, #fff);
  border: 1px solid var(--accent, #00c853);
}
#signin-help-btn i { color: var(--accent, #00c853); }
#signin-help-panel { display: none; width: 100%; text-align: center; }
#signin-help-back { justify-content: center; }
.login-card.signin-help-open > :not(#signin-help-panel) { display: none !important; }
.login-card.signin-help-open > #signin-help-panel { display: block; }
.login-google-btn {
  background: var(--bg-tertiary); color: var(--text-secondary);
  border: 1px solid var(--border, rgba(255,255,255,0.1));
}
.login-google-btn:hover { background: var(--bg-card); }

/* ===== Loading Screen ===== */
#loading-screen {
  position: fixed; inset: 0;
  background: var(--bg-primary);
  display: flex; align-items: center; justify-content: center;
  z-index: 10000;
  transition: opacity 0.5s ease;
}
#loading-screen.fade-out { opacity: 0; pointer-events: none; }
.loader-content { text-align: center; }
.loader-icon { font-size: 3rem; color: var(--accent); margin-bottom: 1rem; animation: pulse 1.5s infinite; }
.loader-content h2 { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; }
.loader-content p { color: var(--text-secondary); margin-bottom: 1.5rem; }
.loader-bar { width: 200px; height: 3px; background: var(--bg-tertiary); border-radius: 3px; overflow: hidden; margin: 0 auto; }
.loader-progress { height: 100%; width: 30%; background: var(--accent); border-radius: 3px; animation: loading 1.5s ease-in-out infinite; }
@keyframes loading { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }

/* ===== Mobile Header ===== */
#mobile-header {
  display: none;
  position: fixed; top: 0; left: 0; right: 0;
  height: var(--mobile-header-height);
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--bg-tertiary);
  align-items: center;
  justify-content: space-between;
  padding: env(safe-area-inset-top, 0px) 12px 0;
  z-index: 1001;
}
#mobile-header h1 { font-size: 1.1rem; font-weight: 700; }
#mobile-header button {
  background: none; border: none; color: var(--text-primary);
  font-size: 1.2rem; padding: 8px; cursor: pointer;
}

/* ===== Sidebar ===== */
.sidebar {
  position: fixed; top: 0; left: 0; bottom: 0;
  width: var(--sidebar-width);
  background: var(--bg-secondary);
  border-right: 1px solid var(--bg-tertiary);
  display: flex; flex-direction: column;
  z-index: 1000;
  transition: transform var(--transition);
  overflow: hidden;
}
.sidebar.collapsed { transform: translateX(-100%); }

/* Desktop sidebar collapse (onX-style). One body-level var override moves every
   --sidebar-width dependent (#map, #info-panel, #weather-timeline, 3D/basemap
   switches) at once. Desktop-only: mobile keeps the .open overlay pattern. */
@media (min-width: 769px) {
  body.sidebar-collapsed { --sidebar-width: 0px; }
  body.sidebar-collapsed .sidebar { transform: translateX(-100%); }
  /* Collapsed sidebar puts #info-panel at left:12px — the same corner the
     fixed #sidebar-expand (») button occupies — covering the site name.
     Shift the pane right to clear the 38px button + gaps. */
  body.sidebar-collapsed #info-panel { left: 62px; }
}
#sidebar-collapse {
  background: none; border: none; color: var(--text-secondary);
  font-size: 1rem; cursor: pointer; padding: 4px 8px;
}
#sidebar-collapse:hover { color: var(--text-primary); }
#sidebar-expand {
  position: fixed; top: 12px; left: 12px; z-index: 1001;
  width: 38px; height: 38px; border-radius: var(--radius-sm);
  background: var(--bg-secondary); border: 1px solid var(--bg-tertiary);
  color: var(--text-primary); font-size: 1rem; cursor: pointer;
  display: none; align-items: center; justify-content: center;
  box-shadow: 0 2px 10px rgba(0,0,0,0.4);
}
body.sidebar-collapsed #sidebar-expand { display: flex; }

.sidebar-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--bg-tertiary);
  flex-shrink: 0;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand i { font-size: 1.5rem; color: var(--accent); }
.brand h1 { font-size: 1.25rem; font-weight: 700; }
#sidebar-close {
  background: none; border: none; color: var(--text-secondary);
  font-size: 1.2rem; cursor: pointer; padding: 4px;
  display: none;
}

.sidebar-body {
  flex: 1; overflow-y: auto; overflow-x: hidden;
  padding: 0;
  scrollbar-width: thin;
  scrollbar-color: var(--bg-tertiary) transparent;
}
.sidebar-body::-webkit-scrollbar { width: 6px; }
.sidebar-body::-webkit-scrollbar-track { background: transparent; }
.sidebar-body::-webkit-scrollbar-thumb { background: var(--bg-tertiary); border-radius: 3px; }

.sidebar-footer {
  padding: 12px 20px;
  border-top: 1px solid var(--bg-tertiary);
  flex-shrink: 0;
}
.sidebar-footer p { font-size: max(0.7rem, var(--text-floor, 0px)); color: var(--text-muted); line-height: 1.5; }

/* ===== Tab Navigation ===== */
.tab-nav {
  display: flex;
  border-bottom: 1px solid var(--bg-tertiary);
  flex-shrink: 0;
}
.tab-btn {
  flex: 1;
  padding: 12px 8px;
  background: none; border: none;
  color: var(--text-secondary);
  font-size: 0.8rem; font-weight: 500;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: all var(--transition);
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.tab-btn:hover { color: var(--text-primary); background: var(--bg-tertiary); }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab-btn i { font-size: 0.9rem; }

.tab-content { display: none; }
.tab-content.active { display: block; }

/* ===== Sidebar Sections ===== */
.sidebar-section {
  padding: 16px 20px;
  border-bottom: 1px solid rgba(255,255,255,0.04);
}
.sidebar-section h3 {
  font-size: 0.8rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-secondary);
  margin-bottom: 12px;
  display: flex; align-items: center; gap: 8px;
}
.sidebar-section h3 i { color: var(--accent); font-size: 0.85rem; }
.sidebar-section h4 {
  font-size: 0.78rem; font-weight: 600;
  color: var(--text-secondary);
  margin: 12px 0 8px;
}
.hint { font-size: 0.78rem; color: var(--text-muted); margin-bottom: 12px; line-height: 1.4; }

/* ===== Search ===== */
.search-box {
  display: flex; align-items: center;
  background: var(--bg-tertiary);
  border-radius: var(--radius);
  padding: 0 12px;
  border: 1px solid transparent;
  transition: border-color var(--transition);
}
.search-box:focus-within { border-color: var(--accent); }
.search-box i { color: var(--text-muted); font-size: 0.85rem; }
.search-box input {
  flex: 1; background: none; border: none; outline: none;
  color: var(--text-primary); padding: 10px 10px;
  font-size: 0.85rem; font-family: inherit;
}
.search-box input::placeholder { color: var(--text-muted); }
.search-box button { background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 4px; }
.search-results {
  margin-top: 8px;
  background: var(--bg-tertiary);
  border-radius: var(--radius);
  max-height: 240px;
  overflow-y: auto;
}
.search-results.hidden { display: none; }
.search-result-item {
  padding: 10px 14px;
  cursor: pointer;
  font-size: 0.82rem;
  border-bottom: 1px solid rgba(255,255,255,0.04);
  transition: background var(--transition);
  display: flex; align-items: center; gap: 8px;
}
.search-result-item:hover { background: rgba(255,255,255,0.05); }
.search-result-item i { color: var(--accent); width: 16px; text-align: center; }
.search-result-item:last-child { border-bottom: none; }
/* Recent searches, listed when the empty box is focused (js/app.js showRecent). */
.search-recent-head { padding: 8px 14px 4px; font-size: max(0.7rem, var(--text-floor, 0px)); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
.search-recent-clear { display: block; width: 100%; padding: 10px 14px; background: none; border: none; text-align: left; color: var(--text-muted); font: inherit; font-size: max(0.76rem, var(--text-floor, 0px)); cursor: pointer; }
.search-recent-clear:hover { color: var(--text-primary); }
.search-results:focus { outline: none; }
/* How far a near-me row is (RoutePlanner._distAttr): after the name, so the
   row's text, which the pick handlers read, stays the name. */
.search-result-item[data-dist]::after, .nav-dest-item[data-dist]::after {
  content: attr(data-dist); margin-left: auto; padding-left: 8px;
  color: var(--text-muted, #9aa4b2); font-size: max(0.85em, var(--text-floor, 0px)); white-space: nowrap; flex: 0 0 auto;
}

/* ===== Layer Toggles ===== */
.layer-toggle {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 0;
  cursor: pointer;
  font-size: 0.85rem;
}
.layer-toggle input { display: none; }
.toggle-slider {
  width: 36px; height: 20px;
  background: var(--bg-tertiary);
  border-radius: 10px;
  position: relative;
  transition: background var(--transition);
  flex-shrink: 0;
}
.toggle-slider::after {
  content: '';
  position: absolute;
  width: 16px; height: 16px;
  background: var(--text-secondary);
  border-radius: 50%;
  top: 2px; left: 2px;
  transition: all var(--transition);
}
.layer-toggle input:checked + .toggle-slider { background: var(--accent-dim); }
.layer-toggle input:checked + .toggle-slider::after { transform: translateX(16px); background: var(--accent); }
.toggle-label { flex: 1; }

/* ===== Route vehicle picker ===== */
/* auto-fit rather than a hardcoded column count: EU ships four classes where the
   US build ships six (see js/vehicle.js), and the labels are long enough
   ("Campervan", "Motorhome") that a fixed 4-across is cramped on a phone. This
   lays out 4 across on a wide sidebar and 2x2 on a narrow one, and survives the
   class list changing. */
.route-vehicle {
  padding: 4px 0 10px;
  border-bottom: 1px solid var(--bg-tertiary);
  margin-bottom: 4px;
}
.route-vehicle-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin-bottom: 7px;
}
.route-vehicle-title {
  font-size: max(0.68rem, var(--text-floor, 0px)); font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-muted, #888);
}
.route-vehicle-reset {
  background: none; border: none; padding: 0; cursor: pointer;
  font-size: max(0.66rem, var(--text-floor, 0px)); color: var(--accent);
  text-decoration: underline; text-underline-offset: 2px;
}
.route-vehicle-reset[hidden] { display: none; }
.route-vehicle-picker {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); gap: 5px;
}
.route-vehicle-btn {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 7px 3px;
  border: 1px solid var(--bg-tertiary);
  border-radius: 6px;
  background: transparent;
  color: var(--text-secondary);
  font-size: max(0.62rem, var(--text-floor, 0px));
  cursor: pointer;
  transition: all var(--transition);
  min-width: 0;
}
.route-vehicle-btn i { font-size: 0.92rem; }
.route-vehicle-btn span { overflow-wrap: anywhere; text-align: center; line-height: 1.2; }
.route-vehicle-btn:hover { border-color: var(--text-muted, #888); color: var(--text-primary); }
.route-vehicle-btn[aria-pressed="true"] {
  border-color: var(--accent);
  background: rgba(0, 200, 83, 0.12);
  color: var(--text-primary);
}
.route-vehicle-btn[aria-pressed="true"] i { color: var(--accent); }
/* The tow tick is a TOGGLE, not another class, so it gets its own full-width
   row at the end of the grid, laid out horizontally to read as a switch rather
   than as another rig to choose between. Same rule as NZ, AUS and USA. */
.route-vehicle-tow {
  grid-column: 1 / -1;
  flex-direction: row;
  justify-content: center;
  gap: 6px;
  padding: 6px 8px;
}
.route-vehicle-note {
  font-size: max(0.66rem, var(--text-floor, 0px));
  color: var(--text-muted, #888);
  margin-top: 7px;
  line-height: 1.45;
}
.route-vehicle-note:empty { display: none; }
.route-vehicle-note .warn { color: #ffab40; }

.badge {
  font-size: max(0.65rem, var(--text-floor, 0px)); font-weight: 600;
  padding: 2px 6px; border-radius: 3px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.badge-green { background: rgba(0,200,83,0.15); color: #00c853; }
.badge-blue { background: rgba(64,196,255,0.15); color: #40c4ff; }
.badge-orange { background: rgba(255,171,64,0.15); color: #ffab40; }
.badge-purple { background: rgba(176,128,255,0.15); color: #b080ff; }
.badge-teal { background: rgba(38,198,178,0.15); color: #26c6b2; }
.badge-yellow { background: rgba(255,230,0,0.15); color: #ffe600; }
.badge-red { background: rgba(230,0,0,0.15); color: #ff5252; }

/* ===== Wild-Camping Legend ===== */
/* Sits under the wild-camping toggle. The four swatches are the same four
   colours the country choropleth paints with, so the map reads without a key. */
.wild-legend { list-style: none; margin: 8px 0 10px; }
.wild-legend li {
  display: flex; align-items: center; gap: 8px;
  font-size: max(0.74rem, var(--text-floor, 0px)); line-height: 1.5;
  color: var(--text-secondary);
  padding: 2px 0;
}
.wild-swatch {
  flex: 0 0 auto;
  width: 12px; height: 12px;
  border-radius: 3px;
  border: 1px solid rgba(255,255,255,0.12);
}
.wild-swatch.wild-allowed { background: var(--wild-allowed); }
.wild-swatch.wild-tolerated { background: var(--wild-tolerated); }
.wild-swatch.wild-restricted { background: var(--wild-restricted); }
.wild-swatch.wild-prohibited { background: var(--wild-prohibited); }

/* ===== Units Selector ===== */
.units-select {
  width: 100%;
  padding: 8px 10px;
  background: var(--bg-tertiary);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-family: inherit;
  font-size: 0.8rem;
  cursor: pointer;
}
.units-select:focus { outline: none; border-color: var(--accent); }

/* ===== Base Map Selector ===== */
/* Warm up CARTO Voyager's pale palette so the base map reads richer, not washed out */
.basemap-selector { display: flex; gap: 6px; }
.basemap-btn {
  flex: 1; padding: 8px;
  background: var(--bg-tertiary);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-size: max(0.75rem, var(--text-floor, 0px)); font-weight: 500;
  cursor: pointer;
  transition: all var(--transition);
  display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.basemap-btn i { font-size: 1rem; }
.basemap-btn:hover { color: var(--text-primary); background: rgba(255,255,255,0.08); }
.basemap-btn.active { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }

/* ===== Filter Chips ===== */
.filter-group { display: flex; flex-wrap: wrap; gap: 6px; }
.filter-chip {
  display: flex; align-items: center; gap: 4px;
  padding: 6px 10px;
  background: var(--bg-tertiary);
  border-radius: 20px;
  font-size: max(0.75rem, var(--text-floor, 0px));
  cursor: pointer;
  transition: all var(--transition);
  border: 1px solid transparent;
  user-select: none;
}
.filter-chip:hover { background: rgba(255,255,255,0.08); }
.filter-chip input { display: none; }
.filter-chip:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); color: var(--accent); }
.filter-chip i { font-size: max(0.7rem, var(--text-floor, 0px)); }

/* ===== Route Planning ===== */
.route-inputs { display: flex; flex-direction: column; gap: 4px; }
.route-point {
  display: flex; align-items: center; gap: 10px;
  background: var(--bg-tertiary);
  border-radius: var(--radius-sm);
  padding: 0 12px;
}
/* The box gives way, the buttons never: with the moon added, a stop row on a
   384 px phone pushed its remove button off the screen (S23, 2026-10-08). An
   input will not shrink below its natural width without min-width: 0. */
.route-point > button { flex: none; }
.route-point input {
  flex: 1; min-width: 0; background: none; border: none; outline: none;
  color: var(--text-primary); padding: 10px 0;
  font-size: 0.85rem; font-family: inherit;
}
.route-point input::placeholder { color: var(--text-muted); }
/* Route search results dropdown */
.route-search-results {
  position: absolute;
  left: 0; right: 0;
  z-index: 9999;
  margin-top: 2px;
  max-height: 200px;
  overflow-y: auto;
  box-shadow: 0 6px 20px rgba(0,0,0,0.4);
  border: 1px solid rgba(255,255,255,0.08);
}
/* Suggestions are positioned in JS (see _positionResults in js/route.js):
   fixed to the viewport but anchored to the input's real position, flipping
   above it when the phone keyboard leaves no room below. The old mobile rule
   pinned them to top:52px of the SCREEN — detached from the box being typed
   in, floating over the header ("the destination box broke out"). */
.route-search-results {
  position: fixed;
  z-index: 10000;
}
.route-start-icon { color: var(--accent); font-size: max(0.6rem, var(--text-floor, 0px)); }
.route-locate-btn {
  flex-shrink: 0;
  padding: 6px 8px;
  border: none;
  color: var(--accent);
  background: none;
  font-size: 1rem;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 0.2s;
}
.route-locate-btn:hover { opacity: 1; }
.route-locate-btn.locating { animation: pulse-locate 1s infinite; }
@keyframes pulse-locate { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
.route-end-icon { color: var(--danger); font-size: 0.85rem; }
.route-waypoint-icon { color: var(--info); font-size: max(0.6rem, var(--text-floor, 0px)); }
/* "Stay here overnight" on a stop's row: dim until set. The input comes
   first in the row, so the mark on it lights its own moon. */
.route-night-btn { padding: 4px; border: none; background: none; color: var(--text-muted); opacity: 0.55; }
.route-point input[data-night] ~ .route-night-btn { color: #ffb300; opacity: 1; }
/* The same amber as the bed the stop becomes on the map (route.js WP_STYLE.night). */
.route-point:has(input[data-night]) .route-waypoint-icon { color: #ffb300; }

.route-actions { display: flex; gap: 8px; margin-top: 8px; }
.route-dates { margin-top: 4px; }
.date-inputs { display: flex; gap: 8px; }
.date-field { flex: 1; }
.date-field label { display: block; font-size: max(0.72rem, var(--text-floor, 0px)); color: var(--text-muted); margin-bottom: 4px; }
.date-field input {
  width: 100%; padding: 8px 10px;
  background: var(--bg-tertiary);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: 0.82rem; font-family: inherit;
  outline: none;
}
.date-field input:focus { border-color: var(--accent); }

/* ===== Buttons ===== */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px;
  background: var(--bg-tertiary);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-size: 0.8rem; font-weight: 500;
  cursor: pointer;
  transition: all var(--transition);
  font-family: inherit;
}
.btn:hover { background: rgba(255,255,255,0.08); color: var(--text-primary); }
.btn-sm { padding: 6px 10px; font-size: max(0.75rem, var(--text-floor, 0px)); }
.btn-primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 600; }
.btn-primary:hover { background: var(--accent-hover); color: #000; }
.btn-danger { border-color: rgba(255,82,82,0.3); color: var(--danger); }
.btn-danger:hover { background: rgba(255,82,82,0.1); }
.btn-block { width: 100%; justify-content: center; margin-top: 12px; }

/* ===== Weather Controls ===== */
.time-slider-container { padding: 4px 0; }
.time-slider-container input[type="range"] {
  width: 100%; -webkit-appearance: none; appearance: none;
  height: 4px; background: var(--bg-tertiary);
  border-radius: 2px; outline: none;
}
.time-slider-container input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px;
  background: var(--accent); border-radius: 50%;
  cursor: pointer;
}
.time-labels { display: flex; justify-content: space-between; margin-top: 4px; font-size: max(0.7rem, var(--text-floor, 0px)); color: var(--text-muted); }

/* ===== Map ===== */
#map {
  position: fixed;
  top: 0; bottom: 0;
  left: var(--sidebar-width);
  right: 0;
  z-index: 1;
}

/* ===== Map Controls ===== */
#map-controls {
  position: fixed;
  top: 16px; right: 16px;
  display: flex; flex-direction: column; gap: 8px;
  z-index: 500;
}
.map-btn {
  width: auto; height: auto;
  padding: 8px 10px;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: var(--radius);
  color: var(--text-primary);
  font-size: 1rem;
  cursor: pointer;
  display: flex; align-items: center; gap: 6px;
  transition: all var(--transition);
  box-shadow: var(--shadow);
}
.map-btn-label {
  font-size: max(0.65rem, var(--text-floor, 0px));
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-secondary);
}
.map-btn:hover { background: var(--bg-tertiary); color: var(--accent); }
.map-btn:hover .map-btn-label { color: var(--accent); }
.map-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.map-btn.active .map-btn-label { color: #fff; }

/* ===== Weather Tile Overlays ===== */
.weather-tile-temperature { filter: saturate(2.5) contrast(1.8) brightness(1.1); }
.weather-tile-precipitation { filter: saturate(2.0) contrast(1.5); }
.weather-tile-clouds { filter: contrast(1.4) brightness(0.95); }
.weather-tile-wind { filter: saturate(1.8) contrast(1.4); }

/* ===== Weather Timeline ===== */
/* ===== Info Panel =====
   Vertical side-loading pane (onX-style), 2026-07-27. Was a bottom-anchored
   horizontal strip eating 50vh; now a left card over the map. Mobile keeps the
   bottom-sheet geometry (overridden in the 768px block below). */
#info-panel {
  position: fixed;
  /* Height follows the content, capped at the viewport — NOT top+bottom, which
     stretched a one-line POI (a toilet) into a full-height empty column
     (Josh, 2026-08-10). Long cards still scroll inside the cap. */
  top: 12px;
  max-height: calc(100vh - 24px);
  left: calc(var(--sidebar-width) + 12px);
  width: 380px;
  max-width: calc(100vw - var(--sidebar-width) - 24px);
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: var(--radius);
  z-index: 600;
  overflow-y: auto;
  box-shadow: 0 8px 30px rgba(0,0,0,0.45);
  transition: transform var(--transition), opacity var(--transition);
}
#info-panel.hidden {
  transform: translateX(calc(-100% - 420px));
  opacity: 0;
  pointer-events: none;
}
#info-close {
  position: sticky; top: 0; right: 0;
  float: right; margin: 12px 12px 0 0;
  background: var(--bg-tertiary); border: none;
  z-index: 10;
  color: var(--text-secondary); width: 32px; height: 32px;
  border-radius: 50%; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.9rem;
}
#info-content { padding: 20px 24px 24px; }

/* Info Panel Content */
.info-header { margin-bottom: 16px; }
.info-header h2 { font-size: 1.15rem; font-weight: 700; margin-bottom: 4px; }
.info-header .info-type { font-size: 0.78rem; color: var(--accent); font-weight: 500; text-transform: uppercase; letter-spacing: 0.03em; }
.info-header .info-status { font-size: max(0.75rem, var(--text-floor, 0px)); padding: 2px 8px; border-radius: 3px; margin-left: 8px; }
.info-header .info-status.open { background: rgba(0,200,83,0.15); color: #00c853; }
.info-header .info-status.closed { background: rgba(255,82,82,0.15); color: #ff5252; }

.info-facilities {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-bottom: 16px;
}
/* The dog rule, its own row above the facility badges (it was the first item
   of their strip, which on a phone scrolls sideways and cut it). */
.info-dogs { margin-bottom: 8px; }
/* The phone's popup card and the cards' drag handle (js/sheet.js) exist only
   in the phone layout; a desktop keeps MapLibre popups. */
#map-sheet, #route-sheet, .sheet-handle { display: none; }
/* The route card's Save / Share / Clear row (js/route-start.js) shows only in
   the route card on a phone; the menu has its own Save and Clear. */
#route-card-actions { display: none; }
/* The route card's red hazard line (js/sheet.js _buildHazardLine): only in the card on a phone. */
#route-hazard-line { display: none; }
/* The route card's mini bar (js/sheet.js _buildMini): only in its mini state, on a phone. */
#route-mini { display: none; }
.facility-badge {
  display: flex; align-items: center; gap: 5px;
  padding: 6px 10px;
  background: var(--bg-tertiary);
  border-radius: var(--radius-sm);
  font-size: max(0.75rem, var(--text-floor, 0px));
  color: var(--text-secondary);
}
.facility-badge.available { color: var(--accent); background: var(--accent-dim); }
.facility-badge i { font-size: 0.8rem; }

.info-details { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
.info-detail { }
.info-detail-label { font-size: max(0.7rem, var(--text-floor, 0px)); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 2px; }
.info-detail-value { font-size: 0.85rem; color: var(--text-primary); }

.info-weather {
  background: var(--bg-tertiary);
  border-radius: var(--radius);
  padding: 14px;
  margin-bottom: 16px;
}
.info-weather h4 { font-size: 0.78rem; color: var(--text-secondary); margin-bottom: 8px; }
.weather-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px;
}
.weather-item { text-align: center; }
.weather-item .wx-icon { font-size: 1.3rem; margin-bottom: 2px; }
.weather-item .wx-temp { font-size: 1rem; font-weight: 600; }
.weather-item .wx-desc { font-size: max(0.68rem, var(--text-floor, 0px)); color: var(--text-muted); }
.weather-item .wx-day { font-size: max(0.68rem, var(--text-floor, 0px)); color: var(--text-secondary); font-weight: 500; }

/* Wild-camping status line in a place popup — coloured left rule + the
   one-sentence summary from Region.WILD_STATUS. */
.info-wild {
  border-left: 3px solid var(--text-muted);
  background: var(--bg-tertiary);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-bottom: 16px;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--text-secondary);
}
.info-wild h4 { font-size: 0.78rem; margin-bottom: 4px; color: var(--text-primary); }
.info-wild.wild-allowed { border-left-color: var(--wild-allowed); }
.info-wild.wild-tolerated { border-left-color: var(--wild-tolerated); }
.info-wild.wild-restricted { border-left-color: var(--wild-restricted); }
.info-wild.wild-prohibited { border-left-color: var(--wild-prohibited); }

.info-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.info-actions .btn i { font-size: 0.8rem; }
/* A card action's phone label (RoutePlanner.routeActionsHTML): the desktop shows the full words. */
.act-short { display: none; }

/* ===== Pane card components (onX card anatomy, shared by trail/land/hazard/
   waypoint cards and reusable by any info-panel content) ===== */
.pane-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 14px; }
.pane-tile { background: var(--bg-tertiary); border-radius: var(--radius-sm); padding: 8px 10px; }
.pane-tile.wide { grid-column: 1 / -1; }
.pane-tile .pt-label { font-size: max(0.68rem, var(--text-floor, 0px)); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; }
.pane-tile .pt-value { font-size: 0.95rem; font-weight: 700; margin-top: 2px; }
.pane-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.pane-chip { border: 1px solid var(--bg-tertiary); background: var(--bg-tertiary); border-radius: 999px; padding: 3px 10px; font-size: max(0.72rem, var(--text-floor, 0px)); color: var(--text-secondary); }
.pane-chip.ok { border-color: var(--accent); color: var(--accent); background: none; }
.pane-chip.warn { border-color: #f59e0b; color: #f59e0b; background: none; }
.pane-chip.bad { border-color: #ff5252; color: #ff5252; background: none; }
.pane-kv { border-top: 1px solid var(--bg-tertiary); padding: 8px 2px; display: flex; gap: 8px; font-size: 0.82rem; }
.pane-kv .k { color: var(--text-muted); min-width: 110px; flex-shrink: 0; }
.pane-kv .v { font-weight: 600; }
.pane-actions { display: flex; gap: 12px; justify-content: space-around; border-top: 1px solid var(--bg-tertiary); padding: 12px 4px 2px; margin-top: 12px; }
.pane-action { background: none; border: none; color: var(--text-secondary); cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 5px; font-size: max(0.7rem, var(--text-floor, 0px)); }
.pane-action .pa-ico { width: 42px; height: 42px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--bg-tertiary); border: 1px solid var(--bg-tertiary); font-size: 1rem; color: var(--text-primary); }
.pane-action.primary .pa-ico { background: var(--accent); color: #00120a; }
.pane-action:hover .pa-ico { border-color: var(--accent); }
.diff-pill { display: inline-block; border-radius: 5px; padding: 2px 10px; font-weight: 700; font-size: 0.78rem; color: #08131c; }
.pane-note { color: var(--text-muted); font-size: max(0.72rem, var(--text-floor, 0px)); margin-top: 10px; line-height: 1.45; }

/* Waypoints */
.wp-icon-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin: 8px 0; }
.wp-icon-grid button { font-size: 1.05rem; padding: 8px 0; border-radius: var(--radius-sm); border: 2px solid var(--bg-tertiary); background: var(--bg-tertiary); cursor: pointer; }
.wp-icon-grid button.sel { border-color: var(--accent); }
.wp-color-row { display: flex; gap: 8px; margin: 8px 0; }
.wp-color-row button { width: 26px; height: 26px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
.wp-color-row button.sel { border-color: #fff; }
.wp-marker { width: 30px; height: 30px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); display: flex; align-items: center; justify-content: center; border: 2px solid #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.5); cursor: pointer; }
.wp-marker span { transform: rotate(45deg); font-size: max(13px, var(--text-floor, 0px)); }
.wp-list-item { display: flex; align-items: center; gap: 10px; padding: 8px 4px; border-bottom: 1px solid var(--bg-tertiary); cursor: pointer; border-radius: var(--radius-sm); }
.wp-list-item:hover { background: var(--bg-tertiary); }
.wp-list-dot { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.85rem; flex-shrink: 0; }

/* Trail overlay legend lines (rendered inside the sidebar section) */
.trail-legend-line { display: flex; align-items: center; gap: 10px; padding: 3px 0; font-size: max(0.75rem, var(--text-floor, 0px)); color: var(--text-secondary); }
.trail-legend-swatch { width: 30px; height: 5px; border-radius: 3px; flex-shrink: 0; }

/* ===== Route Summary Styles ===== */
.route-overview {
  display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;
  margin-bottom: 16px;
}
.route-stat {
  text-align: center;
  padding: 10px;
  background: var(--bg-tertiary);
  border-radius: var(--radius-sm);
}
.route-stat-value { font-size: 1.2rem; font-weight: 700; color: var(--accent); }
.route-stat-label { font-size: max(0.68rem, var(--text-floor, 0px)); color: var(--text-muted); margin-top: 2px; }

.route-segment {
  padding: 12px 0;
  border-bottom: 1px solid rgba(255,255,255,0.04);
}
.route-segment:last-child { border-bottom: none; }
.route-segment-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 6px;
}
.route-segment-header h4 { font-size: 0.85rem; font-weight: 600; }
.route-segment-header span { font-size: max(0.75rem, var(--text-floor, 0px)); color: var(--text-muted); }
.route-segment-weather {
  display: flex; gap: 12px;
  margin-bottom: 6px;
}
.route-segment-weather .wx-mini {
  display: flex; align-items: center; gap: 4px;
  font-size: 0.78rem; color: var(--text-secondary);
}
.route-segment-services { display: flex; flex-wrap: wrap; gap: 4px; }
.route-segment-services .service-tag {
  font-size: max(0.68rem, var(--text-floor, 0px));
  padding: 2px 6px;
  background: var(--bg-tertiary);
  border-radius: 3px;
  color: var(--text-muted);
}
.route-segment-services .service-tag.available { color: var(--accent); background: var(--accent-dim); }

/* ===== Elevation Profile ===== */
.elevation-profile {
  margin-bottom: 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid rgba(255,255,255,0.04);
}
.elevation-stats {
  display: flex; gap: 12px;
  margin-bottom: 8px;
  font-size: max(0.75rem, var(--text-floor, 0px));
  color: var(--text-secondary);
}
.elevation-stats span {
  display: flex; align-items: center; gap: 4px;
}
.elevation-chart-wrap {
  position: relative;
  background: var(--bg-tertiary);
  border-radius: var(--radius-sm);
  padding: 4px;
  cursor: crosshair;
}
.elevation-chart-wrap canvas {
  display: block;
  width: 100%;
}
.elevation-tooltip {
  position: absolute;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: var(--radius-sm);
  padding: 4px 8px;
  font-size: max(0.72rem, var(--text-floor, 0px));
  color: var(--text-primary);
  pointer-events: none;
  white-space: nowrap;
  z-index: 10;
  box-shadow: var(--shadow);
  line-height: 1.3;
}
.elevation-map-tooltip {
  background: var(--bg-secondary) !important;
  border: 1px solid var(--accent) !important;
  color: var(--text-primary) !important;
  font-size: max(0.75rem, var(--text-floor, 0px)) !important;
  font-weight: 600 !important;
  padding: 2px 6px !important;
  border-radius: 4px !important;
  box-shadow: var(--shadow) !important;
}
.elevation-map-tooltip::before {
  border-top-color: var(--accent) !important;
}

/* ===== Weather Overlay Legend ===== */
.weather-legend-bar {
  display: flex; height: 12px;
  border-radius: 6px; overflow: hidden;
  margin-bottom: 4px;
}
.weather-legend-labels { display: flex; justify-content: space-between; font-size: max(0.68rem, var(--text-floor, 0px)); color: var(--text-muted); }

/* ===== MapLibre GL Overrides ===== */
.maplibregl-ctrl-attrib { font-size: max(0.6rem, var(--text-floor, 0px)) !important; background: rgba(15,25,35,0.8) !important; color: var(--text-muted) !important; }
.maplibregl-ctrl-attrib a { color: var(--text-secondary) !important; }
.maplibregl-popup-content {
  background: var(--bg-secondary) !important;
  color: var(--text-primary) !important;
  border-radius: var(--radius) !important;
  box-shadow: var(--shadow) !important;
  border: 1px solid var(--bg-tertiary) !important;
  padding: 12px 14px !important;
  font-family: 'Inter', sans-serif !important;
}
/* The popup tip is a CSS border triangle — theme every anchor direction */
.maplibregl-popup-anchor-bottom .maplibregl-popup-tip { border-top-color: var(--bg-secondary) !important; }
.maplibregl-popup-anchor-top .maplibregl-popup-tip { border-bottom-color: var(--bg-secondary) !important; }
.maplibregl-popup-anchor-left .maplibregl-popup-tip { border-right-color: var(--bg-secondary) !important; }
.maplibregl-popup-anchor-right .maplibregl-popup-tip { border-left-color: var(--bg-secondary) !important; }
.maplibregl-popup-close-button { color: var(--text-secondary) !important; font-size: 20px !important; right: 4px !important; top: 2px !important; }
/* Cluster count styling is data-driven GL symbol paint now (Utils.clusterColors) — no DOM cluster CSS. */

/* (retired .marker-* divIcon classes removed — markers now draw on canvas via Utils.dotMarker; palette lives in Utils.markerStyles) */

/* ===== Responsive ===== */
@media (max-width: 768px) {
  /* Older riders are the bulk of the audience (AUS: 79% aged 55+) and cannot
     read the 11-13px chrome on a phone. The in-between size (Josh, 2026-11-01:
     "yeah go inbetween"): 112.5% takes the root from 16px to 18px, the body
     size presbyopes use on their own phones (Boccardo 2021/2023), and scales
     every rem size in the UI. It was 125% (20px) from 2026-08-22. Map labels
     are WebGL and stay put. Percentage, not 18px, so a user who already raised
     the browser default still gets the extra step.
     --text-floor: nothing on a phone renders under 14px. Every size below it
     is written max(size, var(--text-floor, 0px)), here and in the builders'
     inline styles; the desktop never sets the token, so its sizes do not move. */
  html { font-size: 112.5%; --text-floor: 14px; }

  /* The old title bar is gone from a phone: the floating bar (#mobile-search,
     phone redesign step 3) replaced it. Its buttons stay in the page, hidden. */
  #mobile-header { display: none; }
  #sidebar-close { display: block; }

  .sidebar {
    width: 100%;
    transform: translateX(-100%);
    top: 0;
  }
  .sidebar.open { transform: translateX(0); }
  .sidebar-header { padding-top: calc(var(--mobile-header-height) + 8px); }

  /* Full bleed: the map runs under the floating top bar (phone redesign step
     3). Whatever frames the map measures the bar instead (RoutePlanner
     FRAME_FURNITURE, Sheet._box, Onboard.MAP_TOP). */
  #map {
    left: 0;
    top: 0;
  }

  /* Under the bar, with the bar's own 10 px of air (the token holds it). */
  #map-controls {
    top: var(--mobile-header-height);
    right: 12px;
  }

  /* ===== One card at the bottom: #info-panel and #map-sheet (js/sheet.js) =====
     Phone redesign step 1 (tasks/phone-ux.md in NZ). The card opened at up to
     60vh and covered what it described; it is now a sheet: a peek that ends
     under the actions (and under every warning, which is never cut), a pull up
     to 88% for the rest. Its height is --sheet-h, set by Sheet; nothing here
     sizes it. top/right/width are RESET explicitly: the desktop rule sets them. */
  #info-panel, #map-sheet, #route-sheet {
    position: fixed;
    top: auto; bottom: 0; left: 0; right: 0;
    width: 100%; max-width: 100%;
    height: var(--sheet-h, 0px);
    /* Never under the notch, whatever --sheet-h says (Sheet sizes it from
       innerHeight and the shown bar; this binds only if that read fails). */
    max-height: calc(100% - env(safe-area-inset-top, 0px) - 8px);
    background: var(--bg-secondary); color: var(--text-primary);
    border: none; border-top: 1px solid var(--bg-tertiary);
    border-radius: 16px 16px 0 0;
    box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.42);
    z-index: 600;
    /* The last row would sit under the system bar: padding, not an offset. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    /* In the peek nothing scrolls: a vertical drag moves the card (Sheet),
       a sideways one still scrolls the facilities row. */
    overflow: hidden; overscroll-behavior: contain; touch-action: pan-x;
    transition: height 0.22s ease, transform 0.22s ease;
  }
  #map-sheet, #route-sheet { display: block; }
  #info-panel.sheet-full, #map-sheet.sheet-full, #route-sheet.sheet-full { overflow-y: auto; touch-action: auto; }
  /* The peek ends under the actions; a short fade says "more below" instead
     of the next line showing cut in half. */
  #info-panel.sheet-peek, #map-sheet.sheet-peek, #route-sheet.sheet-peek {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 14px), transparent);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 14px), transparent);
  }
  #info-panel.sheet-dragging, #map-sheet.sheet-dragging, #route-sheet.sheet-dragging { transition: none; }
  #info-panel.hidden, #map-sheet.hidden, #route-sheet.hidden { transform: translateY(100%); opacity: 1; pointer-events: none; }
  body.nav-active #map-sheet, body.dashboard-active #map-sheet,
  body.nav-active #route-sheet, body.dashboard-active #route-sheet { display: none !important; }
  .sheet-handle {
    display: block; position: sticky; top: 0; z-index: 2;
    height: 22px; background: inherit; cursor: grab; touch-action: none;
  }
  /* The bar is 38 x 5; the tap target around it is 48 px tall. */
  .sheet-handle::before {
    content: ''; position: absolute; left: 50%; top: 9px; transform: translateX(-50%);
    width: 38px; height: 5px; border-radius: 3px; background: rgba(255, 255, 255, 0.28);
  }
  /* The tap area starts at the card's top edge (the panel clips anything
     above it) and reaches 48 px down over the name's top margin. */
  .sheet-handle::after { content: ''; position: absolute; left: 30%; right: 30%; top: 0; height: 48px; }
  /* The X: 48 px, under the handle, and taking no height of its own (the
     negative bottom margin), so the card's content keeps the full width;
     the name makes room for it instead. */
  #info-close, #map-sheet .sheet-close, #route-sheet .sheet-close {
    position: sticky; top: 22px; float: right; z-index: 3;
    width: 48px; height: 48px; margin: 0 8px -48px 0; border-radius: 50%;
    border: none; cursor: pointer; background: var(--bg-tertiary); color: var(--text-secondary);
  }
  #info-content { padding: 2px 14px 12px; }
  /* The cards follow the root (1rem, 18 px); density comes from fewer sizes and less padding. */
  #map-sheet .sheet-content, #route-sheet .sheet-content { padding: 2px 14px 12px; font-size: 1rem; }
  #info-content > .info-header, #info-content .pc-head, #info-content .pop-title,
  #map-sheet .pop-title, #map-sheet .popup-title { padding-right: 52px; min-height: 48px; }
  /* Desktop popups size themselves inline; in the card they take its width. */
  #info-content > .pop-card, #map-sheet .sheet-content > * { min-width: 0 !important; max-width: none !important; }

  /* ===== The map menu: a card at the bottom (js/app.js placeCtxCard) =====
     Josh, 2026-11-01, S10: "the 'head here next' popup takes almost the whole
     screen with almost no way to exit the dialog box". The place card's look:
     rounded top, the card's colour and shadow, no drag handle. 40% of the
     screen at most (--ctx-max-h), over the route card or its mini bar when the
     tap menu keeps it (--ctx-bottom); the rest scrolls under a top bar that
     stays, with its 48 px X. Rows 48 px, 18 px words. */
  #context-menu.ctx-card {
    left: 0; right: 0; top: auto; bottom: var(--ctx-bottom, 0px);
    width: auto; min-width: 0; max-width: 480px; margin: 0 auto;
    max-height: var(--ctx-max-h, 40vh);
    padding: 0 0 env(safe-area-inset-bottom, 0px);
    border: none; border-top: 1px solid var(--bg-tertiary);
    border-radius: 16px 16px 0 0;
    box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.42);
    overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain;
  }
  #context-menu.ctx-card .ctx-top {
    position: sticky; top: 0; z-index: 1;
    display: flex; align-items: center; gap: 8px;
    min-height: 56px; padding: 4px 6px 4px 18px;
    background: var(--bg-secondary); border-bottom: 1px solid var(--bg-tertiary);
  }
  #context-menu.ctx-card .ctx-head {
    flex: 1 1 auto; min-width: 0; max-width: none;
    padding: 0; margin: 0; border: none; font-size: 0.8rem;
  }
  #context-menu.ctx-card .ctx-close {
    flex: 0 0 auto; width: 48px; height: 48px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; cursor: pointer; font-size: 20px;
    background: var(--bg-tertiary); color: var(--text-primary);
  }
  #context-menu.ctx-card .ctx-item { min-height: 48px; padding: 0 18px; gap: 14px; font-size: 1rem; }
  #context-menu.ctx-card .ctx-item:active { background: rgba(255, 255, 255, 0.08); }
  #context-menu.ctx-card .ctx-item i { width: 22px; font-size: 1rem; }
  #context-menu.ctx-card .ctx-sub { font-size: 0.9rem; padding-top: 0; padding-bottom: 0; }

  /* ===== The place card's first view on a phone: Ridgeflow's =====
     Josh, 2026-11-01, on the S10: "the holiday park info is still far too
     large on screen. make it like ridgeflow". Ridgeflow's Lakes Edge card
     peeks at 238 px (36% of 668): the name on one line, the type line, ONE
     green link to the place's own site, a row of small facility chips, ONE
     row of three buttons. Ours peeked at 370 px (55%): a two-line name at the
     125% root, Save on its own row, the contact block, the dog row, the
     facilities and six buttons.
     The first view, in this order: the name; the type line with Open /
     Closed; the place's own link row (Book direct / Website / Call, one
     44 px row); the facilities (one sideways row of chips); ONE row of three
     actions: Directions (Head here next with a route up), Stay overnight and
     Save. The actions are the ones RoutePlanner.routeActionsHTML marks
     act-main, and the card's own Save (on the desktop it stays under the
     name, Josh 2026-08-22). A real warning (DOC's alerts, every .card-warn,
     "nobody has confirmed this yet", a stated dog rule) comes straight after
     that row, and the peek grows to show it whole (js/sheet.js). Then the
     card's other actions (Directions from here, a NEW route, a saved spot's
     Rename / Remove), "No dog info" (an empty state, not a warning), the
     details, provenance, reviews, nearby, coverage and weather, and last the
     links that leave the app (Search web, Google Maps, Reviews).
     A wrapping flex row, not a column: every part takes a whole line except
     the actions. An action row is display: contents, so its buttons are laid
     out here, each by its own kind: a first-view action (act-main) a third of
     the line, another button or href="#" link (in-app) half of it, a link
     with an address of its own (it leaves the app) at the foot. js/sheet.js
     _peekEnd() and leaves() read them the same way. CSS only for the order:
     the builders and the desktop card are untouched. */
  #info-content { display: flex; flex-flow: row wrap; column-gap: 8px; align-content: flex-start; }
  #info-content > * { order: 10; flex: 1 1 100%; min-width: 0; }
  #info-content > .info-header { order: 1; margin-bottom: 8px; }
  #info-content > .place-direct { order: 2; }
  #info-content > .info-facilities { order: 3; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; gap: 6px; margin-bottom: 8px; }
  #info-content > .info-facilities:empty { display: none; }
  #info-content > .info-facilities > * { flex: 0 0 auto; }
  /* A vertical drag that starts on the sideways chip row moves the card (iPhone
     WebKit run, 2026-11-01): a scroller resets touch-action, so iOS took the
     pan and sent pointercancel. pan-x keeps the sideways scroll and hands the
     vertical drag to js/sheet.js while the card is a peek. */
  #info-panel.sheet-peek #info-content > .info-facilities,
  #map-sheet.sheet-peek .pop-facilities { touch-action: pan-x; }
  /* Save joins the action row, last of the three, as Ridgeflow's does. */
  #info-content > .info-save-row { display: contents; }
  #info-content > .info-save-row > * { order: 5; }
  /* "Added by a camper, nobody has confirmed this yet" stays directly under
     the name (community-sites.js puts it after the header in the page). */
  #info-content > #community-site-block { order: 1; }
  #info-content > .doc-alerts-slot,
  #info-content > .card-warn { order: 6; }
  /* The details grid opens out so the access block, when it is restrictive
     (layers.js _accessRestrictive marks data-access-warn), joins the warnings
     in the first view (Sheet.WARN); otherwise it keeps its place among the
     details below. */
  #info-content > .info-details { display: contents; }
  #info-content > .info-details > * { order: 10; flex: 1 1 100%; min-width: 0; margin-bottom: 12px; }
  /* "Sealed access" and other good news stay with the details, below the first view. */
  #info-content > .info-details > #info-access[data-access-warn] { order: 6; margin-bottom: 8px; }
  /* The dog row: placed by its own data (Utils.dogBannerHTML marks a
     restrictive rule, official or established by riders, with data-dog-rule:
     a warning, up with the others; "Dogs allowed" sits below the first view). */
  #info-content > .info-dogs { display: contents; }
  #info-content > .info-dogs > #dog-banner { order: 8; margin-bottom: 8px; }
  #info-content > .info-dogs > #dog-banner[data-dog-rule] { order: 6; }
  /* The action rows: the campsite / hut / saved spot strip, and the map
     POI and route-audit rows. A form's or a list's own buttons (the waypoint
     editor's Save / Cancel, the list's Export) stay a row after its fields. */
  #info-content > .info-actions:not(.wp-form-actions), #info-content > .pop-actions { display: contents !important; }
  #info-content > .info-actions:not(.wp-form-actions) > *,
  #info-content > .info-actions:not(.wp-form-actions) > .card-route-actions > *,
  #info-content > .pop-actions > *, #info-content > .pop-actions > .card-route-actions > * { order: 7; }
  #info-content > .info-actions:not(.wp-form-actions) > .act-main,
  #info-content > .info-actions:not(.wp-form-actions) > .card-route-actions > .act-main,
  #info-content > .pop-actions > .act-main, #info-content > .pop-actions > .card-route-actions > .act-main { order: 4; }
  #info-content > .info-actions:not(.wp-form-actions) > a:not([href="#"]),
  #info-content > .pop-actions > a:not([href="#"]) { order: 11; }
  /* The search card (js/place-card.js): the same first view. Its Go button
     stays first among the actions; its address and hours wait below. */
  #info-content .place-card { display: flex; flex-flow: row wrap; column-gap: 8px; align-content: flex-start; }
  #info-content .place-card > * { order: 10; flex: 1 1 100%; min-width: 0; }
  #info-content .place-card > .pc-head { order: 1; }
  #info-content .place-card > .pc-contact { order: 2; border-top: none; padding-top: 0; margin-top: 0; }
  #info-content .place-card > .pc-actions { order: 4; margin: 0 0 8px; }
  #info-content .place-card > .pc-more { display: contents; }
  #info-content .place-card > .pc-more > *, #info-content .place-card > .pc-more > .card-route-actions > * { order: 7; }
  #info-content .place-card > .pc-more > .act-main,
  #info-content .place-card > .pc-more > .card-route-actions > .act-main { order: 4; }
  /* The popup-builder cards (POIs, eateries, fuel, water, pubs, webcams):
     the name with its type line, the place's own links, the badges (a
     station's fuels and EU's live prices), the first-view actions, the
     warnings, the other in-app actions, the rest; the outside links last. */
  .pop-card { display: flex; flex-flow: row wrap; column-gap: 8px; align-content: flex-start; }
  .pop-card > * { order: 10; flex: 1 1 100%; min-width: 0; }
  .pop-card > .pop-hero { order: 0; }
  .pop-card > .pop-title, .pop-card > .pop-sub { order: 1; }
  .pop-card > .place-direct { order: 2; }
  .pop-card > .pop-facilities { order: 3; }
  /* EU: a fuel station's prices from the national feed (js/fuel-prices.js)
     are why it was tapped: with its grades, before the actions. */
  .pop-card > #fuel-prices-block { order: 3; }
  .pop-card > .pop-actions { display: contents !important; }
  .pop-card > .pop-actions > *, .pop-card > .pop-actions > .card-route-actions > * { order: 7; }
  .pop-card > .pop-actions > .act-main, .pop-card > .pop-actions > .card-route-actions > .act-main { order: 4; }
  .pop-card > .card-warn { order: 6; }
  .pop-card > .pop-actions > a:not([href="#"]) { order: 11; }
  /* An in-app action: 48 px, its label wrapping inside the button rather
     than off the card; half the line, or a third in the first view's row
     (two when a card has two). 0.85rem (15.3 px), under the 18 px body, so
     Directions (or Next stop), Stay overnight and Save fit one row at 360 px. */
  #info-content > .info-actions:not(.wp-form-actions) > *,
  #info-content > .info-actions:not(.wp-form-actions) > .card-route-actions > *,
  #info-content > .pop-actions > *, #info-content > .pop-actions > .card-route-actions > *,
  .pop-card > .pop-actions > *, .pop-card > .pop-actions > .card-route-actions > *,
  #info-content .place-card > .pc-more > *, #info-content .place-card > .pc-more > .card-route-actions > *,
  #info-content > .info-save-row > * {
    flex: 1 1 calc(50% - 4px); min-width: 0; margin: 0 0 8px; padding: 2px 6px; gap: 5px;
    justify-content: center; text-align: center; font-size: 0.85rem; line-height: 1.2; white-space: normal; text-wrap: balance;
  }
  #info-content > .info-actions:not(.wp-form-actions) > .act-main,
  #info-content > .info-actions:not(.wp-form-actions) > .card-route-actions > .act-main,
  #info-content > .pop-actions > .act-main, #info-content > .pop-actions > .card-route-actions > .act-main,
  .pop-card > .pop-actions > .act-main, .pop-card > .pop-actions > .card-route-actions > .act-main,
  #info-content .place-card > .pc-more > .act-main, #info-content .place-card > .pc-more > .card-route-actions > .act-main,
  #info-content > .info-save-row > * {
    /* Each takes its own label's width, then an equal share of what is left:
       "Stay overnight" gets the room it needs and never wraps (it did in a
       third of 384 px on the S10), Directions and Save are short. A row too
       narrow for all three puts the last on the next line, unbroken. */
    flex: 1 0 auto; white-space: nowrap;
  }
  #info-content .btn i, #map-sheet .btn i, .pop-actions > a i, .pop-actions .card-route-actions > a i,
  #info-content .pc-more > a i { font-size: 0.8rem; }
  /* A popup's links carry their row's inline size (0.72em): the card's
     buttons are one size. The search card's Save (Layers._saveLink, inline
     0.72em) sits in .pc-more, not .pop-actions: it read 11.5 px beside 15 px
     Directions and Stay overnight (card checker, 2026-11-01). */
  .pop-actions > a, .pop-actions .card-route-actions > a,
  #info-content .pc-more > a { font-size: 0.85rem !important; }
  /* The phone's short label ("Stay overnight"); the full words stay in its
     title and aria-label (RoutePlanner.routeActionsHTML). */
  #info-panel .act-long, #map-sheet .act-long { display: none; }
  #info-panel .act-short, #map-sheet .act-short { display: inline; }
  /* One word wider than its third ("Übernachten") breaks inside the button,
     never past its edge. */
  #info-panel .act-short, #map-sheet .act-short, .map-tools-row button, #layers-sheet .lc-chip { overflow-wrap: anywhere; hyphens: auto; }
  /* An outside link at the foot: its own width, side by side. */
  #info-content > .info-actions:not(.wp-form-actions) > a:not([href="#"]),
  #info-content > .pop-actions > a:not([href="#"]),
  .pop-card > .pop-actions > a:not([href="#"]) { flex: 0 1 auto; }
  /* The card's buttons are its main targets: 48 px tall (.btn-sm was 30). */
  #info-content .btn, #map-sheet .btn, .pop-actions > a, .pop-actions .card-route-actions > a,
  #info-content .place-direct a, #info-content .pc-more > a {
    min-height: 48px; display: inline-flex; align-items: center; gap: 6px; box-sizing: border-box;
  }
  /* The card follows the root: an 18 px body (1rem), a name a step above it
     (1.1rem), the type line, status and chips at 0.8rem (14.4 px), never
     under the 14 px floor. Fewer sizes, closer together. */
  #info-panel, #map-sheet { font-size: 1rem; }
  #info-content > .info-header h2, #info-content .pc-name { font-size: 1.1rem; line-height: 1.25; margin-bottom: 2px; }
  #info-content > .info-header .info-type { font-size: 0.8rem; }
  #info-content > .info-header .info-status { font-size: 0.8rem; padding: 1px 6px; }
  #info-content .pc-sub { font-size: 0.8rem; }
  #info-panel .facility-badge, #map-sheet .facility-badge { font-size: 0.8rem; padding: 3px 8px; gap: 4px; }
  #info-panel .facility-badge i, #map-sheet .facility-badge i { font-size: 0.8rem; }
  /* The place's own links: ONE row, side by side; the site (the one to
     press, Josh 2026-10-02) takes the room, its address cut with an
     ellipsis; Call keeps its own width. */
  #info-panel .place-direct, #map-sheet .place-direct { flex-wrap: nowrap; margin: 0 0 8px; }
  #info-panel .place-direct a, #map-sheet .place-direct a { flex: 1 1 auto; min-width: 0; font-size: 0.9rem; padding: 0 12px; gap: 8px; }
  #info-panel .place-direct a.place-direct-call, #map-sheet .place-direct a.place-direct-call { flex: 0 1 auto; }
  #info-panel .place-direct-detail, #map-sheet .place-direct-detail { font-size: 0.8rem; }
  .pop-actions > a, .pop-actions .card-route-actions > a, #info-content .pc-more > a {
    padding: 0 12px; border: 1px solid rgba(255, 255, 255, 0.14); border-radius: var(--radius-sm);
  }
  /* A link that stands alone on its line (not a word in a sentence) is a
     tap target too: 48 px ("Report a problem with this place" was 29). */
  #info-content [data-error-report-link], #map-sheet [data-error-report-link],
  #info-content .pane-kv .v > a:only-child, #map-sheet .pane-kv .v > a:only-child,
  #info-content .card-link, #map-sheet .card-link {
    min-height: 48px; display: inline-flex; align-items: center; gap: 6px; box-sizing: border-box;
  }
  /* One surface at a time: while a card is up the pan pad, the bottom pills
     and the bottom Start / Clear step aside. Never during the tour
     (body.onboard-on): its "views" step rings the pills. */
  body.sheet-open:not(.onboard-on) #map-ptz,
  body.sheet-open:not(.onboard-on) #basemap-switch,
  body.sheet-open:not(.onboard-on) #mode-3d-switch,
  body.sheet-open:not(.onboard-on) #route-start-btn,
  body.sheet-open:not(.onboard-on) #route-clear-btn,
  body.sheet-open:not(.onboard-on) #search-go-btn { display: none !important; }
  @media (prefers-reduced-motion: reduce) {
    #info-panel, #map-sheet, #route-sheet { transition: none; }
  }

  /* ===== The route card (js/sheet.js bootRoute, phone redesign step 2) =====
     #route-summary moves in here while the map shows. Its first view, by CSS
     order only (route.js renders the menu's order, untouched): the stats
     row, the red hazard line when the route has a hazard, the climb on one
     line, Start, Save / Share / Clear; about 40% of the screen at most
     (Sheet.ROUTE_PEEK_MAX). One pull away: every warning and closure note
     whole, the climb chart, the informational notes, the rest. The summary's
     wrappers are display: contents here so their children can be ordered. */
  #route-sheet #route-summary {
    display: flex; flex-direction: column;
    padding: 0; margin: 0; border: none; background: none; box-shadow: none;
  }
  #route-sheet #route-summary > h3 { display: none; }
  /* The chart keeps its own heading, right above it (Josh, 2026-11-01: "the elevation profile chart
     has no descriptive header"). Same order as the chart, and first in the DOM, so it sits on top. */
  #route-sheet .elevation-profile > h4 { order: 6; margin: 4px 0 6px; }
  #route-sheet #route-summary-content, #route-sheet #route-elevation-slot,
  #route-sheet .elevation-profile { display: contents; }
  #route-sheet #route-summary > *, #route-sheet #route-summary-content > * { order: 10; }
  #route-sheet #route-summary-content > .route-overview { order: 1; margin: 0 52px 6px 0; gap: 6px; }
  #route-sheet .route-stat { padding: 6px 4px; }
  #route-sheet .route-stat-value { font-size: 1rem; }
  #route-sheet .route-stat-label { font-size: 0.78rem; }
  /* The climb fits the card: nothing runs past its edge, nothing scrolls
     sideways (Josh, 2026-11-01, S10, at 360-384 px: "555m gain · 269m loss ·
     753m max · 4% max g..."). The first view has room for three figures on
     one line, so it shows gain, loss and the top; the steepest grade
     (.elev-maxgrade, js/route.js) joins them in the full card, where a line
     that does not fit wraps whole figures, never cut ones. */
  #route-sheet .elevation-stats {
    order: 2; flex-wrap: wrap; gap: 4px 12px; margin: 0 0 8px; font-size: 0.8rem;
    min-width: 0; max-width: 100%;
  }
  #route-sheet .elevation-stats span { flex: 0 0 auto; white-space: nowrap; }
  #route-sheet:not(.sheet-full) .elevation-stats .elev-maxgrade { display: none; }
  /* The red hazard line (js/sheet.js _hazardLineFill; Josh, 2026-11-01, S10:
     "the no dogs warning made the menu come and cover most of the screen"):
     right under the stats, the first hazard's words and "(+1 more)", one line
     cut with an ellipsis, 48 px tall to tap; the tap opens the card full at
     the warnings. Order 2 with the climb: it comes first in the page. */
  #route-sheet #route-hazard-line:not([hidden]) {
    order: 2; display: flex; align-items: center; gap: 8px; width: 100%;
    min-height: 48px; margin: 0 0 6px; padding: 0 12px;
    border: 1.5px solid #ff5252; border-radius: var(--radius-sm);
    background: rgba(255, 82, 82, 0.12); color: #ff8a80;
    font: inherit; font-size: 0.8rem; font-weight: 700; text-align: left; cursor: pointer;
  }
  #route-sheet #route-hazard-line i { flex: 0 0 auto; }
  #route-sheet .route-hazard-line-text { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  #route-sheet #route-start-inline { order: 3; min-height: 48px; margin: 0 0 8px; }
  #route-sheet #route-card-actions { order: 4; display: flex; gap: 8px; margin: 0 0 10px; }
  /* Three equal thirds that may shrink (min-width: 0) and a label that may
     wrap or hyphenate inside its button: one word plus an icon and 14 px
     padding ran Clear off a 360 px screen in de/fr/es, and every Saved label
     past it (EU review, 2026-11-01; html lang is set by I18N, so hyphens work). */
  #route-sheet #route-card-actions .btn { flex: 1 1 0; min-width: 0; min-height: 48px; padding: 2px 8px; justify-content: center; display: inline-flex; align-items: center; gap: 5px; text-align: center; line-height: 1.15; white-space: normal; text-wrap: balance; }
  #route-sheet #route-card-actions .btn > span { min-width: 0; overflow-wrap: anywhere; hyphens: auto; }
  #route-sheet #route-card-actions .btn[hidden] { display: none; }
  /* Clear in red, last in the row so it sits on the right under Start (Josh,
     2026-11-01: "the 'clear' button needs to be red, under the start button
     on the right"), styled like the old floating Clear (#route-clear-btn):
     a 1.5 px red border, the text and its icon in #ff8a80 on the card's own
     dark background, 48 px tall. Save and Share stay neutral. */
  #route-sheet #route-card-actions #route-card-clear {
    border: 1.5px solid #ff5252; background: transparent; color: #ff8a80; font-weight: 700;
  }
  #route-sheet #route-card-actions #route-card-clear i { color: #ff8a80; }
  #route-sheet #route-card-actions #route-card-clear:active { background: #ff5252; color: #fff; }
  #route-sheet #route-card-actions #route-card-clear:active i { color: #fff; }
  /* Hazards, whole right under the first view, where the red line's tap
     lands (Sheet._openWarnings): the stops cut off, the red "Do not take this
     route" box, and a notes or warnings slot holding a hazard route.js marked
     (.route-hazard: a closure on the route, a rig limit, a red warning). A
     slot of informational notes only ("Worth a Stop", "Offline ready", speed
     cameras) goes after the climb chart (NZ S10, Tekapo to Twizel: 62-88% with
     them in the first view, about 38% without). */
  #route-sheet #route-sealed-slot, #route-sheet #route-block-slot { order: 5; }
  #route-sheet #route-notes-slot, #route-sheet #route-warnings-slot { order: 7; }
  #route-sheet #route-notes-slot:has(.route-hazard),
  #route-sheet #route-warnings-slot:has(.route-hazard) { order: 5; }
  /* The offline router's "planned on your device" box is the route's distance
     and time (an offline route has no stat tiles): with the stats, whole. */
  #route-sheet #route-summary-content > .route-device-summary { order: 1; margin: 0 52px 8px 0; }
  #route-sheet .elevation-chart-wrap, #route-sheet .elev-pending { order: 6; margin: 0 0 12px; }
  /* GPX is a file to take elsewhere, not a step to make the route work (Josh, 2026-11-01): last. */
  #route-sheet #route-summary-content > .route-gpx { order: 20; }
  /* Below the fold: the surface key and the route options. Not the GPX block,
     which is also a div with no id after the key slot: :not([id]) made this the
     more specific rule and put GPX at order 8, above the fuel line, the trip
     plan and "Problem with this route" (S10, 2026-11-02). It stays last (20). */
  #route-sheet #route-key-slot, #route-sheet #route-key-slot ~ div:not([id]):not(.route-gpx),
  #route-sheet #route-segments-slot { order: 8; }
  /* One surface for a route on a phone: the route card, in its peek, full or
     mini state (Josh, 2026-11-01, S10: "i closed the route and the start
     button was below it": closing the card brought back the floating Start
     and Clear, the distance tiles and the Save / Email me / Share row). They
     never show on a phone; js/route-start.js and js/route-hud.js do not put
     them up there either, so nothing is framed around them. The desktop
     keeps all four. The tiles show during the tour alone: tour v3's plan
     step rings them, and the route card is shut on every step but its own. */
  #route-actions, #route-start-btn, #route-clear-btn, #btn-clear-route,
  body:not(.onboard-on) #route-hud { display: none !important; }
  /* The mini bar: the handle (22 px) and one 46 px row over the card's top
     (Sheet.MINI = 22 + 46): the distance and time (a tap is the peek) and
     Start. No X: the bar is as small as the card gets while a route is up. */
  #route-sheet.sheet-mini #route-mini {
    display: flex; align-items: center; gap: 10px;
    position: absolute; left: 0; right: 0; top: 22px; height: 48px; z-index: 2;
    padding: 0 12px 0 18px; background: var(--bg-secondary);
  }
  #route-sheet.sheet-mini .sheet-close { display: none; }
  #route-mini .route-mini-figs {
    flex: 1 1 auto; min-width: 0; min-height: 48px; padding: 0; text-align: left;
    background: none; border: none; color: var(--text-primary); cursor: pointer;
    font: inherit; font-size: 1rem; font-weight: 700;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  #route-mini .route-mini-start {
    flex: 0 0 auto; width: auto; min-height: 48px; padding: 0 18px;
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  }
  #route-mini .route-mini-start[hidden] { display: none; }
  /* Under the bar, the phone's own bottom inset (the panel's padding, 34 px
     on an iPhone) would show the card's content: half its Start, a second
     one to tap. Hidden, not removed: Sheet still measures it. */
  #route-sheet.sheet-mini .sheet-content { visibility: hidden; }
  /* One Start in the slot, and the route's own: while a route is up its mini
     bar keeps its Start, and a searched pin's floating Start (js/search-go.js)
     never shows over it. Floated just above the bar it read as the route's
     Start, and a tap cleared the route and went to the searched place
     (2026-11-01 review). The searched place's card has its own Go. */
  body.route-mini #search-go-btn { display: none !important; }
  /* The mini bar is not a card over the map: Topo/Sat, 2D/3D, the pan pad
     and the scale ruler stay, lifted above it (body.route-mini, Sheet._body). */
  body.route-mini:not(.onboard-on) #map-ptz,
  body.route-mini:not(.onboard-on) #basemap-switch,
  body.route-mini:not(.onboard-on) #mode-3d-switch,
  body.route-mini:not(.onboard-on) .maplibregl-ctrl-bottom-right,
  /* ...and the launch cards along the bottom edge (bottom 22 px), which no
     longer wait for the bar (Sheet.cardUp): above it, never over its Start. */
  body.route-mini:not(.onboard-on) #install-card,
  body.route-mini:not(.onboard-on) #share-card,
  body.route-mini:not(.onboard-on) #fb-follow-card,
  /* The trip pass offer too (inline bottom 16 px, z-index 9000): it covered the bar's Start. */
  body.route-mini:not(.onboard-on) #pass-upgrade-card { translate: 0 calc(-70px - env(safe-area-inset-bottom, 0px)); }
  #sidebar-collapse, #sidebar-expand { display: none !important; }

  .info-details { grid-template-columns: 1fr; }
  .route-overview { grid-template-columns: 1fr 1fr; }

  /* Inputs must be ≥16px to prevent iOS Safari from zooming on focus */
  .route-point input,
  .search-box input,
  .date-field input {
    font-size: 1rem;
  }
}

/* In landscape on mobile: compact the sidebar so content fits above the keyboard */
@media (max-width: 768px) and (orientation: landscape) {
  .sidebar-header { padding-top: calc(var(--mobile-header-height) + 4px); padding-bottom: 6px; }
  .sidebar-section { padding: 10px 16px; }
  .sidebar-section h3 { margin-bottom: 8px; }
  .tab-btn { padding: 8px 4px; }
}

@media (max-width: 480px) {
  .tab-btn { padding: 10px 4px; }
  .basemap-selector { flex-direction: column; }
  .weather-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ===== Context Menu ===== */
#context-menu {
  position: fixed;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  z-index: 2000;
  min-width: 200px;
  padding: 4px 0;
  overflow: hidden;
  /* Bounded, or the tail is unreachable: on a landscape phone with about
     304px of viewport the both-ends menu is roughly 390px tall, the clamp
     pins it at top 8px, and "Something missing here? Add it yourself" / "Something's wrong
     here" sat off the bottom of the screen with no way to reach them.
     (Routing sweep, 2026-08-31.) 16px is the clamp's 8px margin at each end;
     the dvh line wins where supported so a phone browser's toolbar does not
     eat the bottom rows. overflow-y after overflow, so x stays clipped to
     the rounded corners while y scrolls. */
  max-height: calc(100vh - 16px);
  max-height: calc(100dvh - 16px);
  overflow-y: auto;
}
.ctx-item {
  padding: 10px 16px;
  font-size: 0.82rem;
  cursor: pointer;
  display: flex; align-items: center; gap: 10px;
  transition: background 0.15s;
  color: var(--text-primary);
}
.ctx-item:hover { background: rgba(255,255,255,0.06); }
.ctx-item i { color: var(--accent); width: 16px; text-align: center; font-size: 0.85rem; }
.ctx-divider { height: 1px; background: var(--bg-tertiary); margin: 4px 0; }
/* The state header. Names what the app ALREADY holds, so the rider can see
   which half of the route they are on before they choose. Not a .ctx-item: it
   takes no click. Place names sit in .place-name, which EU's i18n skips. */
.ctx-head {
  padding: 8px 16px 7px;
  font-size: max(0.72rem, var(--text-floor, 0px));
  color: var(--text-muted);
  border-bottom: 1px solid var(--bg-tertiary);
  margin-bottom: 4px;
  max-width: 270px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The "instead" lines. Real choices, deliberately quieter than the one obvious
   next step above them: a rider who wants to fix a wrong start can, but nobody
   picks one by mistake while reaching for the loud item. */
.ctx-sub {
  font-size: max(0.76rem, var(--text-floor, 0px));
  padding-top: 7px; padding-bottom: 7px;
  color: var(--text-muted);
}
.ctx-sub i { color: var(--text-muted); }

/* ===== Ride Dashboard ===== */

/* When dashboard is open, hide EVERYTHING else */
body.dashboard-active #map,
body.dashboard-active #sidebar,
body.dashboard-active #mobile-header,
body.dashboard-active #map-controls,
body.dashboard-active #info-panel,
body.dashboard-active #context-menu,
body.dashboard-active .maplibregl-control-container {
  display: none !important;
  visibility: hidden !important;
}

body.dashboard-active {
  overflow: hidden;
  background: #0a0e14;
}

/* index.html asks for viewport-fit=cover and a black-translucent status bar, so
   an installed iPhone app draws UNDER the status bar, the Dynamic Island and
   the home indicator. Pad the dash in by those insets so no readout hides
   behind them (an iPhone 16 Pro Max keeps 59px upright). */
#dashboard-overlay {
  position: fixed;
  inset: 0;
  z-index: 99999;
  background: #0a0e14;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)
           env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
  overflow: hidden;
}

/* An absolutely positioned child measures from the overlay's PADDING box, so
   the padding above does not move it — it needs the insets itself. Without
   them the X sat at y=6..46 on an iPhone, inside the strip iOS keeps for the
   status bar and the Control Centre pull-down: the rider could see it and his
   taps went to the system, so the only way out of the dash was to kill the app
   (rider report, 2026-09-18). */
.dash-close-btn {
  position: absolute;
  top: calc(6px + env(safe-area-inset-top, 0px));
  right: calc(6px + env(safe-area-inset-right, 0px));
  z-index: 100002;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--bg-tertiary);
  background: rgba(26,38,50,0.8);
  color: var(--text-primary);
  font-size: 1.1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  touch-action: manipulation;
}
.dash-close-btn:hover { color: var(--danger); }

/* Dashboard layout — landscape-first: top bar, 3-col main, bottom bar */
.dash-layout {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  overflow: hidden;
  position: relative;
}

/* ── Top instrument bar ── */
.dash-top-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(16px, 4vw, 40px);
  padding: 10px 60px;
  background: rgba(10,14,20,0.9);
  border-bottom: 1px solid var(--bg-tertiary);
  min-width: 0;
  z-index: 2;
  overflow: visible;
}

.dash-alt-reading {
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  min-width: 0;
  font-variant-numeric: tabular-nums;
}

.dash-alt-reading i {
  color: var(--accent);
  font-size: 1rem;
}

.dash-alt-value {
  font-size: clamp(1.3rem, 3vw, 1.8rem);
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.dash-alt-unit {
  font-size: 0.9rem;
  color: var(--text-secondary);
}

.dash-alt-reading #dash-heading-value {
  font-size: clamp(1.2rem, 3vw, 1.7rem);
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.dash-info-unit {
  font-size: 0.8rem;
  color: var(--text-secondary);
  margin-left: 2px;
}

/* Pressure trend arrow */
.dash-pressure-arrow {
  transition: transform 0.3s ease;
  align-self: center;
  flex-shrink: 0;
  margin-left: 2px;
}

/* Wind direction arrow */
.dash-wind-arrow {
  transition: transform 0.5s ease-out;
  align-self: center;
  flex-shrink: 0;
  margin-left: 1px;
}

/* ── Main 3-column area ── */
.dash-main {
  flex: 1;
  display: flex;
  min-height: 0;
  overflow: hidden;
}

/* Left column: radar */
.dash-left-col {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 8px;
  gap: 4px;
}

.dash-radar-title {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 3px;
  color: var(--text-primary);
}

.dash-radar-wrap {
  position: relative;
  width: min(55vh, 220px);
  aspect-ratio: 1;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid rgba(255,255,255,0.08);
  background: rgba(20,28,38,0.6);
}

.dash-radar-canvas {
  width: 100%;
  height: 100%;
  border-radius: 12px;
}

.dash-radar-label {
  position: absolute;
  bottom: 4px;
  left: 50%;
  transform: translateX(-50%);
  font-size: max(0.55rem, var(--text-floor, 0px));
  color: var(--text-muted);
  background: rgba(10,14,20,0.85);
  padding: 1px 5px;
  border-radius: 3px;
}

/* Centre column: speed + lean */
.dash-centre-col {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  position: relative;
}

.dash-speed-zone {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

.dash-speed-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 1;
}

.dash-speed-num {
  font-size: min(28vw, 38vh);
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.dash-speed-unit {
  font-size: 1rem;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 3px;
  margin-top: -2px;
}

.dash-speed-sub {
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-top: 4px;
}

.dash-speed-max {
  font-size: 1.2rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Lean angle arcs */
.dash-lean-arcs {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
}

.dash-lean-readout {
  font-size: 1.3rem;
  font-weight: 600;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  min-width: 4ch;
}

/* Right column: compass OR nav mode */
.dash-right-col {
  flex: 0 0 auto;
  min-width: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 8px;
}

/* Nav mode container (minimap + next turn) */
.dash-nav-mode {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  width: 100%;
  min-width: 0;
  height: 100%;
  /* Landscape (the usual dash orientation) has little vertical room: the square
     minimap + next-turn + Home button can exceed the column height. Scroll
     instead of clipping so the buttons are never stranded off-window;
     overscroll-contain keeps the page behind from moving. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* Next turn indicator */
.dash-next-turn {
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 10px;
  padding: 8px 12px;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
  flex-shrink: 0;
  overflow: hidden;
}

.dash-turn-icon {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: rgba(0, 200, 83, 0.15);
  color: #00c853;
  font-size: 1.3rem;
}

.dash-turn-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.dash-turn-dist {
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--text-primary, #fff);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.dash-turn-road {
  font-size: max(0.68rem, var(--text-floor, 0px));
  color: var(--text-muted, #999);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.2;
}

/* Minimap */
.dash-minimap-wrap {
  position: relative;
  flex: 0 0 auto;
  width: min(55vh, 220px);
  aspect-ratio: 1 / 1;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.08);
}

.dash-minimap {
  position: absolute;
  top: -21%;
  left: -21%;
  width: 142%;
  height: 142%;
  background: #0a0e14;
  transition: transform 0.4s ease-out;
  transform-origin: center center;
}

/* Hide MapLibre GL chrome on minimap */
.dash-minimap .maplibregl-control-container { display: none !important; }

/* Home button */
.dash-home-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  max-width: min(55vh, 220px);
  padding: 7px 12px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-primary, #fff);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.5px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dash-home-btn:active { background: rgba(255, 255, 255, 0.14); }
.dash-home-btn.home-set { border-color: rgba(0, 200, 83, 0.4); color: #00c853; }

/* Mode toggle button (compass ↔ map) */
.dash-mode-toggle {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 6px;
  background: rgba(255,255,255,0.06);
  color: var(--text-muted, #999);
  font-size: max(0.68rem, var(--text-floor, 0px));
  cursor: pointer;
  transition: background 0.2s, color 0.2s;
}
.dash-mode-toggle:hover, .dash-mode-toggle:active {
  background: rgba(255,255,255,0.12);
  color: var(--text-primary, #fff);
}

/* Rider dot on minimap */
.dash-minimap-rider { background: none !important; border: none !important; }
.dash-rider-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #00c853;
  border: 2px solid #fff;
  box-shadow: 0 0 8px rgba(0, 200, 83, 0.7);
}

.dash-compass-zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
}

.dash-compass-arrow-wrap {
  flex-shrink: 0;
  width: min(90px, 24vh);
  height: min(90px, 24vh);
  display: flex;
  align-items: center;
  justify-content: center;
  filter: drop-shadow(0 0 14px rgba(0,200,83,0.5));
}

.dash-compass-arrow {
  width: 100%;
  height: 100%;
  transition: transform 0.5s ease-out;
  transform-origin: 50% 50%;
}

.dash-compass-arrow.no-target {
  opacity: 0.25;
  filter: grayscale(1);
}

.dash-compass-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 0;
}

.dash-compass-target {
  font-size: max(0.75rem, var(--text-floor, 0px));
  color: var(--text-primary);
  font-weight: 600;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
}

.dash-recalc-banner {
  font-size: max(0.72rem, var(--text-floor, 0px));
  font-weight: 600;
  color: #40c4ff;
  background: rgba(64, 196, 255, 0.1);
  border: 1px solid rgba(64, 196, 255, 0.3);
  border-radius: 10px;
  padding: 3px 9px;
  white-space: nowrap;
  margin-top: 2px;
}

.dash-compass-readout {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: center;
}

.dash-compass-num {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.dash-compass-brg {
  font-size: max(0.65rem, var(--text-floor, 0px));
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* ── Bottom bar: elevation + stats ── */
.dash-bottom-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  background: rgba(10,14,20,0.9);
  border-top: 1px solid var(--bg-tertiary);
  min-height: 36px;
  z-index: 2;
}

/* Elevation sparkline strip */
.dash-elevation-strip {
  flex: 0 0 auto;
  width: 40%;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-right: 1px solid var(--bg-tertiary);
  height: 100%;
}

.dash-elevation-canvas {
  flex: 1;
  height: 32px;
  min-width: 0;
}

.dash-elevation-stats {
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-size: max(0.6rem, var(--text-floor, 0px));
  color: var(--text-muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.dash-elevation-stats .elev-gain { color: var(--accent); }
.dash-elevation-stats .elev-max { color: var(--text-secondary); }

/* Trip stats strip */
.dash-stats-strip {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  padding: 4px 12px;
  gap: 12px;
}

.dash-stat {
  display: flex;
  align-items: baseline;
  gap: 3px;
  font-variant-numeric: tabular-nums;
}

.dash-stat i {
  color: var(--text-muted);
  font-size: max(0.6rem, var(--text-floor, 0px));
  align-self: center;
}

.dash-stat span:not(.dash-stat-unit) {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-secondary);
  min-width: 2.5ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.dash-stat-unit {
  font-size: max(0.6rem, var(--text-floor, 0px));
  color: var(--text-muted);
}

/* Address input popover */
.dash-compass-input {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(420px, 90vw);
  background: rgba(10,14,20,0.96);
  border: 1px solid var(--bg-tertiary);
  border-radius: 10px;
  padding: 12px;
  z-index: 100003;
  box-shadow: 0 8px 28px rgba(0,0,0,0.6);
}

/* In landscape the virtual keyboard takes the bottom half — anchor popup to top
   so it stays in the visible area above the keyboard. */
@media (orientation: landscape) {
  .dash-compass-input {
    top: max(env(safe-area-inset-top, 0px), 8px);
    transform: translateX(-50%);
    max-height: calc(100dvh - 24px);
    overflow-y: auto;
  }
}

.dash-compass-input input {
  width: 100%;
  padding: 10px 12px;
  background: var(--bg-secondary);
  color: var(--text-primary);
  border: 1px solid var(--bg-tertiary);
  border-radius: 6px;
  font-size: 1rem; /* must be ≥16px to suppress iOS auto-zoom */
  box-sizing: border-box;
}

.dash-compass-results {
  max-height: 180px;
  overflow-y: auto;
  margin-top: 8px;
}

@media (orientation: landscape) {
  .dash-compass-results {
    max-height: min(120px, 30vh);
  }
}

.dash-compass-results-item {
  padding: 8px 10px;
  color: var(--text-primary);
  font-size: 0.85rem;
  border-radius: 4px;
  cursor: pointer;
}

.dash-compass-results-item:hover,
.dash-compass-results-item:active {
  background: var(--bg-tertiary);
}

.dash-compass-input-row {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 8px;
}

.dash-compass-input-row button {
  padding: 8px 14px;
  background: var(--bg-secondary);
  color: var(--text-primary);
  border: 1px solid var(--bg-tertiary);
  border-radius: 6px;
  font-size: 0.85rem;
  cursor: pointer;
}

.dash-compass-input-row button:hover {
  background: var(--bg-tertiary);
}

/* ── Portrait fallback ── */
@media (orientation: portrait) {
  .dash-top-bar { padding: 6px 50px 6px 16px; gap: 10px; flex-wrap: wrap; }
  .dash-main { flex-direction: column; }
  .dash-left-col { flex: 0 0 auto; order: 3; padding: 4px; }
  .dash-left-col .dash-radar-wrap { width: min(30vw, 140px); }
  .dash-centre-col { flex: 1; }
  .dash-speed-num { font-size: min(42vw, 30vh); }
  .dash-right-col { flex: 0 0 auto; order: 2; padding: 4px; }
  .dash-right-col .dash-compass-zone { flex-direction: row; gap: 16px; }
  .dash-right-col .dash-compass-arrow-wrap { width: min(80px, 14vh); height: min(80px, 14vh); }
  .dash-right-col .dash-compass-info { align-items: flex-start; }
  .dash-nav-mode { flex-direction: row; height: auto; gap: 8px; }
  .dash-minimap-wrap { flex: 0 0 120px; height: 120px; }
  .dash-next-turn { padding: 6px 10px; }
  .dash-turn-icon { width: 32px; height: 32px; font-size: 1rem; }
  .dash-turn-dist { font-size: 1rem; }
  .dash-bottom-bar { flex-direction: column; }
  .dash-elevation-strip { width: 100%; border-right: none; border-bottom: 1px solid var(--bg-tertiary); }
  .dash-stats-strip { width: 100%; }
}

/* Desktop: cap the speed font size */
@media (min-width: 769px) and (orientation: landscape) {
  .dash-speed-num { font-size: min(28vw, 38vh); }
  .dash-right-col { flex: 0 0 auto; }
  .dash-radar-wrap { width: min(60vh, 240px); }
  .dash-compass-arrow-wrap { width: min(110px, 28vh); height: min(110px, 28vh); }
  .dash-minimap-wrap { width: min(60vh, 240px); }
}

/* ===== Hidden utility ===== */
.hidden { display: none !important; }

/* ===== Full-screen Navigation Overlay (map + rain radar + speed) ===== */
/* Like the speedo dash, hide everything else behind it. */
body.nav-active #map,
body.nav-active #sidebar,
body.nav-active #mobile-header,
body.nav-active #map-controls,
body.nav-active #info-panel,
body.nav-active #context-menu,
body.nav-active .maplibregl-control-container {
  display: none !important;
  visibility: hidden !important;
}
body.nav-active { overflow: hidden; background: #0a0e14; }

#nav-overlay {
  position: fixed;
  inset: 0;
  z-index: 99999;
  background: #0a0e14;
  overflow: hidden;
}

/* Clip the oversized, rotating map. */
#nav-map-wrap {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
/* A square whose side (145vmax) exceeds any viewport's diagonal (≤ ~1.415·vmax),
   centred with negative margins. It was sized so a CSS rotation never showed a
   corner gap; heading-up is the GL bearing now (js/nav.js applyRotation) and
   the element is never rotated. Nav now shrinks it to the screen in 2D and 3D
   (nav-fit, js/nav.js fitNavCanvasFlat / fitNavCanvasTo3D); the square is
   what the battery switch's "old" puts back. */
#nav-map {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 145vmax;
  height: 145vmax;
  margin-left: -72.5vmax;
  margin-top: -72.5vmax;
  transform-origin: center center;
  /* No CSS transition: nav.js eases the rotation itself, frame by frame (see
     startHeadingLoop). A transition on top would restart every frame and lag. */
  background: #0a0e14;
  will-change: transform;
}
/* The square's size from the LARGE viewport, which does not change as Safari's
   bars slide in and out (vmax does, and every change resized a WebGL canvas,
   which leaks on iOS). max(lvw, lvh) is the long side in either orientation. */
@supports (width: 1lvh) {
  #nav-map {
    width: calc(1.45 * max(100lvw, 100lvh));
    height: calc(1.45 * max(100lvw, 100lvh));
    margin-left: calc(-0.725 * max(100lvw, 100lvh));
    margin-top: calc(-0.725 * max(100lvw, 100lvh));
  }
}
/* The screen-sized canvas, 2D and 3D. The GL camera bearing does heading-up,
   so the square above is never rotated, and every pixel of it outside the
   screen was drawn and then clipped away (2D on an S23: 6.0 Mpx a frame for a
   2.5 Mpx screen). Centred where the square was centred, so the map's centre
   is the same point on screen; in 3D js/nav.js also narrows the field of
   view to match. */
#nav-map.nav-fit {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  will-change: auto;
}
/* Sized from the LARGE viewport, as the square is: a size that follows
   Safari's sliding bars resizes the WebGL canvas each time, which leaks on
   iOS (WebKit bug 219780). The few rows under the bars are clipped. */
@supports (width: 1lvh) {
  #nav-map.nav-fit {
    top: 50%;
    left: 50%;
    width: 100lvw;
    height: 100lvh;
    margin-left: -50lvw;
    margin-top: -50lvh;
  }
}
#nav-map .maplibregl-control-container { display: none !important; }

/* Rider chevron — points up (direction of travel): the marker is viewport-aligned under the GL bearing. */
.nav-rider { background: none !important; border: none !important; }
.nav-rider-chevron {
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  /* eased in JS per frame (see startHeadingLoop) — no CSS transition */
  /* Arrow size setting (js/display-sizes.js). `scale`, not transform: JS owns transform. */
  scale: var(--arrow-scale, 1);
}
.nav-rider-chevron i {
  font-size: 26px;
  color: #2196f3;
  /* fa-location-arrow's art points NE (45°); correct it to point straight up. */
  transform: rotate(-45deg);
  text-shadow: 0 0 6px rgba(0,0,0,0.8), 0 0 2px #fff;
}

/* The in-nav 2D|3D and Topo|Sat pills, and the VOICE pill, were five separate
   pieces of floating glass in four corners. They are one bar now — see #nav-dock
   below (Josh, 2026-09-18). */

/* Low-battery 3D suggestion banner (Android battery API only). */
#nav-battery-suggest {
  position: absolute;
  top: calc(64px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 14;
  max-width: 92vw;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px;
  background: rgba(20,14,10,0.92);
  border: 1px solid rgba(255,180,60,0.4);
  border-radius: 10px;
  font-size: 0.82rem;
  color: var(--text-primary);
}
#nav-battery-suggest button {
  padding: 5px 10px;
  border: 1px solid rgba(255,255,255,0.2);
  border-radius: 6px;
  background: rgba(255,255,255,0.08);
  color: var(--text-primary);
  font-size: 0.78rem;
  cursor: pointer;
}
#nav-battery-suggest button:hover { background: rgba(255,255,255,0.16); }

/* Current speed + speed-limit sign — bottom-left corner. */
#nav-speed {
  position: absolute;
  left: calc(14px + env(safe-area-inset-left, 0px));
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  background: rgba(10,14,20,0.72);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 14px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.nav-speed-read { display: flex; align-items: baseline; gap: 4px; }
#nav-speed-value {
  font-size: 4.2rem;
  font-weight: 800;
  line-height: 1;
  color: #fff;
  font-variant-numeric: tabular-nums;
  letter-spacing: -1.5px;
}
#nav-speed-unit { font-size: 0.9rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; }

/* Speed-limit sign — a circular white disc with a red ring (NZ/EU style). */
#nav-speed-limit {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  width: 54px; height: 54px;
  border-radius: 50%;
  background: #fff;
  border: 5px solid #d32f2f;
  box-shadow: 0 1px 5px rgba(0,0,0,0.55);
}
#nav-speed-limit-value {
  color: #111;
  font-weight: 800;
  font-size: 1.3rem;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.5px;
}

/* Next-turn banner — top. */
/* Rain-radar offline notice — small transient pill, top-centre */
.radar-note {
  position: fixed;
  top: 14px; left: 50%; transform: translateX(-50%);
  z-index: 1200;
  display: flex; align-items: center; gap: 7px;
  max-width: 90vw;
  padding: 8px 14px;
  background: var(--bg-secondary); color: var(--text-primary);
  border: 1px solid var(--bg-tertiary); border-left: 3px solid #ffab40;
  border-radius: 999px; box-shadow: var(--shadow);
  font-size: 0.8rem; font-weight: 600; white-space: nowrap;
  pointer-events: none;
}
.radar-note i { color: #ffab40; }
.radar-note.hidden { display: none; }
#nav-radar-note { top: 70px; z-index: 40; }  /* clear the turn banner in nav */
@media (max-width: 768px) {
  #radar-offline-note { top: 58px; }          /* clear the mobile header */
  .radar-note { font-size: max(0.74rem, var(--text-floor, 0px)); white-space: normal; text-align: center; }
}

#nav-turn {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: min(88vw, 460px);
  padding: 10px 16px 10px 10px;
  background: rgba(10,14,20,0.82);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 16px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 6px 24px rgba(0,0,0,0.4);
}
.nav-turn-icon {
  flex: 0 0 auto;
  width: 46px; height: 46px;
  border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.5rem;
}
.nav-turn-icon.prox-far  { background: rgba(0,200,83,0.15);  color: #00c853; }
.nav-turn-icon.prox-mid  { background: rgba(255,193,7,0.2);  color: #ffc107; }
.nav-turn-icon.prox-near { background: rgba(255,82,82,0.22); color: #ff5252; }
.nav-turn-info { min-width: 0; }
/* A turn more than 3 km off (js/nav.js turnCardFar): not the full card, a
   one-line pill, "Next turn 46 km, Anzac Highway", about a third of the
   card's height. Rod, AUS, 2026-11-03: "it allows you to relax a little
   knowing the next turn of the road you are on is still 46 km away".
   Voice still waits for the usual distances (js/voice.js). Under 3 km the
   same element turns back into the full card, so the two never show at once. */
.nav-turn-next { display: none; }
#nav-turn.turn-far { gap: 8px; padding: 5px 12px 5px 6px; border-radius: 999px; box-shadow: 0 3px 12px rgba(0,0,0,0.35); }
#nav-turn.turn-far .nav-turn-icon { width: 24px; height: 24px; border-radius: 999px; font-size: 0.8rem; background: rgba(255,255,255,0.1); color: #e5e7eb; }
#nav-turn.turn-far .nav-turn-info { display: flex; align-items: baseline; gap: 6px; min-width: 0; white-space: nowrap; }
#nav-turn.turn-far .nav-turn-next { display: inline; font-size: 0.85rem; color: var(--text-secondary); }
#nav-turn.turn-far #nav-turn-dist { font-size: 0.95rem; font-weight: 700; }
.nav-turn-far-road { display: none; }
#nav-turn.turn-far #nav-turn-road { display: none; }
#nav-turn.turn-far .nav-turn-far-road { display: block; font-size: 0.85rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; max-width: 44vw; }
#nav-turn.turn-far .nav-turn-far-road:empty { display: none; }
#nav-turn.turn-far #nav-turn-lanes { display: none; }
#nav-turn-dist { font-size: 1.5rem; font-weight: 800; color: #fff; line-height: 1.05; }
#nav-turn-road {
  font-size: 0.9rem; color: var(--text-secondary);
  /* The card now names the road turned onto (js/route-attrs.js), and
     "At the roundabout, take exit 2 onto Howe Street" is longer than a phone
     is wide: one clipped line showed "onto Ho...", which is the part the
     rider is looking up for. Upright, the line may fold once; on its side
     (no height to spare) it stays one line, clipped. */
  white-space: normal; overflow: hidden; word-break: break-word;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
@media (min-width: 600px) and (max-height: 500px), (orientation: landscape) and (max-height: 500px) {
  #nav-turn-road { display: block; white-space: nowrap; text-overflow: ellipsis; }
}
/* Lane arrows under the turn text (nav.js laneStripSVG). Static SVG, a
   lane is 24 x 28 units drawn at 1.4x: a glance at speed. */
#nav-turn-lanes { margin-top: 6px; line-height: 0; }
#nav-turn-lanes svg { display: block; height: 40px; width: auto; max-width: 100%; }

/* Fixed camera warning — same geometry family as #nav-turn, red-edged, taking
   the slot directly under the turn banner (top offset clears the ~66px turn
   banner sitting at top:12px). A "possible" camera is amber and visibly softer:
   it must never read as a confirmed one. */
#nav-camera {
  position: absolute;
  top: calc(86px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 9;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: min(84vw, 420px);
  padding: 10px 14px;
  background: rgba(40, 12, 12, 0.9);
  border: 1px solid rgba(255, 82, 82, 0.75);
  border-radius: 14px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
}
#nav-camera.hidden { display: none; }
#nav-camera-icon { font-size: 1.5rem; color: #ff5252; flex-shrink: 0; width: 34px; text-align: center; }
#nav-camera-name {
  font-weight: 700; font-size: 0.95rem; color: #fff;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#nav-camera-detail { font-size: 0.78rem; color: var(--text-secondary); }
#nav-camera.cam-possible {
  background: rgba(40, 30, 10, 0.88);
  border-color: rgba(255, 171, 64, 0.7);
}
#nav-camera.cam-possible #nav-camera-icon { color: #ffab40; }

/* "Worth a Stop" heads-up — same geometry as #nav-camera so they share the slot
   under the turn banner cleanly, but purple-edged and it yields to the camera
   warning: when a camera is showing (#nav-overlay.cam-visible) this pill drops
   below it — safety information outranks scenery. Tap = dismiss for the ride. */
#nav-poi {
  position: absolute;
  top: calc(86px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: min(84vw, 420px);
  padding: 10px 14px;
  background: rgba(22, 14, 34, 0.86);
  border: 1px solid rgba(126, 87, 194, 0.55);
  border-radius: 14px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
  cursor: pointer;
}
#nav-poi.hidden { display: none; }
#nav-poi-icon { font-size: 1.5rem; color: #b39ddb; flex-shrink: 0; width: 34px; text-align: center; }
.nav-poi-info { min-width: 0; }
#nav-poi-name {
  font-weight: 700; font-size: 0.95rem; color: #fff;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#nav-poi-detail { font-size: 0.78rem; color: var(--text-secondary); }
/* Scenery yields to the camera warning rather than overlapping it. */
#nav-overlay.cam-visible #nav-poi { top: calc(160px + env(safe-area-inset-top, 0px)); }

/* Camera popup honesty lines (Layers.showCameraPopup) */
.cam-possible { color: #ffab40; font-size: 0.8rem; margin: 4px 0; }
.cam-note { color: var(--text-muted, #999); font-size: max(0.72rem, var(--text-floor, 0px)); margin-top: 6px; line-height: 1.35; }

/* Distance / ETA remaining — top-right. */
#nav-eta {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top, 0px));
  right: calc(14px + env(safe-area-inset-right, 0px));
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  padding: 7px 12px;
  background: rgba(10,14,20,0.72);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
#nav-eta-dist { font-size: 1.15rem; font-weight: 800; color: #fff; line-height: 1.1; }
#nav-eta-time { font-size: 0.8rem; font-weight: 600; color: var(--accent); }

/* Current time — the fullscreen display hides the OS status bar clock (2026-07-28). */
/* The top band (js/nav.js updateTopBand): two small pills in the strip above the clock row,
   left and right of the middle, so a centred camera hole sits between them. Full-screen nav
   hides the phone's status bar; this is ours. Hidden on iPhones (their status bar stays and the
   pills would sit under it) and on the car screen (Motocamp Drive draws its own battery). */
/* Its size comes from the phone's own cutout strip (safe-area-inset-top), so it can never
   reach the clock row in CSS alone: a 34 px strip (S23) gives the full 26 px band 16 px clear
   of the clock; a phone with no strip (or landscape) gives a band 0 px high, nothing drawn. */
.nav-topband { position: absolute; top: max(4px, calc(env(safe-area-inset-top, 0px) - 36px)); z-index: 10; display: flex; align-items: center; gap: 7px;
  height: clamp(0px, calc(env(safe-area-inset-top, 0px) - 8px), 26px); overflow: hidden; padding: 0 10px; box-sizing: border-box; border-radius: 999px;
  background: rgba(10,14,20,0.72); color: #fff; font-weight: 700; font-size: max(0.875rem, var(--text-floor, 0px));
  line-height: 1; white-space: nowrap; pointer-events: none; }
.nav-topband.hidden, html.mc-car .nav-topband, html.ios-top-bar .nav-topband { display: none; }
#nav-status { left: calc(16px + env(safe-area-inset-left, 0px)); }
#nav-battery { right: calc(16px + env(safe-area-inset-right, 0px)); }
#nav-status .nts-sep { width: 1.5px; height: 14px; background: rgba(255,255,255,0.35); border-radius: 1px; }
#nav-battery.low { color: #ff5252; }
#nav-clock {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top, 0px));
  right: calc(14px + env(safe-area-inset-right, 0px));
  z-index: 10;
  padding: 7px 12px;
  background: rgba(10,14,20,0.72);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  font-size: 0.95rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* Route active -> the ETA chip owns the corner; the clock tucks below it. */
/* Direction of travel: a small pill in the same chrome as the clock. */
#nav-compass {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  padding: 7px 10px;
  background: rgba(10,14,20,0.72);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  min-width: 2.2em;
  text-align: center;
  display: flex; align-items: center; gap: 6px;
}
/* A small north-up rose; the needle inside turns to the direction of travel. */
#nav-compass .nav-compass-rose { width: 16px; height: 16px; flex: none; display: block; }
/* Temperature and wind: the compass's twins (same chrome, same height). 32px
   so two rows of them plus the 10px gap (74px) stay inside the ETA chip's
   height: the top row grows not a pixel, which a 360x640 screen with the
   radar note lit has none of (NZ/AUS, 2026-10-18). */
/* The top row is one height: clock, compass, temperature and wind. */
#nav-clock, #nav-compass, .nav-wx-pill { height: 32px; box-sizing: border-box; }
.nav-wx-pill {
  padding: 6px 10px;
  background: rgba(10,14,20,0.72);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
  display: flex; align-items: center; gap: 6px;
}
.nav-wx-pill i { font-size: 0.85rem; opacity: 0.8; }
#nav-wind-arrow { width: 16px; height: 16px; flex: none; display: block; color: rgba(255,255,255,0.45); }
.nav-wx-unit { font-size: max(0.7rem, var(--text-floor, 0px)); font-weight: 600; opacity: 0.75; margin-left: -3px; }
#nav-wx { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
#nav-eta:not(.hidden) ~ #nav-clock {
  top: calc(80px + env(safe-area-inset-top, 0px));
}

/* GPS elevation — bottom-right corner. */
#nav-elev {
  position: absolute;
  right: calc(14px + env(safe-area-inset-right, 0px));
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  z-index: 10;
  display: flex;
  align-items: baseline;
  gap: 5px;
  padding: 7px 12px;
  background: rgba(10,14,20,0.72);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
}
#nav-elev i { font-size: 0.85rem; color: var(--text-secondary); align-self: center; }
#nav-elev-value { font-size: 1.3rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.nav-elev-unit { font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); }

/* Close — a cell of #nav-dock now, not a floating circle in the top-left. */

/* Small screens — trim the outsized readouts so nothing collides. */
@media (max-width: 480px) {
  #nav-speed-value { font-size: 3.4rem; }
  #nav-speed-limit { width: 48px; height: 48px; border-width: 4px; }
  #nav-speed-limit-value { font-size: 1.15rem; }
  #nav-turn { top: calc(66px + env(safe-area-inset-top, 0px)); }
  #nav-camera { top: calc(140px + env(safe-area-inset-top, 0px)); }
  #nav-poi { top: calc(140px + env(safe-area-inset-top, 0px)); }
  #nav-overlay.cam-visible #nav-poi { top: calc(214px + env(safe-area-inset-top, 0px)); }
}

/* ===== Social sign-in buttons (js/auth.js _renderSocialButtons) ===== */
/* Ported from NZ (Trailhead lineage). Rendered only for enabled
   AUTH_PROVIDERS, so these styles are inert until a provider is flipped on at
   go-live. */
.login-divider {
  display: flex; align-items: center; gap: 10px;
  width: 100%; margin: 12px 0;
  font-size: max(0.68rem, var(--text-floor, 0px)); color: var(--text-muted, #999);
  text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap;
}
.login-divider::before, .login-divider::after {
  content: ''; flex: 1; height: 1px; background: rgba(255, 255, 255, 0.12);
}
.social-btn {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; padding: 10px; margin-bottom: 8px;
  border-radius: var(--radius-sm, 6px); border: 1px solid transparent;
  font-size: 0.85rem; font-weight: 600; font-family: inherit;
  cursor: pointer; transition: filter 0.15s ease;
}
.social-btn:hover { filter: brightness(1.08); }
.social-btn i { font-size: 1rem; }
.social-btn-google   { background: #fff;    color: #1f1f1f; border-color: #dadce0; }
.social-btn-google i { color: #4285f4; }
.social-btn-facebook { background: #1877f2; color: #fff; }
.social-btn-apple    { background: #000;    color: #fff; border-color: rgba(255,255,255,0.25); }

/* Voice picker row (Voice Guidance sidebar section) */
.voice-pick-row { display: flex; align-items: center; gap: 8px; margin: 8px 0 4px; }
.voice-pick-row .toggle-label { flex-shrink: 0; }
.voice-pick-row select {
  flex: 1; min-width: 0; padding: 5px 7px; font-size: 0.78rem;
  background: var(--bg-elevated, #1c1c22); color: var(--text-primary, #fff);
  border: 1px solid rgba(255,255,255,0.14); border-radius: var(--radius, 8px);
}
.voice-pick-row .btn-sm { padding: 5px 10px; font-size: max(0.75rem, var(--text-floor, 0px)); flex-shrink: 0; }
/* A list that does nothing right now (the speed alert's, with its switch off). */
.voice-pick-row select:disabled { opacity: 0.45; cursor: default; }
.voice-pick-row:has(select:disabled) .toggle-label { opacity: 0.45; }
/* The speed alert's list has short options: it takes their width, so the
   label stays on one line, and is 44 px tall to tap. */
#speed-alert-over { flex: 0 0 auto; min-height: 44px; }

/* ===== Trip recording (js/ride-recorder.js) ===== */
.map-btn.recording {
  background: #d32f2f;
  border-color: #d32f2f;
  color: #fff;
}
.map-btn.recording .map-btn-label { color: #fff; }
.map-btn.recording i { animation: rec-pulse 1.6s ease-in-out infinite; }
@keyframes rec-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* Current-location puck (shown while recording a trip).
   A green dot + heading arrow marking where you ARE now — the breadcrumb line
   shows where you've BEEN. The arrow points up in the element and the marker is
   rotation-aligned to the map, then rotated to the GPS heading, so it points
   your direction of travel; .no-heading hides it when stationary rather than
   leaving it frozen pointing somewhere you are not going. */
.loc-puck { width: 30px; height: 30px; position: relative; pointer-events: none; }
.loc-puck .puck-pulse {
  position: absolute; inset: 0; margin: auto; width: 20px; height: 20px;
  border-radius: 50%; background: #00e676; opacity: 0.4;
  animation: puckPulse 2s ease-out infinite;
}
.loc-puck .puck-dot {
  position: absolute; inset: 0; margin: auto; width: 18px; height: 18px;
  border-radius: 50%; background: #00e676; border: 3px solid #fff;
  box-shadow: 0 0 3px rgba(0,0,0,0.5);
}
.loc-puck .puck-arrow {
  position: absolute; top: -3px; left: 50%; transform: translateX(-50%);
  width: 0; height: 0; border-left: 6px solid transparent;
  border-right: 6px solid transparent; border-bottom: 9px solid #00e676;
  filter: drop-shadow(0 -1px 0 #fff);
}
.loc-puck.no-heading .puck-arrow { display: none; }
@keyframes puckPulse {
  0% { transform: scale(1); opacity: 0.45; }
  100% { transform: scale(2.6); opacity: 0; }
}

/* ===== 2D | 3D mode switch (⬅ NZ 64526ee / USA) ===== */
#mode-3d-switch {
  position: fixed;
  bottom: 26px;
  left: calc(var(--sidebar-width) + 22px);
  z-index: 500;
  display: flex;
  gap: 4px;
  padding: 4px;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: 999px;
  box-shadow: var(--shadow);
}
.mode-switch-btn {
  min-width: 76px;
  min-height: 46px;
  padding: 0 20px;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--text-secondary);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 1.5px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  transition: all var(--transition);
}
.mode-switch-btn i { font-size: 0.95rem; }
.mode-switch-btn:hover { color: var(--text-primary); background: rgba(255,255,255,0.08); }
.mode-switch-btn.active { background: var(--accent); color: #fff; }
body.dashboard-active #mode-3d-switch,
body.nav-active #mode-3d-switch { display: none !important; }

/* Basemap switch (Topo | Sat) — a pill stacked directly ABOVE the 2D/3D
   toggle, same left edge and the same 62px gap at both breakpoints. EU shipped
   without it: the pair existed only inside the Layers panel, so on the main map
   there was no way to reach satellite without opening the sidebar, unlike every
   other region. Same .basemap-btn contract as the sidebar pair, so
   Layers.setupBasemapSelector wires these too and keeps .active in step across
   both. */
#basemap-switch {
  position: fixed;
  bottom: 88px;
  left: calc(var(--sidebar-width) + 22px);
  z-index: 500;
  display: flex;
  gap: 4px;
  padding: 4px;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: 999px;
  box-shadow: var(--shadow);
}
#basemap-switch .basemap-btn i { font-size: 0.95rem; }
body.dashboard-active #basemap-switch,
body.nav-active #basemap-switch { display: none !important; }

/* One-time 3D notices (software-GL warning, governor floor) — never blocking */
.mode-notice {
  position: fixed;
  top: 18px; left: 50%;
  transform: translateX(-50%);
  z-index: 2500;
  max-width: min(92vw, 560px);
  display: flex; align-items: center; gap: 10px;
  padding: 12px 16px;
  background: var(--bg-secondary);
  border: 1px solid var(--warning);
  border-radius: var(--radius);
  color: var(--text-primary);
  font-size: 0.85rem;
  box-shadow: var(--shadow);
}
.mode-notice i { color: var(--warning); }
.mode-notice .mode-notice-ok {
  margin-left: 6px;
  padding: 8px 16px;
  min-height: 40px;
  background: var(--accent); color: #fff;
  border: none; border-radius: var(--radius-sm);
  font-weight: 700; cursor: pointer;
}
/* On a phone the buttons go UNDER the text, not beside it. The row above put
   Download and Not now alongside a five-line offer and squeezed the words into
   a column four words wide (Josh, 2026-09-02, screenshot: "put the buttons at
   the bottom. not the side."). Icon and text share the top row; the buttons
   take the row below, aligned with the text, two across when there are two.
   The card also drops below the mobile header rather than covering the menu. */
@media (max-width: 768px) {
  .mode-notice {
    top: calc(var(--mobile-header-height) + 8px);
    width: 92vw;             /* a grid shrinks to its buttons; take the row */
    display: grid;
    grid-template-columns: auto 1fr 1fr;
    gap: 10px;
  }
  .mode-notice > i { grid-row: 1; grid-column: 1; }
  .mode-notice > span { grid-row: 1; grid-column: 2 / -1; }
  .mode-notice .mode-notice-ok { grid-row: 2; margin-left: 0; }
  .mode-notice .mode-notice-ok:first-of-type { grid-column: 2; }
  .mode-notice .mode-notice-ok:nth-of-type(2) { grid-column: 3; }
  .mode-notice .mode-notice-ok:only-of-type { grid-column: 2 / -1; }
}
/* Phone placement: the phone's map chrome, end of file. */

/* ===== AI route planner (ported from NZ) ===== */
.ai-route-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 12px; }
.ai-route-head h3 { margin-bottom: 0; }
.ai-route-log { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.ai-route-log:empty { display: none; }
.ai-route-msg { padding: 9px 12px; border-radius: 10px; font-size: 0.95rem; line-height: 1.4; max-width: 90%; white-space: pre-wrap; word-break: break-word; }
.ai-route-user { align-self: flex-end; background: var(--accent-dim); color: var(--text-primary); }
.ai-route-assistant { align-self: flex-start; background: var(--bg-tertiary); color: var(--text-secondary); }
.ai-route-cards { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
/* A turn the APP asked for on the rider's behalf: same bubble, but it says so,
   because a message the rider never typed must not sit in their own voice. */
.ai-route-msg.ai-route-auto { opacity: 0.85; border-left: 3px solid var(--accent, #00c853); }
.ai-route-msg.ai-route-auto::before {
  content: 'Asked for you';
  display: block;
  font-size: max(0.62rem, var(--text-floor, 0px));
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-muted, #9aa8a1);
  margin-bottom: 3px;
}

.ai-route-card { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; text-align: left; padding: 8px 10px; border: 1px solid var(--bg-tertiary); border-radius: var(--radius); background: var(--bg-card); color: var(--text-primary); cursor: pointer; }
.ai-route-card:hover { border-color: var(--accent); }
.ai-route-card-on { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.ai-route-card:disabled { opacity: 0.5; cursor: default; }
.ai-card-title { font-weight: 600; font-size: 1rem; }
.ai-card-dist { font-size: 0.9rem; color: var(--accent); font-weight: 600; }
.ai-card-sum { grid-column: 1 / -1; font-size: 0.88rem; line-height: 1.45; color: var(--text-muted); }
.ai-route-form { display: flex; gap: 6px; }
/* The ask box grows with the question. `field-sizing: content` does this with
   no JS where it is supported; js/ai-route.js sizes it everywhere else. The max
   keeps a very long question from pushing the Send button off the panel.
   (Ported from NZ 2026-10-13: the box became a textarea and this app still
   styled only an input, so it showed as a tiny unstyled monospace field.) */
.ai-route-form { align-items: flex-end; }
.ai-route-form textarea { flex: 1; min-width: 0; background: var(--bg-tertiary);
  border: 1px solid transparent; border-radius: var(--radius); padding: 12px;
  color: var(--text-primary); font-size: 1rem; font-family: inherit; outline: none;
  resize: none; overflow-y: auto; line-height: 1.45;
  min-height: 46px; max-height: 8.2em; field-sizing: content; }
.ai-route-form textarea:focus { border-color: var(--accent); }
.ai-route-form button { flex: none; min-height: 46px; }
/* AI route — accommodation suggestions */
.ai-stay-town { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; font-weight: 600; font-size: 0.95rem;
  margin: 14px 0 6px; padding-top: 10px; border-top: 1px solid var(--bg-tertiary); color: var(--text-primary); }
.ai-stay { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  font-size: 0.95rem; padding: 5px 0; }
.ai-stay-tier { font-size: max(0.72rem, var(--text-floor, 0px)); font-weight: 700; padding: 1px 5px; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.03em; }
.ai-stay-lo { background: rgba(0,200,83,0.18); color: var(--accent); }
.ai-stay-hi { background: rgba(64,196,255,0.18); color: var(--info); }
.ai-stay-camp { background: rgba(210,105,30,0.2); color: #e0a060; }
.ai-stay-name { flex: 1; color: var(--text-secondary); overflow-wrap: anywhere; }
.ai-stay-book { flex: none; color: var(--accent); text-decoration: none; font-weight: 600; white-space: nowrap;
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 6px; }
.ai-stay-book:hover { text-decoration: underline; }
.ai-stay-est { flex: none; font-size: 0.85rem; color: var(--text-muted); white-space: nowrap; }

/* ===== Cancel intercept + renewal toast (retention, js/auth.js) ===== */
#cancel-intercept {
  position: fixed; inset: 0; z-index: 10002;
  background: rgba(0,0,0,0.6);
  align-items: center; justify-content: center; padding: 16px;
  /* display toggled 'none'/'flex' by Auth._openCancelIntercept */
}
#cancel-intercept .cancel-card { position: relative; width: min(400px, 92vw); text-align: center; }
#cancel-intercept h2 { margin: 0 0 14px; font-size: 1.05rem; color: var(--text-primary, #fff); }
#cancel-intercept-close {
  position: absolute; top: 8px; right: 12px;
  background: none; border: none; cursor: pointer;
  color: var(--text-muted, #999); font-size: 1.2rem; line-height: 1; padding: 4px;
}
#cancel-intercept-options { width: 100%; }
/* Seven reasons as wrapping pills (the js/leaving.js layout), not seven
   stacked full-width buttons: on a phone the stack pushed Continue below the
   fold. aria-pressed is the selected state (toggled in Auth._openCancelIntercept). */
.cancel-reasons { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-bottom: 10px; }
.cancel-option { padding: 8px 13px; font-size: 0.82rem; border-radius: 999px; }
.cancel-option[aria-pressed="true"] {
  background: var(--accent, #00c853); color: #00120a;
  border-color: var(--accent, #00c853); font-weight: 700;
}
#cancel-intercept-note {
  width: 100%; box-sizing: border-box; margin-bottom: 10px;
  background: var(--bg-tertiary, #1c2530); color: var(--text-primary, #fff);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;
  padding: 9px; font-size: 0.85rem; font-family: inherit; resize: vertical;
}
#cancel-intercept-slot { width: 100%; }
#cancel-intercept-slot p {
  margin: 0 0 14px; font-size: 0.82rem; line-height: 1.55;
  color: var(--text-muted, #bbb); text-align: left;
}
#renewal-toast {
  position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
  z-index: 2000; display: none; align-items: center; gap: 8px;
  background: #243342; color: #e8edf2; border: 1px solid rgba(255,255,255,0.1);
  border-radius: 8px; padding: 10px 14px; font-size: 0.8rem;
  max-width: min(420px, 90vw); box-shadow: 0 8px 30px rgba(0,0,0,0.5);
}
#renewal-toast a { color: var(--accent, #00c853); }
#renewal-toast button {
  background: none; border: none; cursor: pointer;
  color: var(--text-muted, #999); font-size: 1rem; padding: 0 2px;
}


/* ===== Arrival card (js/trip.js) — shown in BOTH ride views when a trip
   completes. Appended at the end of the sheet deliberately: AUS's style.css is
   five concatenated copies, so a rule inserted mid-file can be overridden by a
   later copy. ===== */

/* Full-screen nav: same pill language as #nav-turn, which it replaces, but
   with full-width buttons — this is the one thing in nav a rider taps wearing
   gloves, so both targets are 44 px+ tall. */
#nav-arrived {
  position: absolute;
  /* A THIRD of the way down, not at the top: up there it covered the close
     button, the ETA chip and the clock — the trip is over, but the rider still
     wants to read the screen (Josh, 2026-07-31). */
  top: 30%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 12;                  /* above the banner it replaces and the POI/camera pills */
  width: min(88vw, 460px);
  padding: 12px 14px;
  background: rgba(10,14,20,0.9);
  border: 1px solid rgba(0,200,83,0.35);
  border-radius: 16px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 6px 24px rgba(0,0,0,0.45);
}
.nav-arrived-head { display: flex; align-items: center; gap: 12px; }
#nav-arrived-icon {
  flex: 0 0 auto;
  width: 56px; height: 56px;
  border-radius: 14px;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.9rem;
  background: rgba(0,200,83,0.16); color: var(--accent, #00c853);
}
.nav-arrived-info { min-width: 0; }
.nav-arrived-title { font-size: 1.5rem; font-weight: 800; color: #fff; line-height: 1.1; }
#nav-arrived-name {
  font-size: 0.95rem; color: var(--text-secondary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Elevation profile of the route just ridden. */
.nav-arrived-elev { margin-top: 12px; }
.nav-arrived-elev canvas {
  display: block;
  width: 100%; height: 56px;
}
.nav-arrived-elev-cap {
  margin-top: 2px;
  font-size: max(0.7rem, var(--text-floor, 0px)); color: var(--text-secondary);
  letter-spacing: 0.02em;
}

/* Distance travelled · time moving · average over that moving time. */
.nav-arrived-stats {
  display: flex;
  gap: 8px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid rgba(255,255,255,0.08);
}
.nav-arrived-stat { flex: 1; min-width: 0; text-align: center; }
.nav-arrived-stat b {
  display: block;
  font-size: 1.15rem; font-weight: 800; color: #fff; line-height: 1.15;
  white-space: nowrap;
}
.nav-arrived-stat span {
  display: block;
  font-size: max(0.7rem, var(--text-floor, 0px)); color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: 0.04em;
}
#nav-arrived-note {
  margin-top: 10px;
  font-size: 0.82rem; color: var(--text-secondary);
}
#nav-arrived-note i { color: #ff5252; margin-right: 5px; }
.nav-arrived-actions { display: flex; gap: 8px; margin-top: 12px; }
.nav-arrived-btn {
  flex: 1;
  padding: 12px 10px;
  font-size: 1rem; font-weight: 700;
  color: #fff;
  background: rgba(255,255,255,0.1);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 12px;
  cursor: pointer;
}
.nav-arrived-btn.primary { background: var(--accent, #00c853); border-color: var(--accent, #00c853); color: #06210f; }

/* Speedo dash: the same card in the next-turn slot. */
.dash-arrived {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
  flex-shrink: 0;
  background: rgba(0, 200, 83, 0.1);
  border: 1px solid rgba(0, 200, 83, 0.3);
  border-radius: 10px;
  padding: 8px 12px;
}
.dash-arrived-head { display: flex; align-items: center; gap: 10px; }
.dash-arrived-icon {
  flex-shrink: 0;
  width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 8px;
  background: rgba(0, 200, 83, 0.15);
  color: #00c853;
  font-size: 1.3rem;
}
.dash-arrived-info { flex: 1; min-width: 0; }
.dash-arrived-title { font-size: 1.2rem; font-weight: 700; color: var(--text-primary, #fff); }
.dash-arrived-name {
  font-size: 0.8rem; color: var(--text-secondary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dash-arrived-elev { margin-top: 8px; }
.dash-arrived-elev canvas { display: block; width: 100%; height: 38px; }
.dash-arrived-elev-cap { margin-top: 1px; font-size: max(0.62rem, var(--text-floor, 0px)); color: var(--text-secondary); }

.dash-arrived-stats {
  display: flex;
  gap: 6px;
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.dash-arrived-stat { flex: 1; min-width: 0; text-align: center; }
.dash-arrived-stat b {
  display: block;
  font-size: 0.95rem; font-weight: 800; color: var(--text-primary, #fff);
  line-height: 1.15; white-space: nowrap;
}
.dash-arrived-stat span {
  display: block;
  font-size: max(0.62rem, var(--text-floor, 0px)); color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.dash-arrived-note { margin-top: 8px; font-size: max(0.75rem, var(--text-floor, 0px)); color: var(--text-secondary); }
.dash-arrived-note i { color: #ff5252; margin-right: 5px; }
.dash-arrived-actions { display: flex; gap: 8px; margin-top: 8px; }
.dash-arrived-btn {
  flex: 1;
  padding: 10px 8px;
  font-size: 0.9rem; font-weight: 700;
  color: #fff;
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 8px;
  cursor: pointer;
}
.dash-arrived-btn.primary { background: #00c853; border-color: #00c853; color: #06210f; }

/* Off-route notice in nav. Appended at the END on purpose: this stylesheet
   carries duplicated .radar-note blocks, so a rule placed beside one copy could
   be overridden by a later one. */
#nav-offline-note { top: 112px; z-index: 40; }
#nav-gps-note { z-index: 40; }
#nav-gps-note i { color: #ffab40; }

/* ===== Start button (js/route-start.js) ===== */
/* Centred on the MAP, not the viewport — on desktop the sidebar owns the left
   380px, so a plain 50% would sit visibly off to the left of the map area.
   --sidebar-width is 0 on mobile and when the sidebar is collapsed, so the one
   expression covers every case. */
#route-start-btn {
  position: fixed;
  /* ABOVE the whole bottom furniture, not beside it. The bottom strip is fully
     spoken for: pills bottom-left (2D/3D at 26px, basemap at 90px, 56px tall
     each, so the stack tops out at 146px) and the imagery credits bottom-right.
     A centred button at 26px was measured colliding with the credits at EVERY
     width from 769px to 1440px — it only cleared at 1600+, which is the one
     size it happened to be developed at. Esri's terms require their credit to
     stay visible, so the button moves, not the credits. 154px clears the pill
     stack with 8px of air and is far above the attribution. */
  bottom: 154px;
  left: calc(50% + var(--sidebar-width) / 2);
  transform: translateX(-50%);
  z-index: 600;          /* above the 2D/3D and basemap pills (500) */
  min-height: 56px;
  padding: 0 38px;
  border: none;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  cursor: pointer;
  display: flex; align-items: center; gap: 12px;
  /* Deliberately louder than any other map control: a green glow on top of the
     standard shadow, so it reads as THE next action rather than one more pill. */
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5), 0 0 0 4px rgba(0, 200, 83, 0.18);
  transition: background var(--transition), box-shadow var(--transition), transform var(--transition);
}
#route-start-btn:hover {
  background: var(--accent-hover);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.55), 0 0 0 7px rgba(0, 200, 83, 0.22);
}
#route-start-btn:active { transform: translateX(-50%) scale(0.97); }

/* Clear is CENTRED by default, because Start is phone-only: the rule further
   down hides #route-start-btn at >=769px, so on a desktop Clear is the only
   button on that baseline and an offset one would sit lopsided beside nothing.
   Right at the bottom edge and 30% smaller (Josh, 2026-08-11, twice: 154px
   hovered over the map, then 96px was still too high). At this size it is
   ~39px tall and ~100px wide, so a 12px baseline sits under the bottom-LEFT
   pill stack's centre gap at every desktop width.
   EU MEASUREMENT (2026-08-11, the standing "measure fixed controls" rule):
   there is NO imagery-credits element to clear — every map here is built with
   attributionControl:false and no attribution control is ever added
   (js/gl-map.js, js/nav.js, js/dashboard.js), so the .maplibregl-ctrl-attrib
   rule near the top of this file styles something that never renders. What IS
   down there is the PTZ pad (right:16px, bottom:44px, 134x220). Centred Clear
   spans y 12-51, so it only shares the pad's lowest 7px, and horizontally it
   clears the pad at every width ABOVE ~780px. In the 769-780px band with the
   sidebar OPEN the two corners overlap by up to ~5.5px — the old 96px/full-size
   button overlapped the same pad by ~28x56px there, so this is a large
   improvement rather than a new fault. Noted, not worked around: the fleet
   shares these numbers. */
#route-clear-btn {
  position: fixed;
  bottom: 12px;
  left: calc(50% + var(--sidebar-width) / 2);
  transform: translateX(-50%);
  z-index: 600;
  min-height: 39px;
  padding: 0 18px;
  border: 1.5px solid #ff5252;
  border-radius: 999px;
  background: rgba(20, 16, 16, 0.92);
  color: #ff8a80;
  font-size: max(0.7rem, var(--text-floor, 0px));
  font-weight: 800;
  letter-spacing: 1px;
  text-transform: uppercase;
  cursor: pointer;
  display: flex; align-items: center; gap: 7px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
  transition: background var(--transition), box-shadow var(--transition), transform var(--transition);
}
#route-clear-btn:hover {
  background: #ff5252;
  color: #fff;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.55), 0 0 0 6px rgba(255, 82, 82, 0.22);
}
#route-clear-btn:active { transform: translateX(-50%) scale(0.97); }
#route-clear-btn i { font-size: max(0.66rem, var(--text-floor, 0px)); }
#route-clear-btn.hidden { display: none; }
body.dashboard-active #route-clear-btn,
body.nav-active #route-clear-btn { display: none !important; }
/* Phone: centred 8px above Start (Start: centred, bottom 56px, 54px tall), so
   the pair reads as one stacked control group clear of the PTZ pad on the
   right. `left: 50%`, NOT the desktop calc: nothing zeroes --sidebar-width
   below 769px (only desktop body.sidebar-collapsed does), so the inherited
   calc sat the button 302px right of centre — measured fully OFF-SCREEN at
   384px. The on-map Clear had never been visible on a phone until 2026-08-11,
   found while lowering it.
   The pill shrinks with its desktop twin, but the BOTTOM stays 118px. USA
   tightened this to 102 "to match the smaller pill"; measured here, that
   collides: Start is bottom:56px and min-height:54px, so its TOP edge is at
   110px, and shrinking Clear moves Clear's top down, never its bottom. 118 is
   already the tightest value with air — 8px of it — and 102 puts Clear's
   bottom 8px INSIDE Start. Both buttons are visible together whenever a route
   is planned, so that is two overlapping tap targets. Deliberate EU
   divergence; the same 8px overlap should be fixed in USA. */
@media (max-width: 768px) {
  /* 106px keeps the documented 8px of air above Start (bottom 44, 54 tall, so
     its top edge is 98). That puts Clear's top at 145, and the height slider
     moves from 148 to 168 in the same pass so the gap is 23px rather than 3:
     the strip between the scale ruler and the slider is only 110px and the two
     buttons need 101 of it, so both had to give. */
  #route-clear-btn { left: 50%; bottom: 106px; transform: translateX(-50%); padding: 0 13px; font-size: max(0.63rem, var(--text-floor, 0px)); }
  #route-clear-btn:active { transform: translateX(-50%) scale(0.97); }
}
#route-start-btn i { font-size: 1.05rem; }
#route-start-btn.hidden { display: none; }
/* Full-screen overlays own the whole screen — the button goes with them.
   (Nav in particular: it IS nav, so offering to start it again is nonsense.) */
body.dashboard-active #route-start-btn,
body.nav-active #route-start-btn { display: none !important; }

/* The panel twin in Route Summary. Same job, so the same loud green.
   Taller than the .btn default: measured 33px on an S23, under the 44px tap
   target the rest of the map UI holds itself to, and this is the primary action
   on the panel. (The Google / GPX buttons below it are the same 33px — that is
   pre-existing and deliberately left alone.) */
.route-start-inline {
  min-height: 48px;
  font-weight: 800;
  letter-spacing: 1px;
  text-transform: uppercase;
  display: flex; align-items: center; justify-content: center; gap: 9px;
}
.route-start-inline.hidden { display: none; }

@media (max-width: 768px) {
  /* CENTRED on a phone (Josh, 2026-08-11): bottom-right sat straight on the
     PTZ pad — measured at 384px, the pad spans x 277-373 / bottom 55-195 and
     Start covered its lower half. The old reason for going right (a centred
     button hit the basemap pill under ~460px) died when the pills went
     icon-narrow (~90px wide now). `left: 50%`, not the desktop calc — nothing
     zeroes --sidebar-width below 769px. 56px still clears the full-width
     imagery credits (Esri's terms require their credit stays visible). */
  #route-start-btn {
    left: 50%;
    right: auto;
    /* 20px, level with the 2D/3D pill's baseline, NOT 56px.
       Josh, 2026-08-25: "The Start & clear route button cover the height slider
       on mobile. Move the start & clear button down a bit."
       At 56 the pair sat Start y 56..110 and Clear y 118..157, and the height
       slider is at 148..186 with a 210px-wide box from the left edge, so Clear
       lay ON the slider by up to 69x9px at 390-500px wide. MEASURED, by
       scripts/qa/map-furniture-overlap-test.js, which now fails if any two
       floating map controls touch at 360/390/412/500/768px.
       There is room to go down because there is nothing below: the old comment
       here said 56 "clears the full-width imagery credits", but this map is
       built with attributionControl:false (js/gl-map.js) and never draws one.
       The comment on #route-clear-btn's desktop rule already said so; the two
       contradicted each other and the wrong one was load-bearing.
       It stops at 44 and not lower because there IS one thing down there: the
       MapLibre scale ruler, bottom-right at bottom 12px and 26px tall. At
       bottom 20 the measured overlap was 1x18px at 381px wide, which is the
       narrowest screen this rule applies to and therefore the one to tune on. */
    bottom: 44px;
    transform: translateX(-50%);
    min-height: 54px;
    padding: 0 26px;
    font-size: 1.05rem;
  }
  #route-start-btn:active { transform: translateX(-50%) scale(0.97); }
}

/* Narrow phones: centred still clips the PTZ pad's left edge (on a 320px SE
   the pad spans x 213-309 and Start reaches 231). The pad tops out 195px off
   the bottom, so lift the WHOLE pair above it — 204 clears it with 9px of
   air, and Clear keeps its 62px step above Start. */
@media (max-width: 380px) {
  /* 214/276, up from 204/266: the height slider moved from 148 to 168 in the
     same pass, and at 204 Start's lower edge sat 2px inside it (measured at
     360px). 214 restores the 8px of air this block was tuned with. */
  #route-start-btn { bottom: 214px; }
  #route-clear-btn { bottom: 276px; }
  /* 125% root makes a row of four tab labels wider than a 320px SE, and
     the brand title wraps inside the 56px header. Stack the tabs (icon over
     word) and keep the title on one line. 0.7rem is still 14px — above the
     old 11.5px — so this is packing, not undoing the bump. */
  .tab-btn {
    flex-direction: column;
    gap: 2px;
    padding: 8px 2px;
    font-size: max(0.7rem, var(--text-floor, 0px));
  }
  .tab-btn i { font-size: 0.9rem; }
  #mobile-header h1 { font-size: 1rem; white-space: nowrap; }
}

/* Phones only (Josh, 2026-08-04). Start hands a planned route to turn-by-turn
   nav — something you do on the phone clipped to the bars, never at a desk. The
   gate is VIEWPORT WIDTH, not native-vs-web: the web app on a phone is exactly
   where this belongs, and 768/769 is already the phone breakpoint everywhere
   else in this file. Both go: the floating map button and its Route Summary
   twin are one feature with one label, so leaving one behind would just be the
   same button in a quieter place. Planning a route on a desktop is untouched —
   only the hand-off to nav disappears. */
@media (min-width: 769px) {
  #route-start-btn,
  .route-start-inline { display: none !important; }
}
/* No turn by turn on a desktop, at any window size (Josh, 2026-10-13: "users
   will never, or should never be offered or shown nav on desktop - only
   tablet & mobile"). Desktop is the app's own test, Utils.isDesktop: the
   primary pointer is not a finger. "not all and (pointer: coarse)" is that
   test exactly, so a narrow desktop window loses Nav and Start too, and a
   phone or tablet keeps them. js/nav.js and js/route-start.js hide the same
   buttons in script and refuse to open nav, for anything that gets past this. */
@media not all and (pointer: coarse) {
  #btn-nav,
  #mobile-nav,
  #route-start-btn,
  .route-start-inline { display: none !important; }
}

/* ===== Arrival clock (Utils.arrivalTime) ===== */
/* Quieter than the distance and dimmer than the remaining-time green: it is
   reference information, not what a rider glances down for at speed. Hidden
   when empty so the chip does not grow a blank line before the first ETA. */
#nav-eta-arrive { font-size: max(0.72rem, var(--text-floor, 0px)); font-weight: 600; color: rgba(255,255,255,0.6); }
#nav-eta-arrive:empty { display: none; }
@media (max-width: 768px) {
  /* Three short stats fit a phone fine; two columns would orphan Arrive onto
     a row of its own. Appended at EOF so it wins in AUS, whose style.css is
     five concatenated copies. */
  .route-overview { grid-template-columns: repeat(3, 1fr); gap: 6px; }
  .route-stat { padding: 8px 4px; }
}


/* ===== Guided search chips + elevation profile (onX port, 2026-08-03) ======
   APPENDED AT THE END ON PURPOSE. The AUS stylesheet is five concatenated
   copies of itself (tasks/lessons.md), so a rule written anywhere else can be
   silently overridden by a later duplicate. Last-in wins at equal specificity,
   and the end of the file is the only position that is last in all regions. */
/* Layers tab only (Josh, 2026-08-25: "remove waterfalls lookouts lakes huts
   buttons from routes"). The chips live ABOVE #tab-nav in the markup, so they
   were drawn on every tab — Route, Weather and Offline included, where they are
   noise on a panel the rider opened to do something else.

   Driven off the tab's own .active class, not a JS hook on the tab buttons:
   switchToRoute() and the onboarding tour both change tabs by setting those
   classes directly, so a handler-based version would leave the chips on screen
   down exactly the paths that matter. :has() is already shipped in this
   stylesheet (.filter-chip:has(input:checked)). */
#elev-canvas { background: #111a22; margin: 6px 0 10px; }

/* Road Closures is zoom-gated (Layers.ROAD_EVENT_MINZOOM) and ships ON, so the
   toggle can be lit while the map is empty. This says which. */
.layer-toggle .zoom-hint {
  margin-left: auto; font-size: max(.64rem, var(--text-floor, 0px)); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-muted, #9aa8a1); opacity: .8; white-space: nowrap;
}

/* Mute for the spoken directions. Lives in #nav-dock now; muted is a state a
   rider has to read at a glance from a moving bike, so it changes colour as
   well as the icon and the word. */
#nav-voice.muted { color: #ffab40; }
#nav-voice.muted i { color: #ffab40; }

/* ===== Pan / tilt / zoom pad (js/app.js) =====
   Bottom-right, because the button column owns the top-right and the basemap
   and 2D/3D pills own the bottom-left. Lifted clear of MapLibre's attribution:
   Esri's terms require their credit to stay readable, so nothing may sit on it. */
#map-ptz {
  position: fixed;
  right: 16px;
  bottom: 44px;
  z-index: 500;
  display: flex; flex-direction: column; gap: 6px;
  padding: 6px;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: var(--radius);
  box-shadow: 0 4px 16px rgba(0,0,0,0.35);
}
#map-ptz .ptz-pad {
  display: grid;
  grid-template-columns: repeat(3, 38px);
  gap: 4px;
}
/* Column-first, so each pair (in/out, tilt/flatten, left/right) is one column.
   Laid out across instead, the pairs split apart as soon as a phone drops the
   grid to two columns. */
#map-ptz .ptz-row {
  display: grid;
  grid-template-rows: repeat(2, 38px);
  grid-auto-flow: column;
  gap: 4px;
}
#map-ptz button {
  width: 38px; height: 38px;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-tertiary);
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text-primary);
  font-size: 0.95rem;
  cursor: pointer;
  touch-action: manipulation;
}
#map-ptz button:hover { border-color: var(--accent); color: var(--accent); }
#map-ptz button:active { transform: scale(0.94); }
#map-ptz .ptz-home { color: var(--accent); }

/* Hidden wherever a full-screen view has taken the map away, exactly like the
   other on-map furniture. */
body.dashboard-active #map-ptz,
body.nav-active #map-ptz { display: none; }

@media (max-width: 768px) {
  /* A phone gets the camera controls that gestures make hardest: tilt, turn and
     zoom. Panning by dragging a map is the one thing everybody already does, and
     a 3x3 pad costs a third of the screen width to duplicate it. */
  #map-ptz { bottom: 56px; right: 12px; padding: 5px; }
  #map-ptz .ptz-pad { display: none; }
  /* Row-first here: two columns, one pair per row. */
  #map-ptz .ptz-row {
    grid-template-rows: none;
    grid-template-columns: repeat(2, 40px);
    grid-auto-flow: row;
  }
  #map-ptz button { width: 40px; height: 40px; }
}


/* ===== Small on/off map buttons (Sat, 3D) =====
   Replaces the two-option pills. Lit = on; off is topo and flat, which is how
   the app opens, so an unlit pair is the default state rather than a missing
   choice. Sized to match the PTZ pad's cells so the map's furniture is one
   family. */
.map-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 38px;
  padding: 0 12px;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: 8px;
  color: var(--text-secondary);
  font-size: 0.82rem; font-weight: 700; letter-spacing: 0.5px;
  cursor: pointer;
  touch-action: manipulation;
  box-shadow: 0 4px 16px rgba(0,0,0,0.35);
}
.map-toggle:hover { color: var(--text-primary); border-color: var(--accent); }
.map-toggle[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #00120a;
}
/* The pills used to carry their own backgrounds and padding; the containers are
   now just anchors for one button each. */
#basemap-switch, #mode-3d-switch {
  background: none;
  border: none;
  box-shadow: none;
  padding: 0;
  display: flex;
}
/* Re-derived from the new size: one 38px button and no container padding, where
   the old pills were 56px tall. 20 + 38 + 8 of air = 66. */
#mode-3d-switch { bottom: 20px; }
#basemap-switch { bottom: 66px; }
/* The phone's own part (the round picture of the other map): not on a
   desktop. Shown in the phone's map chrome, end of file. */
.map-toggle-pic { display: none; }

/* ===== Granted access running out (js/auth.js _maybeGrantEndingNotice) =====
   Ported from AUS 2026-08-09. An invited or comped rider has a paid_until with
   no Stripe customer, which makes _paidActive true and so skips BOTH the trial
   nudge and the renewal toast. Until this banner they simply lost access one
   day with no warning. */
#grant-ending-note {
  position: fixed; bottom: 132px; left: 50%; transform: translateX(-50%);
  z-index: 2001; display: none; align-items: center; gap: 8px;
  background: #243342; color: #e8edf2; border: 1px solid rgba(255,255,255,0.12);
  border-radius: 8px; padding: 10px 14px; font-size: 0.8rem;
  max-width: min(440px, 92vw); box-shadow: 0 8px 30px rgba(0,0,0,0.5);
}
#grant-ending-note i { color: var(--accent, #00c853); flex: none; }
#grant-ending-note span { flex: 1 1 auto; }
#grant-ending-note a {
  color: var(--accent, #00c853); font-weight: 700; text-decoration: none; flex: none;
}
#grant-ending-note button {
  background: none; border: none; cursor: pointer; flex: none;
  color: var(--text-muted, #999); font-size: 1rem; padding: 0 2px; line-height: 1;
}
/* Last 48 hours only — an amber edge, not a red alarm. Nothing is broken and
   nobody is being charged; they are about to lose something they were given. */
#grant-ending-note.urgent {
  border-color: rgba(255, 167, 38, 0.55);
  background: #33291d;
}
#grant-ending-note.urgent i { color: #ffa726; }

/* "Our welcome email to X came back" (js/auth.js _showEmailBounceCard,
   2026-10-13). It waits for an answer. */
#email-bounce-note {
  position: fixed; bottom: 132px; left: 50%; transform: translateX(-50%);
  z-index: 2001; display: none; align-items: center; gap: 8px; flex-wrap: wrap;
  background: #243342; color: #e8edf2; border: 1px solid rgba(255,255,255,0.12);
  border-radius: 8px; padding: 10px 14px; font-size: 0.85rem;
  width: min(460px, 92vw); box-shadow: 0 8px 30px rgba(0,0,0,0.5);
}
#email-bounce-note i { color: var(--accent, #00c853); flex: none; }
#email-bounce-note span { flex: 1 1 80%; min-width: 0; }
#email-bounce-note input {
  flex: 1 1 200px; min-width: 0; background: rgba(0,0,0,0.28);
  border: 1px solid rgba(255,255,255,0.16); border-radius: 6px;
  padding: 8px 9px; color: #e8edf2; font-size: 16px;
}
#email-bounce-note input:focus { outline: none; border-color: var(--accent, #00c853); }
#email-bounce-note .btn { flex: none; }
#email-bounce-note button[aria-label="Not now"] {
  background: none; border: none; cursor: pointer; flex: none;
  color: var(--text-muted, #999); font-size: 1.1rem; padding: 0 2px; line-height: 1;
}
#email-bounce-note .email-bounce-msg {
  flex: 1 1 100%; color: #ffa726; font-style: normal; font-size: 0.8rem;
}

/* Dropdown for the map stack's vehicle selector (app.js setupVehicleButton).
   top/right are set inline from the button's rect; above the mobile header
   overlay (1001) so it survives every stacking context. */
#vehicle-menu {
  position: fixed;
  z-index: 1002;
  display: flex; flex-direction: column; gap: 2px;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: var(--radius);
  padding: 6px;
  box-shadow: 0 6px 24px rgba(0,0,0,0.45);
}
#vehicle-menu button {
  display: flex; align-items: center; gap: 10px;
  background: none; border: none; color: var(--text-primary);
  font-size: 0.9rem; padding: 9px 12px; border-radius: calc(var(--radius) - 2px);
  cursor: pointer; text-align: left;
}
#vehicle-menu button i { width: 20px; text-align: center; }
#vehicle-menu button[aria-pressed="true"] {
  background: var(--accent-dim); color: var(--accent);
}

/* Save sits under the campsite name, above everything else in the panel
   (2026-08-22). Its own row rather than a member of .info-actions, which is the
   button strip at the foot. Appended rather than placed beside .info-actions
   because some regions' style.css is several concatenated copies. */
.info-save-row { margin: -4px 0 12px; }

/* ===== Scale bar + route HUD (2026-08-22) ===== */

/* Bottom-RIGHT corner of the map (Josh, 2026-08-23), tucked under the PTZ pad.
   The left corner is already three deep in furniture — Topo/Sat, 2D/3D and the
   terrain slider — and the bar was landing on top of the 2D/3D button.

   Two things this block has to get right, both learned the hard way:
   (1) !important, because vendor/maplibre-gl/maplibre-gl.css is linked AFTER
       this file in index.html, so vendor rules win on load order at equal
       specificity. The old rules here carried none, so NONE of them applied and
       the bar sat wherever MapLibre put it.
   (2) No --sidebar-width offset. MapLibre's corner containers live inside #map,
       which already starts at the sidebar edge, so adding it offsets twice. */
/* bottom: 10px measured, not guessed — the bar renders 25px tall and the pad
   stops 44px up, so this leaves 9px of air between them. */
.maplibregl-ctrl-bottom-right { right: 0 !important; bottom: 10px !important; z-index: 400; }
/* Drawn as a RULER, not a label (Josh, 2026-08-23: "it's unclear how it's
   measuring that distance"). MapLibre sets the element's WIDTH to the distance
   the text names, so the two end ticks and the baseline between them are the
   whole point — a floating "50 mi" measures nothing you can see. The half-way
   tick is there because judging a span against one long bar is guesswork. */
.maplibregl-ctrl-bottom-right .maplibregl-ctrl-scale {
  position: relative;
  box-sizing: border-box;          /* the ticks are borders: keep them INSIDE
                                      the width, or the ruler over-reads */
  height: 26px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  /* The label sits at the TOP, clear of the baseline: text resting on the
     ruler line hid the half-way tick behind its own letters. */
  padding: 2px 4px 0;
  background: rgba(16, 18, 21, 0.55);
  border: 2px solid rgba(255, 255, 255, 0.92);
  border-top: none;                /* end ticks + baseline, open at the top */
  border-radius: 0 0 3px 3px;
  color: #fff;
  font-size: max(0.63rem, var(--text-floor, 0px));
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  /* CENTRED under #map-ptz, not right-aligned to it (Josh, 2026-09-20:
     "center the ruler to the zoom/tilt etc button box"). #map-ptz is 136px
     wide at right:16px (measured in the live app, not read off the CSS), so
     its centre sits 84px in from the right edge. The margin puts the ruler's
     RIGHT edge there and the translate slides it back by half its own width,
     which is the point: MapLibre resets that width on every zoom, so a fixed
     margin would only line up at one zoom level. */
  margin: 0 84px 0 0 !important;
  transform: translateX(50%);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
}
.maplibregl-ctrl-bottom-right .maplibregl-ctrl-scale::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 2px;
  height: 7px;
  margin-left: -1px;
  background: rgba(255, 255, 255, 0.92);
}

/* Distance, time and arrival, on the map, while a route is up. The route panel
   has the same numbers and is a sidebar the rider closes to look at the map. */
#route-hud {
  position: fixed;
  top: 16px;
  left: calc(var(--sidebar-width, 0px) + 16px);
  /* Bounded on the RIGHT too (2026-09-02). A 2,300 km route printed
     "1429.2mi", "28h 30m" and "Wed 2:20 PM"; the row was only ever bounded on
     the left, grew past #map-controls (fixed top-right, the same z-index 500,
     later in the DOM, so it paints on top) and the Arrive tile sat under the
     Nav button. The controls column is one button wide, and that is its widest
     label: "VEHICLE", or Italian "CANCELLA" in EU, measure 96-99px in Roboto
     and SF (icon 20 + gap 6 + label 48-59 + padding 20 + border 2), which a
     360px S23 rendered at 116px, not the 100px first guessed: the wrapped
     Arrive tile still ran 8px under the column. So the width is MEASURED:
     route-hud.js reads #map-controls when the tiles go up and sets
     --map-controls-w on the HUD; 116px is only the fallback. Plus the column's
     own 16px margin, plus 8px of air. fit-content keeps the box as narrow as
     its tiles until they need the room: with left and right both set, that
     room is the space between them. */
  right: calc(16px + var(--map-controls-w, 116px) + 8px);
  width: fit-content;
  z-index: 500;
  display: flex;
  flex-wrap: wrap;                 /* whole tiles move to a second row; a value never breaks mid-string */
  gap: 6px;
  pointer-events: none;            /* never steal a tap meant for the map */
}
#route-hud.hidden { display: none !important; }
/* A wrapped tile goes to the RIGHT end of its row, not the left. The elevation
   chip (#map-readouts) and the 3D compass sit under the tiles at the left,
   stepped clear of ONE row, and a second row at the left would land on them:
   the 2026-09-01 overlap again. On one row this costs nothing, because a
   fit-content box has no spare width for an auto margin to take. */
.route-hud-tile:last-child { margin-left: auto; }
.route-hud-tile {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 64px;
  padding: 5px 9px;
  background: var(--bg-secondary, #1c1f24);
  border: 1px solid var(--bg-tertiary, #2a2f36);
  border-radius: var(--radius-sm, 6px);
  box-shadow: var(--shadow, 0 2px 10px rgba(0, 0, 0, 0.4));
}
.route-hud-label {
  /* A caption never wraps and never widens a tile past its cap: the third tile's
     caption is a PLACE NAME, and "Coffs Harbour" must cost the same room as
     "Arrive". It ellipsises instead. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: max(0.58rem, var(--text-floor, 0px));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted, #9aa8a1);
}
.route-hud-value { font-size: 0.92rem; font-weight: 700; color: var(--text-primary, #fff); white-space: nowrap; }

/* The 3D compass is added to the map's own top-left corner, which is where the
   tiles sit. Only while they are up. */
body.route-hud-on .maplibregl-ctrl-top-left { margin-top: 52px; }

/* Sidebar collapsed on desktop: #sidebar-expand is fixed at top:12px left:12px,
   38px square, z-index 1001 — parked straight on top of the first tile (Josh,
   2026-08-25). 62px is the same step #info-panel already takes to clear that
   same button. Desktop only: on a phone the tiles are placed by the rule below
   and body.sidebar-collapsed must not drag them sideways. */
@media (min-width: 769px) {
  body.sidebar-collapsed #route-hud { left: 62px; }
}

@media (max-width: 768px) {
  /* On a phone the sidebar is an overlay, so no offset — and the tiles shrink
     rather than crowd the top of a small map. */
  /* BELOW the mobile header, not under it. #mobile-header is position:fixed at
     z-index 1001 and the HUD is 500, so top:10px parked the tiles behind the
     title bar: they had never once been visible on a phone since they shipped
     (measured on an S23, 2026-08-25 — header 0-56px, tiles 10-55px). A rider
     who closes the sidebar to look at the line is exactly who they are for.
     right: #map-controls sits at right:12px on a phone; the same 100px column
     and 8px of air as the desktop rule above. */
  #route-hud { left: 10px; top: calc(var(--mobile-header-height, 56px) + 8px); right: calc(12px + var(--map-controls-w, 116px) + 8px); gap: 4px; }
  body.route-hud-on .maplibregl-ctrl-top-left { margin-top: calc(var(--mobile-header-height, 56px) + 58px); }
  /* 5px sides, not 7: on a 360px phone the three tiles of a SHORT route need 221px
     and the space left of the button column is 213px, so at 7px even "74.6mi, 2h 12m,
     18:26" wrapped (measured 2026-09-02). Long routes still wrap, which is what the
     right-aligned second row is for. */
  .route-hud-tile { min-width: 0; padding: 4px 5px; }
  /* Three tiles, one row. The third tile's caption is the next stop's NAME, so
     its width is whatever the rider typed: capped here and ellipsised, because
     one long town name must not push a tile onto a second row. 86px is what is
     left after the other two on a 390 (measured 2026-09-11: distance 81px,
     driving 64px, 4px gaps, 244px beside the button column). */
  .route-hud-tile-next { max-width: 86px; }
  /* A place name is longer than a word like "Arrive", so this caption gets its
     own tighter type: at the shared 0.58rem/0.06em even "Cervantes" ellipsised
     inside the cap. Measured on the real app, not a harness — the app's own
     font is wider than a bare stylesheet renders. */
  .route-hud-tile-next .route-hud-label { font-size: max(0.52rem, var(--text-floor, 0px)); letter-spacing: 0.01em; }
  .route-hud-value { font-size: 0.82rem; }
  /* Matches the phone PTZ pad, which moves to right: 12px, bottom: 56px. */
  .maplibregl-ctrl-bottom-right { bottom: 12px !important; }
  /* Same centring on a phone. Here #map-ptz drops the 3x3 pad and is 96px wide
     (2 x 40px + 4px gap + 2 x 5px padding + 2 x 1px border) at right:12px, so
     its centre is 60px in. */
  .maplibregl-ctrl-bottom-right .maplibregl-ctrl-scale { margin: 0 60px 0 0 !important; transform: translateX(50%); }
}

/* ===== Trial first-charge banner (js/auth.js _maybeTrialChargeNotice) =====
   Appended at EOF so it cannot be overridden by a later rule.

   Shares #grant-ending-note's slot (132px) rather than claiming a new one: the
   two can never fire for the same rider. This one requires a Stripe customer id
   and that one returns null the moment there is one. */
#charge-note {
  position: fixed; bottom: 132px; left: 50%; transform: translateX(-50%);
  z-index: 2001; display: none; align-items: center; gap: 8px;
  background: #243342; color: #e8edf2; border: 1px solid rgba(255,255,255,0.12);
  border-radius: 8px; padding: 10px 14px; font-size: 0.8rem;
  max-width: min(460px, 92vw); box-shadow: 0 8px 30px rgba(0,0,0,0.5);
}
#charge-note i { color: var(--accent, #00c853); flex: none; }
#charge-note span { flex: 1 1 auto; }
#charge-note button {
  background: none; border: none; cursor: pointer; flex: none;
  color: var(--text-muted, #999); font-size: 1rem; padding: 0 2px; line-height: 1;
}
/* Last 48 hours. Amber, not red: nothing is broken — but money is about to move,
   which is why this one names the amount and the grant banner does not. */
#charge-note.urgent {
  border-color: rgba(255, 167, 38, 0.55);
  background: #33291d;
}
#charge-note.urgent i { color: #ffa726; }

/* Offline map source: Auto / Offline only / Online only. Replaced the "Use
   offline maps" checkbox on 2026-08-25 — the app now switches itself when tiles
   stop arriving, and this is the override. */
/* "Advanced" on the Offline tab: the map source picker, folded away. */
.offline-advanced { margin: 0 0 8px; }
.offline-advanced > summary { font-size: max(0.72rem, var(--text-floor, 0px)); color: var(--text-muted); cursor: pointer; padding: 4px 0; }
.offline-advanced[open] > summary { margin-bottom: 4px; }
.offline-mode-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 8px 0; min-height: 44px;
}
.offline-mode-label { font-size: 0.85rem; color: var(--text-primary); }
.offline-mode-select {
  background: var(--bg-tertiary); color: var(--text-primary);
  border: 1px solid var(--bg-tertiary); border-radius: var(--radius);
  padding: 8px 10px; min-height: 40px; font-size: 0.82rem; font-family: inherit;
  cursor: pointer;
}
.offline-mode-select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
#offline-maps-note .offline-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; vertical-align: 1px;
}

/* `hidden` must beat the flex display above, or a toggle js/app.js hides for a
   bicycle or a rig stays on screen. (Found 2026-08-28: the bicycle hiding had
   never worked for this reason.) */
.layer-toggle[hidden] { display: none; }

/* ===== Live map readouts (js/elevation-hud.js) =====
   Top-left of the map, on phones only (the module never starts on a desktop). NOT an empty corner, which is what the first cut of this
   comment claimed: #route-hud (the distance / time / arrival tiles) is fixed at
   exactly this spot and is up whenever a route is, which is most of the time
   the chips have anything to say. The port review caught the two chips drawing
   straight over the ETA tile (2026-09-01, found in AUS and EU, inherited from
   here). The rules after the mobile block step the column BELOW the tiles the
   same way the 3D compass already does (body.route-hud-on
   .maplibregl-ctrl-top-left), and clear #sidebar-expand the same way
   #route-hud and #info-panel already do. #map-controls owns top-right, the
   basemap and 2D/3D pills own bottom-left, MapLibre's attribution and scale own
   bottom-right. Deliberately quiet — it is a fact you glance at, not a control
   you press, so it takes no border and no shadow that would make it look
   tappable.

   The POSITION lives on the wrapper, not on the chip, so a second chip (the
   daylight countdown lived here until Josh cut it, 2026-09-02) stacks under it
   with a gap instead of hardcoding the first one's height. */
#map-readouts {
  position: fixed;
  top: 16px;
  left: calc(var(--sidebar-width) + 16px);
  z-index: 500;
  display: flex;
  flex-direction: column;
  align-items: flex-start;   /* each chip is only as wide as its own text */
  gap: 6px;
  pointer-events: none;      /* never eat a map drag that starts here */
}
.map-readout {
  display: flex; align-items: center; gap: 7px;
  padding: 6px 11px;
  border-radius: var(--radius);
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  box-shadow: var(--shadow);
  color: var(--text-primary);
  font-size: 0.95rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;   /* the number must not jiggle as it changes */
  user-select: none;
}
.map-readout i { color: var(--accent); font-size: 0.85rem; }
.map-readout[hidden] { display: none !important; }


/* The dashboard and nav both carry their own altitude readout, and both take
   the whole screen. Same rule the basemap and 2D/3D pills follow. */
body.dashboard-active #map-readouts,
body.nav-active #map-readouts { display: none !important; }

/* While a route is up, #route-hud owns top:16px in this corner: a 36px row of
   tiles whose bottom edge is 52px (the same 52px body.route-hud-on gives the 3D
   compass). Step the column under it with 8px of air. If the tiles ever change
   height, this and the compass rule move together. */
body.route-hud-on #map-readouts { top: 60px; }

/* Sidebar collapsed on desktop: #sidebar-expand is fixed at top:12px left:12px,
   38px square, z-index 1001, and --sidebar-width goes to 0 — which parks the
   chips under that button. 62px is the step #info-panel and #route-hud already
   take to clear it (Josh, 2026-08-25). Desktop only, like theirs. */
@media (min-width: 769px) {
  body.sidebar-collapsed #map-readouts { left: 62px; }
}

@media (max-width: 768px) {
  /* The map starts below the mobile header, and the sidebar is an overlay
     rather than a column, so the left offset collapses to the plain margin. */
  #map-readouts {
    top: calc(var(--mobile-header-height) + 12px);
    left: 12px;
  }
  /* #route-hud sits at header + 8px on a phone and its tiles are shorter; its
     bottom edge is header + 58px, which is the figure the compass rule uses. */
  body.route-hud-on #map-readouts { top: calc(var(--mobile-header-height) + 66px); }
}

/* The signup error sits between the fields and the button now, and the bad box
   gets a red edge. Live telemetry, 21 days to 2026-09-04: 136 no_email, 26
   short_password, 12 bad_email, 10 no_password. Those messages exist to be
   read, and they were rendering below the button and below the microcopy. */
.signup-box .field-invalid {
  border-color: #ff5252 !important;
  box-shadow: 0 0 0 2px rgba(255, 82, 82, 0.18);
}

/* ===== Nav destination card (js/nav-destination.js) =====
   Nav opened with no route and offered nothing but a moving dot. This is the
   "where to?" box, over the nav map, top-centre so it clears the close button
   and the clock. Sized for gloved thumbs on a bar mount, not for a desk. */
#nav-dest {
  position: absolute;
  top: 74px;
  left: 12px;
  right: 12px;
  z-index: 1200;
  background: rgba(16, 24, 33, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 14px;
  padding: 10px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
  max-width: 560px;
  margin: 0 auto;
}
#nav-dest.hidden { display: none; }
.nav-dest-row { display: flex; align-items: center; gap: 8px; }
.nav-dest-icon { color: #8fa3b8; font-size: 16px; flex: 0 0 auto; }
#nav-dest-input {
  flex: 1 1 auto;
  min-width: 0;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 10px;
  color: #fff;
  font-size: 17px;              /* 17px: iOS zooms the page on anything smaller */
  padding: 11px 12px;
}
#nav-dest-input::placeholder { color: #8fa3b8; }
#nav-dest-close {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: #8fa3b8;
  font-size: 18px;
  padding: 8px 10px;
  cursor: pointer;
}
#nav-dest-list { margin-top: 6px; max-height: 40vh; overflow-y: auto; }
.nav-dest-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: none;
  border: none;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  color: #e8eef5;
  font-size: 16px;
  font-family: inherit;
  text-align: left;
  padding: 13px 4px;            /* 44px total: a gloved thumb target */
  cursor: pointer;
}
.nav-dest-item:first-child { border-top: none; }
.nav-dest-item i { color: #00c853; font-size: 14px; flex: 0 0 auto; }
.nav-dest-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#nav-dest-note { margin: 6px 2px 0; color: #8fa3b8; font-size: max(13px, var(--text-floor, 0px)); line-height: 1.35; }
#nav-dest-list.hidden, #nav-dest-place.hidden, .pc-line.hidden { display: none; }
#nav-dest-place { margin-top: 8px; max-height: 60vh; overflow-y: auto; color: #e8eef5; }
/* A searched place's card (js/place-card.js): nav's "Where to?" box and the
   map search's info panel. Which one, before Go / Start. */
.pc-head { display: flex; align-items: flex-start; gap: 12px; padding: 2px 2px 10px; }
.pc-head > i { color: #00c853; font-size: 20px; margin-top: 3px; flex: 0 0 auto; }
.pc-name { font-size: 20px; font-weight: 700; line-height: 1.2; }
.pc-sub { color: var(--text-muted, #8fa3b8); font-size: 14px; margin-top: 3px; }
.pc-line { display: flex; gap: 10px; align-items: flex-start; font-size: 15px; line-height: 1.35; padding: 8px 2px; border-top: 1px solid rgba(255, 255, 255, 0.08); }
.pc-line i { color: var(--text-muted, #8fa3b8); width: 16px; text-align: center; margin-top: 2px; flex: 0 0 auto; }
.pc-fuel:empty, .pc-contact:empty { display: none; }
.pc-fuel, .pc-contact { border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 8px; margin-top: 2px; }
.pc-actions { display: flex; gap: 8px; margin: 6px 0 10px; }
.pc-actions button { font-family: inherit; font-size: 17px; font-weight: 600; border-radius: 12px; padding: 14px 12px; cursor: pointer; border: none; }
.pc-back { flex: 0 0 auto; background: rgba(255, 255, 255, 0.1); color: #e8eef5; }
.pc-go { flex: 1 1 auto; background: #00c853; color: #04150b; }
/* The map's card keeps the app's own route actions under Start. */
.pc-more { display: flex; flex-wrap: wrap; gap: 6px; margin: -2px 0 10px; }
body.place-card-open #search-go-btn { display: none; }
#nav-dest-note.hidden { display: none; }

/* ===== Nav pill buttons (Topo|Sat, 2D|3D) =====
   These had NO base rule: the only styling was geometry inside
   #nav-mode-switch / #nav-base-switch, so every one of them rendered as a raw
   Chrome button — grey #efefef, black Arial, native border — sitting on top of
   the dark pill it belongs in, and spilling past it (Josh, phone, 2026-09-04).
   The pill also had no way to show which mode you were in: the JS has toggled
   .active and aria-pressed since it shipped and no rule ever read them. */
.mode-switch-btn {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  border-radius: 999px;
  color: #cfd8e3;
  font-family: inherit;
  font-weight: 700;
  line-height: 1;
  text-transform: uppercase;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s ease, color 0.15s ease;
}
.mode-switch-btn:hover { color: #fff; }
/* The mode you are IN, not the one the press would take you to — the button's
   own text already says the current mode (see updateNavBaseToggle). */
.mode-switch-btn.active {
  background: rgba(0, 200, 83, 0.9);
  color: #06210f;
}

/* ===== The hamburger says where it goes (2026-09-07) =====
   A bare bars icon told a rider nothing about what the tap does. The button
   now stacks the icon over one small word: Menu while the map is up, Map while
   the menu is (app.js syncSidebarToggleLabel writes it). The column keeps the
   pair inside the 56px header, and the .58rem word still fits a 360px phone
   beside the nowrap title. Two ids so it beats #mobile-header button above. */
#mobile-search #sidebar-toggle {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  flex: none;
  width: 48px; height: 48px;
  border-radius: 24px;
  background: none; border: none;
  color: var(--text-primary);
  font-size: 1.05rem;
  padding: 0;
  line-height: 1;
  cursor: pointer;
}
#sidebar-toggle .map-btn-label {
  font-size: max(.58rem, var(--text-floor, 0px));
  letter-spacing: .5px;
  text-transform: uppercase;
}


/* Every POI row in the layer list wears the badge its dots wear on the map
   (Layers.setupToggles hangs a MarkerIcons.swatch canvas on the row,
   2026-09-07). The row is a flex line with its own 10px gap, which already
   keeps the icon clear of a LIVE / Trial / New tag beside it. */
.layer-toggle .layer-icon {
  margin-left: auto; flex-shrink: 0;
  width: 18px; height: 18px; border-radius: 50%;
}

/* Drag-to-reorder stops (2026-09-08, backtrack-audit fix). Pointer Events,
   not HTML5 drag-and-drop — DnD is mouse-only and this app is mobile-first. */
.route-point.stop-draggable .route-waypoint-icon { cursor: grab; touch-action: none; padding: 4px; margin: -4px; }
.route-point.stop-draggable.dragging { opacity: 0.55; background: var(--bg-secondary); }

/* ===== Plan with AI: the panel's own rules =====
   Ported with js/ai-route.js on 2026-09-12. The planner renders these
   classes and this stylesheet had never seen them, so the stay chips and
   the night badges came out unstyled. */
/* The day's ride to this night's town, measured along the drawn route. The km
   on a stay ROW is the campground's own offset from the town, which is a
   different number and reads as the leg if the leg is not also shown
   (Josh, 2026-09-11: "are they correct? from night one to night 2 etc?"). */
.ai-stay-leg {
  margin-left: auto;
  flex: none;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-muted, #97a3b2);
  font-variant-numeric: tabular-nums;
}

/* The night number leads, so the eye reads down the trip in order. */
/* The date of a night, beside its town. A date the rider gave is in the accent. */
.ai-stay-date { flex: none; font-size: 0.78rem; color: var(--text-muted, #97a3b2); }
.ai-stay-date.ai-stay-fixed { color: var(--accent, #5fd08a); font-weight: 700; }
.ai-stay-night { flex: none; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.03em;
  text-transform: uppercase; padding: 3px 8px; border-radius: 999px;
  background: var(--accent-dim); color: var(--text-primary); }

/* What sort of place each night — a row of taps over the stay list, so a rider
   can swap free camps for holiday parks without planning the trip again
   (Josh, 2026-09-11). Sized for a thumb on a phone, not a mouse. */
.ai-stay-chip {
  background: var(--bg-elevated, #1f2630);
  border: 1px solid var(--border, #2a3340);
  color: var(--text, #e8edf3);
  border-radius: 999px;
  padding: 7px 13px;
  font-size: 0.84rem;
  line-height: 1.1;
  min-height: 34px;
  cursor: pointer;
  white-space: nowrap;
}

.ai-stay-chip:hover { border-color: var(--accent, #4caf50); }

.ai-stay-chip.on {
  background: var(--accent, #4caf50);
  border-color: var(--accent, #4caf50);
  color: #0d1117;
  font-weight: 600;
}

/* ===== AI mode: the planner takes the sidebar over =====
 *
 * WHY A TAKEOVER AND NOT A FIFTH TAB. What comes back from the planner is not
 * a route, it is a TRIP: three options, four nights, night stops, places to
 * stay, booking links. That was being rendered inside a form built for "A to
 * B", in a 379px column the rider had to scroll 2.8 screens to read. A new tab
 * would be a permanent cost in the nav to solve a temporary state, and it
 * would split "plan a trip" across two places, which is worse for the riders
 * this is for. So: while you are planning with AI, the planner owns the panel,
 * and one obvious control gives it back.
 *
 * Everything stays in the DOM and is only hidden, so anything that writes into
 * the route inputs while AI mode is on (the tour, the map menu's "Directions
 * from here") still finds its fields. AiRoute.exit() is called on those paths
 * so the rider actually sees the result. */
/* The planner owns the panel, EXCEPT the thing it just produced. Hiding
   #route-summary here meant planning an AI trip hid the trip: distance, climb,
   the warnings and the save button all vanished behind the chat (Josh,
   2026-09-11: "once an AI route is planned that's the priority for the
   sidebar"). The summary stays, and once it is up it goes FIRST, because the
   answer matters more than the question that produced it. */
body.ai-mode #tab-route > .sidebar-section:not(#ai-route):not(#route-summary) { display: none; }

body.ai-mode #tab-route { display: flex; flex-direction: column; }

body.ai-mode #route-summary { order: -1; }

body.ai-mode #ai-route { order: 0; }

body.ai-mode #ai-route { border-bottom: 0; }

/* The exit is a full-width control, not an icon: it has to be findable by
   someone who does not hunt for affordances. 44px tall for the same reason. */
/* Saving is the whole point: a saved trip is what brings a rider back, and
   only 4 of 21 cancelled riders ever saved one. So this is not a small button
   in a list, it is the loudest thing on the panel once a route exists. */
.ai-save-offer { display: none; align-items: center; justify-content: center; gap: 10px;
  width: 100%; min-height: 52px; margin: 14px 0 0; padding: 0 16px;
  background: var(--accent); color: #04150c; border: 0; border-radius: var(--radius);
  font-family: inherit; font-size: 1.02rem; font-weight: 700; cursor: pointer; }

.ai-save-offer:hover { filter: brightness(1.06); }

.ai-save-offer.on { display: flex; }

.ai-save-note { margin: 6px 0 0; font-size: 0.85rem; color: var(--text-muted); text-align: center; display: none; }

.ai-save-note.on { display: block; }
.ai-edit-row { margin-top: 10px; }
.ai-edit-row[hidden] { display: none; }
.ai-route-edit { display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 48px; padding: 0 14px;
  background: var(--bg-tertiary); color: var(--text-primary);
  border: 1px solid var(--accent); border-radius: var(--radius);
  font-family: inherit; font-size: 0.98rem; font-weight: 600; cursor: pointer; }
.ai-route-edit:hover { background: var(--accent-dim); }
.ai-edit-hint { margin: 6px 0 0; font-size: 0.85rem; color: var(--text-muted); text-align: center; }

.ai-route-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }

.ai-route-exit, .ai-route-map { display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 44px; padding: 0 14px;
  background: var(--bg-tertiary); color: var(--text-secondary);
  border: 1px solid var(--bg-tertiary); border-radius: var(--radius);
  font-family: inherit; font-size: 0.95rem; cursor: pointer; }

.ai-route-exit:hover, .ai-route-map:hover { border-color: var(--accent); color: var(--text-primary); }

/* Looking at the map is the likelier of the two, so it leads and it is the one
   that looks like a button worth pressing. */
.ai-route-map { background: var(--accent-dim); color: var(--text-primary); border-color: transparent; font-weight: 600; }

/* On a desktop the map is already beside the panel: peeking at it is pointless. */
.ai-route-map { display: none; }

/* The emailed trip plan (ported from NZ 2026-09-12). */
.ai-plan-row { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--bg-tertiary); }
.ai-plan-date-label { display: block; font-size: 0.92rem; font-weight: 600; margin-bottom: 6px; color: var(--text-primary); }
.ai-plan-date {
  width: 100%; min-height: 46px; padding: 0 12px; margin-bottom: 10px;
  background: var(--bg-tertiary); border: 1px solid transparent; border-radius: var(--radius);
  color: var(--text-primary); font-family: inherit; font-size: 1rem;
}
.ai-plan-date:focus { border-color: var(--accent); outline: none; }
.ai-plan-email {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; min-height: 56px; padding: 0 16px;
  background: var(--accent); color: #0d1117; border: 0; border-radius: var(--radius);
  font-family: inherit; font-size: 1.05rem; font-weight: 700; cursor: pointer;
  box-shadow: 0 2px 10px rgba(0, 200, 83, 0.3);
}
.ai-plan-email:hover { filter: brightness(1.06); }
.ai-plan-email:disabled { opacity: 0.6; cursor: default; box-shadow: none; }
.ai-plan-hint { margin: 8px 0 0; font-size: 0.85rem; line-height: 1.45; color: var(--text-muted); }
/* The trip's days and nights, said before the PDF is made (js/trip-plan.js refreshDays). */
.trip-plan-days { margin: 0 0 8px; font-size: 0.9rem; line-height: 1.45; color: var(--text); }
.trip-plan-days:empty { display: none; }
.route-fuel { margin: 4px 0 10px; font-size: 0.9rem; line-height: 1.45; color: var(--text); }
.route-fuel:empty { display: none; }
.route-fuel .fa-gas-pump { color: var(--text-muted); margin-right: 2px; }
.route-fuel-set, .route-fuel-change { background: none; border: 0; padding: 0; font: inherit; color: var(--accent, #4caf50); cursor: pointer; text-decoration: underline; }
.route-fuel-set { text-align: left; }
.route-fuel-new { display: inline-block; font-size: max(.68rem, var(--text-floor, 0px)); font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: #0b1a10; background: var(--accent, #00c853); border-radius: 999px; padding: 1px 7px; margin-right: 4px; vertical-align: 1px; }
.ai-plan-note { margin: 8px 0 0; font-size: 0.9rem; color: var(--text-muted); display: none; text-align: center; }
.ai-plan-note.on { display: block; }
.ai-plan-note.bad { color: var(--warning, #ffab40); }
/* Download PDF and Share, under the big email button (js/trip-plan.js). */
.trip-plan-more { display: flex; gap: 8px; justify-content: center; margin-top: 8px; }
.trip-plan-more .btn[hidden] { display: none; }

/* The planner panel's own controls: the phone map peek and the sticky way
   out. The 2026-09-12 planner port brought the JS and the rest of the CSS and
   left these behind with the markup. */
/* On a desktop the map is already beside the panel: peeking at it is pointless. */
.ai-route-map { display: none; }
@media (max-width: 768px) { .ai-route-map { display: flex; } }

/* On a phone the planner IS the screen (.sidebar is width:100% under 768px), so
   the way out has to be reachable without scrolling to the bottom of a long
   answer. Pinned to the foot of the panel, above the home indicator. */
@media (max-width: 768px) {
  body.ai-mode .ai-route-actions {
    position: sticky; bottom: 0; z-index: 2;
    padding: 10px 0 calc(10px + env(safe-area-inset-bottom, 0px));
    margin-top: 14px;
    background: linear-gradient(to top, var(--bg-secondary) 72%, transparent);
  }
}

/* ===== The planner panel, brought level with AUS and NZ (2026-09-12) =====
   The 12 September port brought this region's planner JS and left the
   markup and these rules behind: the JS sizes a textarea that was still an
   input, and the thread was a 260px scroller inside a sidebar that scrolls,
   which is the worst thing to put in front of the riders this feature is
   for. Appended, because this stylesheet is several concatenated copies of
   itself and an earlier copy would otherwise win. */

/* ===== Nav screen: one grid, every readout in its own cell (2026-09-14) =====
   Until now each pill on the nav screen was placed by hand with absolute
   offsets spread over three media queries, and nothing tied a pill's slot to
   its neighbour's real height. When the ETA chip grew an arrival line the
   turn banner ended up 18px under the clock on an S23, the 2D/3D pill touched
   the elevation chip, and the voice pill never got the phone rule the other
   two pills had. Josh, 2026-09-14: nothing on the nav screen may cover
   anything else; there is space between every piece of information.

   The overlay is now a CSS grid and each readout is a grid item, so two of
   them cannot share a pixel by construction. The map stays absolute behind
   them. Every earlier top/left/right/bottom/transform for these ids is reset
   here and is dead. Narrow screens stack the three pills above the elevation
   chip on the right; from 600px wide they sit in a row along the bottom edge
   and the clock moves beside the ETA chip, which is what keeps a phone in
   landscape (354px tall) from running out of height. */
#nav-overlay:not(.hidden) {
  display: grid !important;   /* nav.js sets inline display:block on open */
  grid-template-columns: auto 1fr auto;
  /* 1 close/clock/eta · 2 turn · 3 battery · 4 camera · 5 worth-a-stop
     6 off-route · 7 radar · 8 spare · 9 voice · 10 basemap · 11 2D/3D
     · 12 speed/elev
     Row 8 is the elastic one: it takes the slack, so nothing may be placed in
     it — a control there costs a whole row and pushed the speed and elevation
     off the bottom of a 640px phone (2026-09-18, the manual zoom buttons).
     THE RIGHT STACK HAS NO SPARE ROW EITHER, at 360x640 with every alert lit.
     So the zoom group sits on the LEFT, flat, directly above the speed readout
     (Josh, 2026-09-18). It takes row 11 in column 1, which the 2D/3D pill
     already holds on the right, so the band grows by the few px that a 44px
     button beats a 40px pill and nothing is pushed off. Stacked upright it
     would not fit: 144px of buttons plus a 74px speed cluster is taller than
     the four rows the right-hand pills give us. */
  grid-template-rows: auto auto auto auto auto auto auto 1fr auto auto auto auto;
  column-gap: 10px;   /* rows keep their air with margin-bottom: an empty row must add none */
  row-gap: 0;
  padding: calc(12px + env(safe-area-inset-top, 0px))
           calc(14px + env(safe-area-inset-right, 0px))
           calc(18px + env(safe-area-inset-bottom, 0px))
           calc(14px + env(safe-area-inset-left, 0px));
  box-sizing: border-box;
  transition: padding-bottom 0.25s ease;
}
/* The browser's exit-fullscreen notice sits over the bottom of the screen for
   ~10.5 s after nav asks for fullscreen and takes every tap there (js/nav.js
   liftForFsNotice). The bottom row rides above it until it goes. */
#nav-overlay.fs-notice:not(.hidden) { padding-bottom: calc(112px + env(safe-area-inset-bottom, 0px)); }
/* Manual zoom in nav: + / - and the Auto hand-back. Sits in the empty 1fr
   column between the speed cluster and the pill stack, so it costs no height:
   the pills already make those rows taller than it is. */
/* Two cells side by side, not stacked: upright at 360x640 with every alert lit
   the screen is 17px short of fitting a 94px-tall stepper, and a wide one costs
   46px of height instead (measured, scripts/qa/nav-overlap-test.js). It is also
   the shape landscape needs, so there is now one shape at every width. */
#nav-zoom {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  border-radius: 22px;
  overflow: hidden;
  background: rgba(10,14,20,0.74);
  border: 1px solid rgba(255,255,255,0.12);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
/* Each button carries its own pill chrome. The other nav pills put the dark
   background, border and blur on the CONTAINER, because each is a single
   button; these are three separate targets with gaps between them, so without
   this they render as bare pale-grey glyphs on a moving map — present, and
   invisible (Josh, 2026-09-18: "eu nnot showing it, I've opened it 3 times").
   Same treatment as #nav-close. */
#nav-zoom .mode-switch-btn {
  min-width: 46px;
  min-height: 46px;
  padding: 0;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1;
  color: #fff;
  background: transparent;
  border: 0;
  border-radius: 0;
}
#nav-zoom .mode-switch-btn + .mode-switch-btn { border-left: 1px solid rgba(255,255,255,0.12); }
#nav-zoom .mode-switch-btn:active { background: rgba(0,200,83,0.9); }
/* The rider holds the zoom. Shown by lighting the stepper rather than by
   growing a third button mid-ride: nothing appears or disappears while moving.
   A long press on either cell hands it back (js/nav.js). */
#nav-zoom.held .mode-switch-btn {
  border-color: rgba(0,200,83,0.75);
  color: #6dffa4;
}

/* ===== THE DOCK =====================================================
   One bar along the bottom carrying every riding-screen control (Josh,
   2026-09-18, option 1 of the ten-layout report). It replaced five separate
   floating clusters carrying three different target sizes.
   64px cells: above Apple's 44pt floor, and the tallest row that still fits
   four cells plus the speed and elevation readouts on a 360x640 phone.
   Google's Design for Driving asks 76dp for a screen used in a vehicle and
   says to trade width for height when it will not fit — which is why these
   are tall and narrow rather than short and wide. */
#nav-dock {
  display: flex;
  align-items: stretch;
  gap: 4px;
  padding: 5px;
  background: rgba(10,14,20,0.78);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 20px;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
/* flex-basis auto, not 0 (2026-10-27): each cell starts at its own word's
   width and the spare room is shared out. Five equal cells on a 338px phone
   are 56px, and OVERVIEW in Inter is 71px; sized from the words, all five
   fit in ~276px of the 310px dock. */
.nav-dock-btn {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 64px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 0 4px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 15px;
  color: #fff;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.nav-dock-btn i { font-size: 1.15rem; line-height: 1; }
/* The word, not an abbreviation the rider has to decode. Letter-spaced small
   caps are slow to read, so this is the one place the app keeps them short:
   END, VOICE, 2D, TOPO — four characters at most, never a sentence. */
.nav-dock-label {
  font-size: max(0.62rem, var(--text-floor, 0px));
  font-weight: 700;
  letter-spacing: 0.08em;
  line-height: 1;
  color: var(--text-secondary, #9aa8a1);
  text-transform: uppercase;
}
.nav-dock-btn:active { background: rgba(255,255,255,0.10); }
/* OVERVIEW is hidden until there is a route and the switch is on; the flex
   rule above would otherwise beat the hidden attribute. Its words (OVERVIEW,
   BACK TO ME; Josh chose them, 2026-10-27) are the only ones over four
   characters, and stay on one line: two lines made the dock taller and ran it
   off a 360x640 screen. */
.nav-dock-btn[hidden] { display: none; }
#nav-overview .nav-dock-label { white-space: nowrap; }
/* Lit = you are IN the non-default state. The word says which state; the light
   says the word is a readout, not a command. Both change together, which is
   what the bare "2D" pill could not do. */
.nav-dock-btn.active {
  background: rgba(0,200,83,0.22);
  border-color: rgba(0,200,83,0.6);
}
.nav-dock-btn.active i,
.nav-dock-btn.active .nav-dock-label { color: #6dffa4; }
#nav-voice.muted { background: rgba(255,171,64,0.16); border-color: rgba(255,171,64,0.55); }
#nav-voice.muted .nav-dock-label { color: #ffab40; }
/* END is the one press a rider cannot take back, so it never lights green and
   it reads in the warning colour the moment a finger is on it. */
#nav-close:active { background: rgba(255,82,82,0.25); }
#nav-close:hover i { color: var(--danger); }
/* A phone on its side has ~354px of height: the dock drops to a single row of
   icon-beside-word so it costs 52px instead of 74. */
@media (max-height: 500px) {
  .nav-dock-btn { min-height: 44px; flex-direction: row; gap: 6px; }
  .nav-dock-label { font-size: max(0.6rem, var(--text-floor, 0px)); }
  /* Five cells (OVERVIEW joined, 2026-10-27) do not fit the ~310px the bottom
     row leaves a 640x360 screen at one width each: 425px measured, pushing
     the zoom and elevation off the screen. On its side the row is as tall as
     the speed readout anyway, so the cells go icon-over-word and each takes
     its own word's width (44px at least): 5 cells in ~300px with BACK TO ME
     in Inter, the widest the row has to take. */
  #nav-dock { gap: 3px; }
  .nav-dock-btn { flex: 0 0 auto; min-width: 44px; flex-direction: column; gap: 3px; padding: 0 3px; }
}

#nav-overlay:not(.hidden) > #nav-turn,
#nav-overlay:not(.hidden) > #nav-eta,
#nav-overlay:not(.hidden) > #nav-clock,
#nav-overlay:not(.hidden) > #nav-battery-suggest,
#nav-overlay:not(.hidden) > #nav-camera,
#nav-overlay:not(.hidden) > #nav-poi,
#nav-overlay:not(.hidden) > #nav-offline-note,
#nav-overlay:not(.hidden) > #nav-gps-note,
#nav-overlay:not(.hidden) > #nav-radar-note,
#nav-overlay:not(.hidden) > #nav-compass,
#nav-overlay:not(.hidden) > #nav-wx,
#nav-overlay:not(.hidden) > #nav-dock,
#nav-overlay:not(.hidden) > #nav-zoom,
#nav-overlay:not(.hidden) > #nav-speed,
#nav-overlay:not(.hidden) > #nav-elev {
  position: relative;
  top: auto; right: auto; bottom: auto; left: auto;
  transform: none;
  margin: 0;
  min-width: 0;
  max-width: 100%;
  box-sizing: border-box;
  z-index: 10;
}
/* Narrow (a phone upright). Row 1: close, clock, ETA. Row 2: the turn banner
   on its own full-width row, so a road name has the whole screen width.
   Speed spans the first two columns and the right-hand stack the last, so
   neither the 172px speed cluster nor the pills can size the top row's columns
   (a grid item that spans the 1fr track does not grow the auto tracks). */
#nav-overlay:not(.hidden) > #nav-close { grid-column: 1; grid-row: 1; align-self: start; margin: 0 0 10px 0; }
/* The clock takes column 1, which the X left empty when it moved into the dock.
   It used to sit in the flexible 1fr track, and the grid reset above gives every
   child min-width: 0 — so a longer time string was squeezed narrower than its own
   text and the time drew OUTSIDE its pill, over the map (Josh, 2026-09-18: "the
   time is outside the pill"; measured on the S23, a 75px pill holding an 85px
   "04:26 PM"). A 24-hour "16:26" fitted, which is why it was never seen here.
   min-width: max-content is the guard, and it is the WHOLE fix: the track is
   1fr, which is minmax(auto, 1fr), so an item that refuses to go below its own
   text makes the track at least that wide. Moving the clock to another column
   instead was tried and reverted — it stole width from the auto tracks and
   pushed the dock off the bottom of a 360x640 screen. */
#nav-overlay:not(.hidden) > #nav-clock { grid-column: 1; grid-row: 1; justify-self: start; align-self: start; margin: 0 0 10px 0; white-space: nowrap; min-width: max-content; }
/* The ETA chip's three lines each stay on one line. Left to wrap, a long one
   ("arrive 11:42 PM") folded onto a fourth line, and the top row grew enough to
   push the dock off the bottom of a 360x640 screen. */
#nav-overlay:not(.hidden) > #nav-eta > span { white-space: nowrap; }
/* Same for the two corner readouts. Wrapped, the elevation chip went from 41px
   to 68px tall and took the dock off the bottom with it. A readout that folds
   is unreadable at a glance anyway. */
#nav-overlay:not(.hidden) > #nav-elev,
#nav-overlay:not(.hidden) > #nav-speed { white-space: nowrap; }
/* A CONTROL IS NEVER SMALLER THAN ITSELF.
   The grid reset above gives every child min-width: 0 so a long road name in the
   turn banner cannot widen the whole row. That is right for the banner and wrong
   for anything a rider presses: in landscape the 1fr track squeezed the zoom
   stepper to ELEVEN PIXELS on the S23 (measured, 2026-09-18) — present, and
   impossible to hit. The same squeeze had already put the clock's time outside
   its pill. Both controls now refuse to go below their own content. */
#nav-overlay:not(.hidden) > #nav-zoom,
#nav-overlay:not(.hidden) > #nav-dock { min-width: max-content; }
#nav-overlay:not(.hidden) > #nav-eta { grid-column: 3; grid-row: 1; justify-self: end; margin: 0 0 10px 0; }
/* The heading takes the top row's middle cell, which held nothing: its own
   cell, so it can never sit over the clock or the ETA (one grid, one readout
   per cell). min-width: max-content for the same reason as the clock. */
#nav-overlay:not(.hidden) > #nav-compass { grid-column: 2; grid-row: 1; justify-self: center; align-self: start; margin: 0 0 10px 0; white-space: nowrap; min-width: max-content; }
/* Upright, a 2x2 with the clock and compass (Josh, 2026-10-18): temperature
   under the clock, wind under the compass. The wrapper steps out of the way
   (display: contents) so each pill is a grid item in its own column, one
   pill height (32px) plus the 10px gap below the top row, so neither can sit on the
   clock or the compass. */
#nav-overlay:not(.hidden) > #nav-wx { display: contents; }
#nav-overlay:not(.hidden) > #nav-wx > #nav-temp { grid-column: 1; grid-row: 1; justify-self: start; align-self: start; margin: 42px 0 10px 0; }
#nav-overlay:not(.hidden) > #nav-wx > #nav-wind { grid-column: 2; grid-row: 1; justify-self: center; align-self: start; margin: 42px 0 10px 0; min-width: max-content; }
/* The clock keeps its corner with or without a route: column 1 is free at every
   width now that the X lives in the dock. */
#nav-overlay:not(.hidden) > #nav-turn { grid-column: 1 / -1; grid-row: 2; justify-self: center; align-self: start; margin: 0 0 10px 0; }
/* Alerts stack under the turn banner, centred, one per row. */
#nav-overlay:not(.hidden) > #nav-battery-suggest { grid-column: 1 / -1; grid-row: 3; justify-self: center; margin: 0 0 10px 0; }
#nav-overlay:not(.hidden) > #nav-camera { grid-column: 1 / -1; grid-row: 4; justify-self: center; margin: 0 0 10px 0; }
#nav-overlay:not(.hidden) > #nav-poi { grid-column: 1 / -1; grid-row: 5; justify-self: center; margin: 0 0 10px 0; }
#nav-overlay:not(.hidden) > #nav-offline-note { grid-column: 1 / -1; grid-row: 6; justify-self: center; margin: 0 0 10px 0; white-space: normal; text-align: center; }
/* Weak GPS sits just above the speed it spoils, beside the elevation chip:
   row 10's first two cells hold nothing else, so it adds no row. */
#nav-overlay:not(.hidden) > #nav-gps-note { grid-column: 1 / 3; grid-row: 10; justify-self: start; align-self: end; margin: 0 0 10px 0; white-space: nowrap; }
/* Wide (a phone on its side) has no height to spare: the top row's first cell,
   left of the turn banner, is free there. */
@media (min-width: 600px), (orientation: landscape) and (max-height: 500px) {
  #nav-overlay:not(.hidden) > #nav-gps-note { grid-column: 1; grid-row: 1; align-self: start; }
}
#nav-overlay:not(.hidden) > #nav-radar-note { grid-column: 1 / -1; grid-row: 7; justify-self: center; margin: 0 0 10px 0; white-space: normal; text-align: center; }
/* Bottom, in three bands rather than two ragged columns:
   row 10  the zoom stepper, floating right, clear of everything
   row 11  speed left, elevation right — the two readouts, on one line
   row 12  the dock, full width
   This is why the dock is the shape it is: it owns a whole row, so nothing can
   ever be stacked beside it and no future control needs a row of its own. */
#nav-overlay:not(.hidden) > #nav-elev { grid-column: 3; grid-row: 10; justify-self: end; align-self: end; margin: 0 0 10px 0; }
/* The zoom stepper shares the speed's row rather than taking one of its own:
   at 360x640 with every alert lit there is no row to spare, and the control
   belongs nearer the thumb than the elevation readout does anyway. */
#nav-overlay:not(.hidden) > #nav-speed { grid-column: 1; grid-row: 11; justify-self: start; align-self: end; margin: 0 0 10px 0; }
#nav-overlay:not(.hidden) > #nav-zoom { grid-column: 3; grid-row: 11; justify-self: end; align-self: end; margin: 0 0 10px 0; }
#nav-overlay:not(.hidden) > #nav-dock { grid-column: 1 / -1; grid-row: 12; justify-self: stretch; align-self: end; margin: 0; }
#nav-turn-dist { white-space: nowrap; }

/* Wide enough (a phone on its side, a tablet, a desktop): the turn banner sits
   between the close button and the clock on the top row, the three pills in a
   row along the bottom edge between the speed and the elevation. The clock
   shares its columns with the 2D/3D pill, which is on another row, so a
   narrow 1fr track cannot wrap it. Height is what a phone on its side lacks
   (354px on an S23), and this shape spends 66px on the top row, not 137. */
@media (min-width: 600px), (orientation: landscape) and (max-height: 500px) {
  #nav-overlay:not(.hidden) { grid-template-columns: auto 1fr auto auto auto 1fr auto; }
  #nav-overlay:not(.hidden) > #nav-turn { grid-column: 2 / 5; grid-row: 1; }
  /* On its side the top row is full (at 640 px wide a column for the heading
     pushed the ETA chip off the screen), so the heading takes the one free
     cell: column 1, left of the turn banner. The weak-GPS note owns that cell
     while it shows, and a fix too weak to trust has no heading worth printing,
     so the heading steps aside for it (the note comes first in the markup). */
  #nav-overlay:not(.hidden) > #nav-compass { grid-column: 1; grid-row: 1; justify-self: start; align-self: start; margin: 0 0 10px 0; }
  #nav-overlay:not(.hidden) > #nav-gps-note:not(.hidden) ~ #nav-compass { display: none; }
  /* On its side there is no height to spare: under the compass they pushed
     the dock off a 640x360 screen with every alert lit, and the bottom row's
     gap beside the dock is too narrow at 640. They sit above the speed, in
     the elevation's row, which is empty on the left: no height of their
     own. Spanning into the 1fr columns keeps them from widening column 1
     and squeezing the turn banner. */
  #nav-overlay:not(.hidden) > #nav-wx { display: flex; grid-column: 1 / 7; grid-row: 11; justify-self: start; align-self: end; margin: 0 0 10px 0; flex-wrap: nowrap; }
  #nav-overlay:not(.hidden) > #nav-wx > .nav-wx-pill { margin: 0; }
  /* The speed alert owns that row while it shows; safety before weather. */
  #nav-overlay:not(.hidden) > #nav-speed-alert:not(.hidden) ~ #nav-wx { display: none; }
  #nav-overlay:not(.hidden) > #nav-clock { grid-column: 5 / 7; grid-row: 1; margin: 0 0 10px 0; }
  #nav-overlay:not(.hidden) > #nav-eta { grid-column: 7; }
  #nav-overlay:not(.hidden) > #nav-eta.hidden ~ #nav-clock { grid-column: 5 / 8; }
  /* On its side there is no room for a band of its own: a 640x360 screen with
     every alert lit pushed a full-width dock clean off the bottom (measured by
     scripts/qa/nav-overlap-test.js, 2026-09-18). So the whole bottom edge is
     ONE row — speed, zoom, dock, elevation — and the dock takes the middle,
     where the thumb is, in its short icon-beside-word form. It is still one
     bar with four cells; it just is not the full width here. */
  #nav-overlay:not(.hidden) > #nav-speed { grid-column: 1; grid-row: 12; }
  #nav-overlay:not(.hidden) > #nav-zoom { grid-column: 2; grid-row: 12; justify-self: start; align-self: end; margin: 0 0 0 10px; }
  #nav-overlay:not(.hidden) > #nav-dock { grid-column: 4 / 6; grid-row: 12; justify-self: center; align-self: end; margin: 0; }
  #nav-overlay:not(.hidden) > #nav-elev { grid-column: 7; grid-row: 12; }
}

/* Short (a phone on its side): the two heads-up pills share a row, worth-a-stop
   on the left, camera on the right, so four stacked alerts cannot push the
   speed off the bottom of a 354px screen. Pills lose a little padding. */
@media (min-width: 600px) and (max-height: 500px), (orientation: landscape) and (max-height: 500px) {
  #nav-overlay:not(.hidden) > #nav-poi { grid-column: 1 / 5; grid-row: 4; justify-self: end; }
  #nav-overlay:not(.hidden) > #nav-camera { grid-column: 5 / 8; grid-row: 4; justify-self: start; }
  #nav-overlay:not(.hidden) #nav-zoom .mode-switch-btn { min-width: 44px; min-height: 44px; }
}

/* Phone-sized readouts (narrow, or short as a phone on its side): a step
   smaller so the top row and the bottom row both fit with air between them. */
@media (max-width: 480px), (max-height: 500px) {
  #nav-speed-value { font-size: 3rem; }
  #nav-speed { gap: 10px; padding: 6px 12px; }
  #nav-speed-limit { width: 46px; height: 46px; border-width: 4px; }
  #nav-speed-limit-value { font-size: 1.1rem; }
  #nav-turn { gap: 10px; padding: 8px 12px 8px 8px; }
  .nav-turn-icon { width: 40px; height: 40px; font-size: 1.3rem; }
  #nav-turn-dist { font-size: 1.35rem; }
  #nav-turn-road { font-size: 0.85rem; }
  #nav-eta-dist { font-size: 1.05rem; }
  #nav-clock { font-size: 0.85rem; padding: 6px 10px; }
  #nav-elev-value { font-size: 1.15rem; }
  #nav-elev { padding: 6px 10px; }
}

/* ===== Closure choice card (js/closure-choice-card.js) =====
 * A road on the route is shut and we know when it opens. Josh, 2026-09-19:
 * "a very loud warning on the main map ... the user has to say ok, or be
 * given the option to route around it." Over the map, map dimmed, no way out
 * but a decision. */
.closure-choice-backdrop {
  position: fixed;
  inset: 0;
  z-index: 100004;                 /* above every map control and panel */
  background: rgba(0, 0, 0, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;                   /* the phone gutter; never a horizontal scroll */
  overscroll-behavior: contain;
}
.closure-choice-card {
  background: #1b1b1d;
  color: #fff;
  border: 3px solid #ff1744;
  border-radius: 12px;
  max-width: 420px;
  width: 100%;
  max-height: calc(100vh - 32px);
  overflow-y: auto;
  padding: 18px 18px 16px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.6);
}
.closure-choice-flag {
  background: #ff1744;
  color: #fff;
  font-weight: 800;
  font-size: max(13px, var(--text-floor, 0px));
  letter-spacing: 0.06em;
  text-align: center;
  padding: 7px 10px;
  border-radius: 5px;
  margin: -4px -4px 12px;
}
.closure-choice-name { font-size: 21px; line-height: 1.25; margin: 0 0 8px; font-weight: 700; }
.closure-choice-why { font-size: 14px; line-height: 1.45; margin: 0 0 8px; color: #dcdcdf; }
.closure-choice-opens {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
  background: #24240f;
  border-left: 4px solid #ffc107;
  border-radius: 5px;
  padding: 10px 12px;
  margin: 12px 0 10px;
}
.closure-choice-opens-label { font-size: max(12px, var(--text-floor, 0px)); text-transform: uppercase; letter-spacing: 0.07em; color: #ffc107; font-weight: 700; }
.closure-choice-opens-date { font-size: 20px; font-weight: 800; color: #fff; }
.closure-choice-opens-away { font-size: 14px; color: #c9c9a8; }
.closure-choice-src { font-size: max(12px, var(--text-floor, 0px)); color: #9c9ca2; margin: 0 0 4px; }
.closure-choice-src a { color: #7fb5ff; }
.closure-choice-buttons { display: flex; gap: 10px; margin: 14px 0 10px; }
.closure-choice-buttons button {
  flex: 1;
  min-height: 48px;                /* a gloved thumb in a moving vehicle */
  border-radius: 8px;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  border: 2px solid transparent;
  padding: 10px 8px;
}
.closure-choice-avoid { background: #2e7d32; color: #fff; border-color: #2e7d32; }
/* An OUTLINED button, not a link. On the S23 and on desktop this rendered as
 * bare red text beside a solid green button, which quietly made "take it" look
 * like the lesser option rather than the other half of a real choice
 * (measured on the phone 2026-09-19). */
.closure-choice-take {
  background: rgba(255, 23, 68, 0.10);
  color: #ff8a80;
  border: 2px solid #ff1744;
}
/* "I'm going on or after {day}" (2026-10-21): its own full-width row under
 * the two, because the day makes it the longest label and it is a different
 * kind of answer (when, not which way). Blue, so it reads as neither "safe"
 * green nor "risky" red. */
.closure-choice-buttons { flex-wrap: wrap; }
/* Two classes deep, so it outranks `.closure-choice-buttons button { flex: 1 }`
 * above; one class lost to it and squeezed the label into a third column. */
.closure-choice-buttons .closure-choice-wait {
  flex: 1 1 100%;
  background: rgba(127, 181, 255, 0.10);
  color: #cfe2ff;
  border: 2px solid #7fb5ff;
}
.closure-choice-foot { font-size: max(12px, var(--text-floor, 0px)); color: #8e8e94; margin: 0; line-height: 1.4; }

@media (max-width: 380px) {
  .closure-choice-buttons { flex-direction: column; }
  .closure-choice-name { font-size: 19px; }
}

/* The trip date on the Route panel (js/trip-date.js, 2026-09-23): the day the
   closures are judged for. Same box as the AI panel's date. */
.route-date { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 10px 0 2px; }
.route-date label { font-size: 0.88rem; font-weight: 600; color: var(--text-primary); white-space: nowrap; }
.route-date input[type="date"] {
  flex: 1; min-width: 150px; min-height: 40px; padding: 0 10px;
  background: var(--bg-tertiary); border: 1px solid transparent; border-radius: var(--radius);
  color: var(--text-primary); font-family: inherit; font-size: 0.95rem;
}
.route-date input[type="date"]:focus { border-color: var(--accent); outline: none; }
.route-date-note { flex-basis: 100%; font-size: 0.82rem; line-height: 1.4; color: var(--warning, #ffab40); }
.route-date-note[hidden] { display: none; }

/* Fuel prices on the fuel station card (js/fuel-prices.js). Grey = last known
   prices, not confirmed recently; the footer line says how old. */
.fuel-prices { margin: 10px 0 4px; padding: 8px 10px; border: 1px solid var(--border, rgba(255,255,255,0.12)); border-radius: 8px; }
.fuel-prices .fp-title { font-size: max(0.8em, var(--text-floor, 0px)); font-weight: 600; margin-bottom: 4px; }
.fuel-prices .fp-table { width: 100%; border-collapse: collapse; font-size: max(0.85em, var(--text-floor, 0px)); }
.fuel-prices .fp-table td, .fuel-prices .fp-table th { padding: 2px 0; text-align: left; }
.fuel-prices .fp-table th { font-weight: 500; font-size: max(0.85em, var(--text-floor, 0px)); color: var(--text-muted); }
.fuel-prices .fp-price, .fuel-prices .fp-tm, .fuel-prices .fp-table th:not(:first-child) { text-align: right; font-variant-numeric: tabular-nums; }
.fuel-prices .fp-price { font-weight: 600; }
.fuel-prices .fp-tm { color: var(--text-muted); padding-left: 10px; }
.fuel-prices .fp-old { font-size: max(0.8em, var(--text-floor, 0px)); color: var(--text-muted); }
.fuel-prices .fp-foot { font-size: max(0.72em, var(--text-floor, 0px)); color: var(--text-muted); margin-top: 6px; line-height: 1.4; }
.fuel-prices .fp-foot a { color: var(--accent); }
.fuel-prices.fp-stale .fp-table { opacity: 0.55; }

/* ===== A place's own website and phone, first on its card (js/place-contact.js) =====
   Josh, 2026-10-02: the direct link is the one to press, so it is larger than
   the other links and filled; Booking.com and the rest stay small below it. */
.place-direct { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 12px; }
.place-direct a {
  display: flex; align-items: center; gap: 8px; flex: 1 1 190px; min-width: 0;
  min-height: 44px; padding: 8px 12px; border-radius: var(--radius-sm);
  font-size: 0.95rem; font-weight: 600; text-decoration: none;
}
.place-direct-site { background: var(--accent); color: #000; border: 1px solid var(--accent); }
.place-direct-site:hover { background: var(--accent-hover); }
.place-direct-call { color: var(--accent); border: 1px solid var(--accent); background: transparent; }
.place-direct-label { flex-shrink: 0; }
.place-direct-detail { font-size: 0.8rem; font-weight: 400; opacity: 0.85; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ===== Speed limit sign: its own cell, dead centre at the bottom (Josh, 2026-10-03)
   The sign sat in the speed box, to the right of the number, so it slid
   sideways every time the speed went from one digit to two to three. And the
   grid reset gives every child min-width: 0, which held the speed box at its
   one-digit width: on an S23 "108" drew 34px past the right edge of its box.
   Now the sign is a grid item of its own, centred on the screen in the speed's
   row with its bottom on the speed box's bottom. The speed box is one size
   whatever the digits (room for three, the unit under the number), so it can
   neither spill nor reach the sign: on a 384px S23 the box ends at 135px and
   the sign starts at 166px.
   Appended at the end on purpose: in AUS this file is several copies of itself
   and the last rule is the one the browser obeys. */
#nav-overlay:not(.hidden) > #nav-speed { min-width: max-content; grid-column: 1 / 3; }
/* Digits have no descenders, so the number's line box is cut to 0.85 of the
   font: the same size digits, 7px less height, which a 360x640 phone with every
   alert lit needs. */
#nav-speed { padding: 4px 10px; }
.nav-speed-read { flex-direction: column; align-items: center; gap: 2px; }
#nav-speed-value { font-size: 3.2rem; line-height: 0.85; min-width: 1.95em; text-align: center; }
#nav-speed-unit { font-size: max(0.75rem, var(--text-floor, 0px)); line-height: 1; }
/* One sign size at every screen size, with digits sized so a three-digit limit
   ("100") sits inside the ring's opening instead of on the red. */
#nav-speed-limit { width: 52px; height: 52px; border-width: 5px; }
#nav-speed-limit-value { font-size: 0.95rem; letter-spacing: -0.5px; }
#nav-overlay:not(.hidden) > #nav-speed-limit {
  position: relative;
  top: auto; right: auto; bottom: auto; left: auto;
  z-index: 10;
  grid-column: 1 / -1;
  grid-row: 11;
  justify-self: center;
  align-self: end;
  margin: 0 0 10px 0;
}
@media (max-width: 480px), (max-height: 500px) {
  #nav-speed-value { font-size: 2.4rem; }
}
/* On its side the bottom edge is one row: speed, sign, dock, zoom, all on one
   bottom line. The elevation readout moves up a row on the right to make the
   width: it is 5px shorter than the zoom stepper, and EU's longer off-route
   note left a 640x360 screen exactly that short of height. */
@media (min-width: 600px), (orientation: landscape) and (max-height: 500px) {
  #nav-overlay:not(.hidden) > #nav-speed { grid-column: 1; margin: 0; }
  #nav-overlay:not(.hidden) > #nav-speed-limit { grid-column: 2 / 4; grid-row: 12; margin: 0; }
  #nav-overlay:not(.hidden) > #nav-zoom { grid-column: 7; grid-row: 12; justify-self: end; margin: 0; }
  #nav-overlay:not(.hidden) > #nav-elev { grid-column: 7; grid-row: 11; justify-self: end; margin: 0 0 10px 0; }
}
/* That row costs a phone on its side height, so the off-route and radar notes
   share one row there, as worth-a-stop and camera already do: AUS at 640x360
   with every alert lit ran 34px off the bottom otherwise. */
@media (min-width: 600px) and (max-height: 500px), (orientation: landscape) and (max-height: 500px) {
  #nav-overlay:not(.hidden) > #nav-offline-note { grid-column: 1 / 5; grid-row: 6; justify-self: end; white-space: nowrap; }
  #nav-overlay:not(.hidden) > #nav-radar-note { grid-column: 5 / 8; grid-row: 6; justify-self: start; white-space: nowrap; }
}

/* ===== A phone on its side gets the landscape nav (Josh, 2026-10-03)
   Every nav rule above that says "(min-width: 600px)" also says "or a phone on
   its side": js/phone-layout.js answers width terms against a handset's SHORT
   side (384 on an S23) so the app keeps its phone layout when turned, and that
   killed the whole landscape nav. On the S23 on its side the upright layout
   ran the speed, sign and dock off the bottom of a 384px-tall screen. The
   orientation term has no width in it, so the rewrite leaves it alone. */

/* ===== Speedo top bar: each reading at its own size, clear of the X (2026-10-03)
   Readings were allowed to shrink below their own contents, so once the clock
   joined the bar the squeezed wind reading drew its arrow under the close
   button on a phone on its side (S23: arrow at 770-790px, X from 777px). Now a
   reading never shrinks, and a phone on its side buys the width back: the
   left padding (60px was only ever needed on the right, for the X), a little
   less gap, and one step smaller numbers and units. Checked at the widest real
   readings ("2228 m", "NW 315°") by scripts/qa/dash-safe-area-test.js. */
.dash-alt-reading { flex-shrink: 0; }
@media (orientation: landscape) and (max-height: 500px) {
  .dash-top-bar { padding-left: 16px; gap: 12px; }
  .dash-alt-value, .dash-alt-reading #dash-heading-value { font-size: 1.1rem; }
  .dash-alt-unit { font-size: max(0.75rem, var(--text-floor, 0px)); }
}

/* ===== Speed alert (js/speed-alert.js, 2026-10-07)
   One quiet notice when the speed is well over a posted limit, gone after
   5 s. It may not cost a row: at 360x640 with every alert lit the screen has
   7px to spare (a row of its own pushed the dock 36px off the bottom,
   scripts/qa/nav-overlap-test.js). So it takes a cell that is already there.
   Upright: the Weak GPS note's cell, just above the speed, left of the
   elevation chip. The two never want it at once: a weak fix never alerts
   (its speed is as vague as its place), and should the note come up while
   the notice shows, the note wins. On its side: row 11 over the sign, beside
   the elevation chip (the weak-GPS note lives in the top row there).
   Discreet on purpose: the dash's own dark glass, a thin amber edge, no red,
   no flashing, and taps go through it to the map. */
#nav-speed-alert {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 6px 10px;
  background: rgba(10,14,20,0.8);
  border: 1px solid rgba(255,255,255,0.12);
  border-left: 3px solid #ffb74d;
  border-radius: 12px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  font-size: 0.9rem;
  font-weight: 700;
  line-height: 1.2;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
#nav-speed-alert i { color: #ffb74d; font-size: 0.95rem; }
/* Fades in, holds, fades out inside the 5 s; the script hides it at 5 s. */
#nav-speed-alert.sa-on { animation: nav-speed-alert-life 5s ease-in-out both; }
@keyframes nav-speed-alert-life {
  0% { opacity: 0; }
  6% { opacity: 1; }
  88% { opacity: 1; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  #nav-speed-alert.sa-on { animation: none; }
}
#nav-overlay:not(.hidden) > #nav-speed-alert {
  position: relative;
  top: auto; right: auto; bottom: auto; left: auto;
  z-index: 10;
  min-width: 0;
  max-width: 100%;
  box-sizing: border-box;
  grid-column: 1 / 3;
  grid-row: 10;
  justify-self: start;
  align-self: end;
  margin: 0 0 10px 0;
}
#nav-speed-alert-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
#nav-overlay:not(.hidden) > #nav-gps-note:not(.hidden) ~ #nav-speed-alert { display: none; }
@media (min-width: 600px), (orientation: landscape) and (max-height: 500px) {
  #nav-overlay:not(.hidden) > #nav-speed-alert { grid-column: 2 / 4; grid-row: 11; justify-self: center; }
}

/* ===== Sign-in value screen + trial day-6 card (js/value-first.js) ===== */
/* While the switch answers (at most 1.5 s), a first-time visitor's card stays
   hidden so it does not flash before the value screen. */
#login-screen.vf-wait .login-card { visibility: hidden; }
.vf-card { align-items: stretch; padding: 22px 20px; gap: 10px; }
.vf-logo { width: 48px; height: 48px; border-radius: 12px; align-self: center; }
.vf-title { margin: 0 0 4px; font-size: 1.2rem; line-height: 1.3; text-align: center; color: var(--text-primary, #fff); }
.vf-row { display: flex; align-items: center; gap: 12px; }
.vf-row img { width: 112px; height: 82px; flex-shrink: 0; object-fit: cover; border-radius: 8px; border: 1px solid var(--border, rgba(255,255,255,0.12)); background: var(--bg-tertiary, #222); }
.vf-row p { margin: 0; font-size: 0.86rem; line-height: 1.4; color: var(--text-primary, #fff); }
.vf-terms { margin: 4px 0 2px; font-size: max(0.76rem, var(--text-floor, 0px)); line-height: 1.45; color: var(--text-muted, #bbb); text-align: center; }
.vf-card .login-btn { margin-bottom: 0; }
.vf-link { background: none; border: none; color: var(--text-muted, #999); font-size: 0.8rem; text-decoration: underline; }

/* ===== Start on a searched place (js/search-go.js) =====
   Sits in the planned-route Start's own slot, with its look, and that Start
   steps aside while this one shows (body.search-go-on). Same positions as
   #route-start-btn: 154px on a desktop, 44px on a phone, 214px under 381px.
   Appended at the end: the AUS stylesheet is concatenated copies of itself,
   so only the end of the file is last in every region. */
#search-go-btn {
  position: fixed;
  bottom: 154px;
  left: calc(50% + var(--sidebar-width) / 2);
  transform: translateX(-50%);
  z-index: 600;
  min-height: 56px;
  padding: 0 38px;
  border: none;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  cursor: pointer;
  display: flex; align-items: center; gap: 12px;
  white-space: nowrap;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5), 0 0 0 4px rgba(0, 200, 83, 0.18);
  transition: background var(--transition), box-shadow var(--transition), transform var(--transition);
}
#search-go-btn:hover { background: var(--accent-hover); }
#search-go-btn:active { transform: translateX(-50%) scale(0.97); }
#search-go-btn i { font-size: 1.05rem; }
#search-go-btn.hidden { display: none; }
body.search-go-on #route-start-btn,
body.dashboard-active #search-go-btn,
body.nav-active #search-go-btn { display: none !important; }
@media (max-width: 768px) {
  #search-go-btn { left: 50%; bottom: 44px; min-height: 54px; padding: 0 26px; font-size: 1.05rem; }
}
@media (max-width: 380px) {
  #search-go-btn { bottom: 214px; }
}

/* ===== Save, Email me, Share on the map (js/route-start.js, 2026-10-21) =====
   Under the route tiles (#route-hud), same left edge and the same right bound
   (the button column), so the pair reads as one block about this route. The
   top is the tiles' MEASURED bottom edge (--route-actions-top, set by
   RouteStart.placeActions); the calc is only the fallback for one row of tiles.
   Appended at the end: the stylesheet is concatenated copies of itself, so
   only the end of the file is last in every region. */
#route-actions {
  position: fixed;
  top: var(--route-actions-top, 64px);
  left: calc(var(--sidebar-width, 0px) + 16px);
  right: calc(16px + var(--map-controls-w, 116px) + 8px);
  width: fit-content;
  z-index: 500;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
#route-actions.hidden { display: none !important; }
#route-actions button {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 38px;
  padding: 0 12px;
  background: var(--bg-secondary, #1c1f24);
  color: var(--text-primary, #fff);
  border: 1px solid var(--bg-tertiary, #2a2f36);
  border-radius: 999px;
  box-shadow: var(--shadow, 0 2px 10px rgba(0, 0, 0, 0.4));
  font-size: 0.8rem; font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}
#route-actions button[hidden] { display: none; }
#route-actions button i { color: var(--accent, #00c853); font-size: 0.85rem; }
#route-actions button:hover { border-color: var(--accent, #00c853); }
#route-actions button:disabled { cursor: default; opacity: 0.85; }
#route-actions button.is-saved { border-color: var(--accent, #00c853); }
body.dashboard-active #route-actions,
body.nav-active #route-actions { display: none !important; }
@media (min-width: 769px) {
  body.sidebar-collapsed #route-actions { left: 62px; }
}
@media (max-width: 768px) {
  /* Under the phone's tiles (header + 8, 45 tall) with 6px of air. 44px tall:
     the map's own tap target on a phone. */
  #route-actions {
    top: var(--route-actions-top, calc(var(--mobile-header-height, 56px) + 59px));
    left: 10px;
    right: calc(12px + var(--map-controls-w, 116px) + 8px);
    gap: 4px;
  }
  /* Icon over word, so all three fit beside the button column on a 360px
     phone (side by side they wrapped to two rows at 390, measured). */
  #route-actions button {
    flex-direction: column; justify-content: center; gap: 2px;
    min-height: 44px; min-width: 48px; padding: 3px 6px;
    border-radius: var(--radius-sm, 6px);
    font-size: max(0.64rem, var(--text-floor, 0px)); line-height: 1.1;
  }
  #route-actions button i { font-size: 0.9rem; }
}
/* EU: on a 360px phone the Spanish column ("Compartir" beside it) leaves the
   group 194px; four pixels less padding per button keeps it one row in all
   five languages (scripts/qa/map-furniture-overlap-test.js, 2026-10-21). */
@media (max-width: 374px) {
  #route-actions button { min-width: 44px; padding: 3px 4px; }
}
/* What sat under the tiles steps under the group too: the 3D compass and the
   phone's elevation chip. */
body.route-actions-on .maplibregl-ctrl-top-left { margin-top: calc(var(--route-actions-bottom, 108px) + 8px); }
body.route-actions-on #map-readouts { top: calc(var(--route-actions-bottom, 108px) + 8px); }
@media (max-width: 768px) {
  /* The compass rule for this row is in the phone chrome block below, after
     the route-hud rule it must beat (same weight, so the later one wins). */
  body.route-actions-on #map-readouts { top: calc(var(--route-actions-bottom, 163px) + 8px); }
  /* While the route card is up the tiles and the row step aside: the chip
     moves up with the compass (phone-ux-int review, 1 Nov). */
  body.route-card-open #map-readouts { top: calc(var(--mobile-header-height) + 12px); }
}

/* ===== The phone's map chrome (phone redesign steps 3 and 4) =====
   Josh, 2026-11-01 (tasks/phone-ux.md, answers 1 and 2): on a phone the map at
   rest carries a floating search bar (Menu inside it, and "Rain x" while the
   rain radar is on), Layers, Rig, Nav and Locate down the right edge, and
   Topo/Sat and 2D/3D at the bottom left. Nine controls, where the S10 counted
   nineteen. Dash, Reset view and Full screen moved to the menu's Map tools;
   the tilt and zoom pad is off until the rider switches it on there. Wired in
   js/map-chrome.js. Appended at the end so it is last in every region. The
   desktop keeps its layout: none of this shows there. */
#mobile-search, #map-tools, #btn-layers, #layers-sheet, #btn-compass { display: none; }
#rain-chip[hidden] { display: none !important; }
#mobile-search-x[hidden] { display: none !important; }

@media (max-width: 768px) {
  /* The space the bar takes: 10 above, 48 for the bar, 10 below. Every phone
     rule that cleared the old 56 px header clears this instead. */
  :root { --mobile-header-height: calc(68px + env(safe-area-inset-top, 0px)); }
  /* The map runs full-bleed under the status bar (viewport-fit=cover, and the
     home-screen app's black-translucent status bar): a dark strip the height
     of the notch keeps the clock and battery readable over a light Topo map.
     0 px tall where there is no inset (Android, a Chrome tab). */
  body::before {
    content: ''; position: fixed; z-index: 1000; top: 0; left: 0; right: 0;
    height: env(safe-area-inset-top, 0px); pointer-events: none;
    background: linear-gradient(rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.35));
  }
  body.nav-active::before, body.dashboard-active::before { display: none; }
  /* Facebook's in-app browser bar (js/inapp.js) is fixed at the very top and
     already pads for the status bar: the floating bar goes under it. */
  body.inapp-shifted { --mobile-header-height: calc(68px + var(--inapp-bar-h, 64px)); }

  #mobile-search {
    display: flex; align-items: center; gap: 2px;
    position: fixed; z-index: 1001;
    top: calc(env(safe-area-inset-top, 0px) + 10px); left: 12px; right: 12px;
    height: 48px; padding: 0 6px 0 0;
    background: rgba(26, 38, 50, 0.96);
    border: 1px solid var(--bg-tertiary); border-radius: 24px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
  }
  body.inapp-shifted #mobile-search { top: calc(var(--inapp-bar-h, 64px) + 10px); }
  /* The Menu / Map word under the hamburger keeps inside the bar's round left
     end (Josh, S10: "the 'menu' txt slightly breaches the top bar, move it a
     few pixels to the right so it fits properly"). At 14 px the word's box
     reached 3 px past the curve; 8 px in puts every corner of it 2 px or more
     inside (tests/map-chrome.test.js works it out from the bar's numbers). */
  #mobile-search > #sidebar-toggle { margin-left: 10px; }
  #mobile-search-box {
    flex: 1 1 auto; min-width: 0; height: 48px;
    display: flex; align-items: center; gap: 10px; padding: 0 8px;
    background: none; border: none; border-radius: 22px;
    color: var(--text-secondary); font: inherit; font-size: 0.85rem; text-align: left;
    cursor: text;
  }
  #mobile-search-box span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* A searched place on the bar (Josh, 2026-11-01, S10: "how do I clear a
     search result"): its name in the text colour, one line, and an X before
     the Rain chip that clears the search in one tap (js/map-chrome.js
     showPlace). 48 px to touch, the bar's full height. */
  #mobile-search-box.has-place { color: var(--text-primary); padding-left: 14px; }
  /* The name has the room: no magnifier beside a place, as Google Maps. */
  #mobile-search-box.has-place i { display: none; }
  #mobile-search-x {
    flex: none; width: 48px; height: 48px; padding: 0;
    display: flex; align-items: center; justify-content: center;
    background: none; border: none; border-radius: 24px;
    color: var(--text-secondary); font-size: 1.1rem;
    cursor: pointer; touch-action: manipulation;
  }
  /* The Rain chip: a 32 px pill inside a 48 px tap target, shown both ways.
     Green "Rain x" while the radar is on (.is-on) turns it off; a quiet
     "Rain" with no x while it is off turns it back on in one tap (Josh
     2026-09-16: rain radar on / off from the map). js/map-chrome.js. */
  #rain-chip {
    flex: none; height: 48px; padding: 0 4px 0 2px;
    display: inline-flex; align-items: center;
    border: none; background: none; color: inherit;
    font: inherit; cursor: pointer; touch-action: manipulation;
  }
  #rain-chip .rc-pill {
    display: inline-flex; align-items: center; gap: 5px;
    height: 32px; padding: 0 11px; border-radius: 16px;
    background: var(--bg-tertiary); color: var(--text-secondary);
    font-size: max(0.72rem, var(--text-floor, 0px)); font-weight: 700;
  }
  #rain-chip .rc-x { display: none; }
  #rain-chip.is-on .rc-pill { background: var(--accent); color: #00120a; }
  #rain-chip.is-on .rc-x { display: inline; }
  /* With the menu open the bar is only its Menu / Map button: the menu has the
     search box itself, and two boxes one above the other read as two searches. */
  body:has(#sidebar.open) #mobile-search { right: auto; padding-right: 0; }
  body:has(#sidebar.open) #mobile-search-box,
  body:has(#sidebar.open) #mobile-search-x,
  body:has(#sidebar.open) #rain-chip { display: none !important; }

  /* Search on the map (Josh, 2026-11-01, S10: the bar opened the menu). A tap
     on the search line moves the menu's own box into the bar and its results
     into the panel under it (js/map-chrome.js). The Rain chip steps aside
     while the rider types and comes back after. Rows 48 px, text 18 px,
     nothing under 14 px. */
  #mobile-search.searching { border-color: var(--accent); }
  #mobile-search.searching #mobile-search-box,
  #mobile-search.searching #rain-chip { display: none !important; }
  #mobile-search.searching #mobile-search-x { display: none !important; }
  #mobile-search .search-box {
    flex: 1 1 auto; min-width: 0; height: 48px; gap: 4px;
    padding: 0 0 0 8px; background: none; border: none; border-radius: 22px;
  }
  #mobile-search .search-box i { font-size: 1rem; }
  #mobile-search .search-box input {
    min-width: 0; height: 48px; padding: 0 6px; font-size: 1rem;
    text-overflow: ellipsis;
  }
  #mobile-search .search-box input::placeholder { font-size: 0.85rem; }
  #mobile-search #search-clear {
    flex: none; width: 48px; height: 48px; padding: 0;
    display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
  }
  #mobile-search #search-clear.hidden { display: none !important; }
  #map-search-panel {
    position: absolute; top: calc(100% + 6px); left: -1px; right: -1px;
    display: flex; flex-direction: column; gap: 6px;
    max-height: 50vh; overflow-y: auto; overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  #map-search-panel .search-results {
    margin-top: 0; max-height: none; overflow: visible; flex: none;
    background: rgba(26, 38, 50, 0.98);
    border: 1px solid var(--bg-tertiary); border-radius: 16px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  }
  #map-search-panel .search-result-item {
    min-height: 48px; padding: 8px 16px; gap: 12px;
    font-size: 18px; line-height: 1.25;
  }
  #map-search-panel .search-result-item i { font-size: 16px; width: 20px; }
  #map-search-panel .search-result-item[data-dist]::after { font-size: 14px; }
  #map-search-panel .search-recent-head { padding: 12px 16px 4px; font-size: 14px; }
  #map-search-panel .search-recent-clear { min-height: 48px; padding: 0 16px; font-size: 14px; }
  /* Plan with AI's row (js/smart-search.js styles it inline, for the menu):
     solid over the map, and the sizes above. */
  #map-search-panel #smart-search-ai {
    flex: none; margin-top: 0 !important; min-height: 48px !important;
    padding: 8px 16px !important; font-size: 18px !important;
    background: #12382b !important; border-radius: 16px !important;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  }
  #map-search-panel #smart-search-ai span span { font-size: 14px !important; }

  /* MapLibre's top-left corner (the 3D compass) clears the bar. While a route
     is up the route rules above set the same margin with more weight; with the
     map now starting at the top of the screen they no longer count the header
     twice. */
  /* ...and the slot under the elevation chip (#map-readouts, fixed at the
     same left edge just below the bar): --elev-slot is the chip's 32 px and
     8 px of air, less the control's own 10 px top margin. :where(#map) scopes
     it to the main map with no id weight. Order matters: the three rules
     below have the same weight, so the later wins. The route-actions row
     (tiles plus Save / Share / Clear) comes after the tiles alone, and the
     open route card (both step aside) last. The route-actions rule used to
     sit ABOVE the tiles rule and lost: the compass sat under the elevation
     chip whenever a route was up (phone-ux-s34 graft, phone-ux-int review). */
  :where(#map) .maplibregl-ctrl-top-left { --elev-slot: 30px; margin-top: calc(var(--mobile-header-height) + 12px + var(--elev-slot)); }
  body.route-hud-on :where(#map) .maplibregl-ctrl-top-left { margin-top: calc(var(--mobile-header-height) + 66px + var(--elev-slot)); }
  body.route-actions-on :where(#map) .maplibregl-ctrl-top-left { margin-top: calc(var(--route-actions-bottom, 163px) + 8px + var(--elev-slot, 30px)); }
  /* While the route card is up the tiles step aside: no room kept for them. */
  body.route-card-open.route-hud-on :where(#map) .maplibregl-ctrl-top-left,
  body.route-card-open.route-actions-on :where(#map) .maplibregl-ctrl-top-left { margin-top: calc(var(--mobile-header-height) + 12px + var(--elev-slot)); }
  /* Nav and the dash are full-screen views: no search bar over them (the old
     header had this rule; the overlays alone left MapChrome.topBar() saying
     it was on screen). */
  body.nav-active #mobile-search, body.dashboard-active #mobile-search { display: none !important; }

  /* The right edge: Layers, Rig, Nav, Locate. Round and 48 px, with no words
     (each keeps its title, which names it to a screen reader). Nav shows with
     no route too: there it opens "where to?" (js/nav-destination.js). */
  #btn-layers { display: flex; }
  #map-controls { gap: 10px; }
  #map-controls .map-btn {
    width: 48px; height: 48px; padding: 0;
    justify-content: center; border-radius: 50%; font-size: 1.05rem;
  }
  #map-controls .map-btn-label { display: none; }
  #btn-layers { order: 1; }
  /* The compass (js/map-chrome.js _wireCompass): first in the column, above
     Layers, only while the map is turned or tilted. 48 px round like the
     Topo/Sat and 3D buttons and the rest of the column.
     MapLibre's own compass (top left in 3D) is hidden on a phone. */
  #btn-compass.is-on { display: flex; order: 0; width: 48px; height: 48px; }
  #btn-compass .compass-needle { display: block; transition: transform 0.12s linear; }
  :where(#map) .maplibregl-ctrl-top-left .maplibregl-ctrl-compass,
  :where(#map) .maplibregl-ctrl-top-left .maplibregl-ctrl-group:has(> .maplibregl-ctrl-compass:only-child) { display: none; }
  #btn-vehicle { order: 2; }
  #btn-nav { order: 3; }
  #btn-locate { order: 4; }
  /* Off the map on a phone: Dash, Reset view and Full screen are in the menu's
     Map tools, the Rain chip stands in for Radar, and the route keeps its own
     Clear at the bottom of the map (#route-clear-btn). */
  #btn-radar, #btn-reset, #btn-fullscreen, #btn-clear-route { display: none !important; }
  /* Dash is back on the map, a fifth button on the right edge after Locate
     (Josh, 2026-11-03: "back on the map"; two taps into the menu is too deep
     for a speedo). It is in the menu's Map tools as well. */
  #btn-dash-moto { order: 5; }

  /* Topo/Sat, 2D/3D and the height slider, bottom left (Josh, 2026-11-01, S10:
     "the 3d height slider is too far up the page, it covers the map. re-work
     the sat/topo/2d/3d buttons and slider to be more elegant"). Two round
     48 px buttons in a column, 12 px from the edges plus the safe areas (the
     iPhone's home strip, a notch on a phone on its side). Topo/Sat on top is
     a picture of the OTHER map, Google Maps style: one tap goes there. 3D
     below it is lit while 3D is on. The height slider that slid out beside
     it is gone (Josh, 2026-11-01: "lock it at 1.15, remove the slider").
     One size and one origin place both, so they cannot land on each other.
     The route card's mini bar lifts them (body.route-mini, above); a card
     hides them (body.sheet-open). */
  :root {
    --mvc-size: 48px;
    --mvc-x: calc(12px + env(safe-area-inset-left, 0px));
    --mvc-y: calc(12px + env(safe-area-inset-bottom, 0px));
  }
  #basemap-switch, #mode-3d-switch { left: var(--mvc-x); }
  #mode-3d-switch { bottom: var(--mvc-y); }
  #basemap-switch { bottom: calc(var(--mvc-y) + var(--mvc-size) + 10px); }
  /* The right edge's round buttons, 4 px bigger. */
  #basemap-toggle, #mode-3d-toggle {
    position: relative; overflow: hidden;
    width: var(--mvc-size); height: var(--mvc-size); min-height: 0; padding: 0;
    justify-content: center; border-radius: 50%;
    background: var(--bg-secondary); border: 1px solid var(--bg-tertiary);
    color: var(--text-primary); box-shadow: var(--shadow);
  }
  #basemap-toggle > i, #basemap-toggle > span,
  #mode-3d-toggle > i, #mode-3d-toggle > span { display: none; }
  /* The picture alone, cropped round inside a thin white ring (no word on
     it: Josh, 2026-11-01, S10). Its aria-label names what a tap does. */
  #basemap-toggle { border: 2px solid rgba(255, 255, 255, 0.92); background: var(--bg-tertiary); }
  #basemap-toggle .map-toggle-pic {
    display: block; position: absolute; top: 0; left: 0;
    width: 100%; height: 100%; object-fit: cover; pointer-events: none;
  }
  #mode-3d-toggle::after { content: '3D'; font-size: max(15px, 0.8rem); font-weight: 800; letter-spacing: 0.5px; }
  #mode-3d-toggle[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #00120a; }
  /* The tilt and zoom pad: off on a phone until the rider turns it on in Map
     tools (Josh, 2026-11-01, answer 2; js/map-chrome.js sets body.ptz-on). */
  #map-ptz { display: none; }
  body.ptz-on:not(.nav-active):not(.dashboard-active) #map-ptz { display: flex; }
  /* With the pad off nothing sits bottom-right but the scale ruler: it lines
     up flush with the button column's right edge (12 px) and is not slid by
     half its width, so at its widest (110 px) it stays on the screen. With the
     pad on it is centred under the pad again (tests/ruler-centred.test.js). */
  body:not(.ptz-on) .maplibregl-ctrl-bottom-right .maplibregl-ctrl-scale { margin: 0 12px 0 0 !important; transform: none; }

  /* Map tools in the menu: Dash, Reset view, Full screen and the tilt and zoom
     switch, one row, icon over word. */
  #map-tools { display: block; }
  .map-tools-row { display: flex; gap: 8px; }
  .map-tools-row button {
    flex: 1 1 0; min-width: 0; min-height: 60px;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
    padding: 6px 4px;
    background: var(--bg-tertiary); border: 1px solid transparent; border-radius: var(--radius);
    color: var(--text-primary); font: inherit; font-size: max(0.66rem, var(--text-floor, 0px)); font-weight: 600; line-height: 1.15;
    text-align: center; cursor: pointer;
  }
  .map-tools-row button i { font-size: 1rem; color: var(--accent); }
  #menu-ptz[aria-pressed="true"] { background: var(--accent); color: #00120a; }
  #menu-ptz[aria-pressed="true"] i { color: #00120a; }

  /* The layers card (phone redesign step 4): a sheet like #map-sheet (js/sheet.js
     sizes it through --sheet-h), opened by the Layers button. */
  #layers-sheet {
    display: block; position: fixed;
    top: auto; bottom: 0; left: 0; right: 0; width: 100%;
    height: var(--sheet-h, 0px);
    background: var(--bg-secondary); color: var(--text-primary);
    border-top: 1px solid var(--bg-tertiary); border-radius: 16px 16px 0 0;
    box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.42);
    z-index: 600;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    overflow: hidden; overscroll-behavior: contain; touch-action: pan-x;
    transition: height 0.22s ease, transform 0.22s ease;
  }
  #layers-sheet.sheet-full { overflow-y: auto; touch-action: auto; }
  #layers-sheet.sheet-dragging { transition: none; }
  @media (prefers-reduced-motion: reduce) { #layers-sheet { transition: none; } }
  #layers-sheet.hidden { transform: translateY(100%); pointer-events: none; }
  body.nav-active #layers-sheet, body.dashboard-active #layers-sheet { display: none !important; }
  #layers-sheet .sheet-close {
    position: sticky; top: 22px; float: right; z-index: 3;
    width: 48px; height: 48px; margin: 0 8px -48px 0; border-radius: 50%;
    border: none; cursor: pointer; background: var(--bg-tertiary); color: var(--text-secondary);
  }
  /* 26 px down from the handle's own 22 px: its 48 px tap area (.sheet-handle::after)
     covered the top of the Topo and Satellite pictures, and a tap there pulled the card. */
  #layers-sheet .sheet-content { padding: 28px 18px 14px; }
  /* The two pictures leave the X its corner. */
  #layers-sheet .lc-maps { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-right: 52px; }
  #layers-sheet .lc-map {
    position: relative; height: 64px; padding: 0; overflow: hidden; cursor: pointer;
    background: var(--bg-tertiary); border: 2px solid var(--bg-tertiary); border-radius: 12px;
  }
  #layers-sheet .lc-map img { display: block; width: 100%; height: 100%; object-fit: cover; }
  #layers-sheet .lc-map span {
    position: absolute; left: 8px; bottom: 5px;
    color: #fff; font-size: max(0.72rem, var(--text-floor, 0px)); font-weight: 700; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
  }
  #layers-sheet .lc-map[aria-pressed="true"] { border-color: var(--accent); }
  #layers-sheet .lc-h {
    margin: 14px 0 8px; font-size: max(0.62rem, var(--text-floor, 0px)); font-weight: 700;
    letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted);
  }
  #layers-sheet .lc-chips { display: grid; grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); gap: 6px; }
  #layers-sheet .lc-chip {
    min-height: 56px; padding: 4px 2px;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
    background: var(--bg-primary); border: 1px solid var(--bg-tertiary); border-radius: 12px;
    color: var(--text-secondary); font: inherit; font-size: max(0.64rem, var(--text-floor, 0px)); font-weight: 700; cursor: pointer;
  }
  #layers-sheet .lc-chip i { font-size: 1rem; }
  #layers-sheet .lc-chip[aria-pressed="true"] { background: var(--accent-dim); border-color: var(--accent); color: var(--text-primary); }
  #layers-sheet .lc-chip[aria-pressed="true"] i { color: var(--accent); }
  #layers-sheet .lc-more {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    width: 100%; min-height: 48px; margin-top: 10px; padding: 0 4px;
    background: none; border: none; color: var(--accent);
    font: inherit; font-size: 0.8rem; font-weight: 700; cursor: pointer;
  }
}

/* A phone on its side is about 300-360 px tall, and a column of four buttons
   would run into the pills and the pad, so the four line up in a row at the
   top right beside a shorter search bar. Josh, 2026-11-01: Nav, Locate,
   Layers and Rig stay in reach on a phone on its side. (Before the redesign
   this block HID the column, because the old header kept Nav, Dash and
   Locate.) Not width-gated: an S23 in landscape is ~780px wide and fails
   (max-width: 768px), see js/elevation-hud.js. The LAST #map-controls rule
   in the file (tests/mobile-ui-controls.test.js). */
@media (pointer: coarse) and (orientation: landscape) and (max-height: 500px) {
  #map-controls { flex-direction: row; top: calc(env(safe-area-inset-top, 0px) + 12px); right: calc(12px + env(safe-area-inset-right, 0px)); gap: 8px; }
  body.inapp-shifted #map-controls { top: calc(var(--inapp-bar-h, 64px) + 12px); }
  /* The compass sits under the row's right end, so the row (and the search
     bar sized beside it) keeps its width while it comes and goes. */
  #btn-compass.is-on { position: absolute; top: calc(100% + 8px); right: -2px; margin: 0; }
  /* The bar is sized by width, not by counting the buttons beside it, so a
     fifth right-edge button (Dash, if Josh asks) is a one-line change. Both
     clear a notch or camera cutout on a phone on its side (phone-ux-s34 graft). */
  #mobile-search { left: calc(12px + env(safe-area-inset-left, 0px)); right: auto; width: min(420px, calc(100vw - 280px)); }
}

/* ===== Phone targets: 48 px at least (the in-between size, 2026-11-01) =====
   Older hands tap less accurately on small targets (Leitao & Silva 2012), so
   every control a phone shows is 48 px in its short side: the menu's tabs, its
   tour and guide buttons, its X, the sign-in fields and buttons, the search
   field, a link alone on its line. Last in the file so it wins over the
   desktop rules it lifts; links inside a sentence keep their size. */
@media (max-width: 768px) {
  #sidebar-close { min-width: 48px; min-height: 48px; }
  .tab-btn, #open-tour, #open-guide, #report-problem { min-height: 48px; }
  .search-box input, #auth-email, #auth-password { min-height: 48px; }
  #auth-form .btn, #auth-form button { min-height: 48px; }
  #fb-page-link { display: inline-flex; align-items: center; min-height: 48px; }
  /* Menu > Route's controls and the route card's lower buttons (Export GPX,
     Download PDF, Share this trip), rem-sized and 10% smaller on the 18 px
     root (phone-ux-final review). Not yet the Rig menu: eight 48 px rows are
     about 410 px, and tour v3's card then covers the list at 384 x 668. */
  #plan-route, #add-waypoint, #clear-route, #save-route, #export-gpx, #save-home,
  .route-point input, #route-date, #route-sheet .btn { min-height: 48px; }
  .route-locate-btn, .route-night-btn { min-width: 48px; min-height: 48px; }
  /* Built with inline 44 px heights (the tour card, the new-look offer, the
     done card, the launch cards, "Start from here?"): lifted here. */
  #onboard-card button, #onboard-offer button, #onboard-done button,
  #install-card button, #share-card button, #fb-follow-card button, #fb-follow-card a,
  #route-start-sheet button { min-height: 48px !important; }
  #onboard-x { min-width: 48px !important; }
  /* Whole words in Route options' vehicle picker: "Motorhome" broke as
     "Motorhom" / "e" at 360 px on the 14 px floor. 1 px sides leave 72 px. */
  .route-vehicle-btn { padding: 7px 1px; }
  /* hyphens: a German compound wider than its tile ("Straßenmotorrad",
     "Reiseenduro") ran into the next tile at 390 px; it now breaks at a
     syllable with a hyphen. html lang is set by I18N, so the right language's
     rules apply; English words that fit stay whole. */
  .route-vehicle-btn span { overflow-wrap: normal; -webkit-hyphens: auto; hyphens: auto; }
}

/* Plan with AI: "Which are you taking?" with the rider's own vehicles, and "Planning for <vehicle> · change" (js/ai-route.js). */
.ai-vehicle-choices { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.ai-vehicle-choice { display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px solid var(--bg-tertiary); border-radius: var(--radius); background: var(--bg-card); color: var(--text-primary); cursor: pointer; font: inherit; }
.ai-vehicle-choice[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
.ai-vehicle-line { font-size: 0.85rem; color: var(--text-secondary); padding: 2px 2px 0; }
.ai-vehicle-change { background: none; border: 0; padding: 0; color: var(--accent); text-decoration: underline; cursor: pointer; font: inherit; }

/* Far-off turn pill, phone upright: it sits in the turn card's own row, just
   under the temperature and wind pills, so all three show at once. It used to
   sit up beside the ETA box and hide temperature and wind while it showed
   (an AUS rider, 2026-11-05: "We miss the temp and wind speed ... Can the
   notification of turn outside 3KM just be lowered so we can see the Temp
   and wind"). The pill is one line, so the row costs the map about 40px. */
@media (max-width: 599px) and (orientation: portrait) {
  #nav-overlay:not(.hidden) > #nav-turn.turn-far { grid-column: 1 / -1; grid-row: 2; justify-self: center; align-self: start; margin: 0 0 10px 0; padding: 3px 10px 3px 4px; }
  #nav-overlay:not(.hidden) > #nav-turn.turn-far .nav-turn-far-road { max-width: 60vw; }
}

/* The account panel's "Towing a caravan or trailer" tick (js/auth.js
   #acct-tow-row), as AUS. */
.acct-vehicle-tow {
  display: flex; align-items: center; gap: 7px;
  margin-top: 8px;
  font-size: max(0.76rem, var(--text-floor, 0px));
  cursor: pointer;
}
.acct-vehicle-tow[hidden] { display: none; }
