:root {
  --bg: #14110e;
  --panel: #1d1812;
  --panel2: #241d15;
  --line: #3a2f22;
  --line2: #4c3f2c;
  --text: #ece3d4;
  --text-dim: #a89a86;
  --text-faint: #7a6d5c;
  --accent: #e8b13f;
  --accent-ink: #1a1410;
  --bad: #d9635a;
  --good: #6fbf8f;
  --accent-bg: #3a2f16;
  --accent-bd: #6b551f;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  /* Bottom-sheet heights (task: "roughly the lower third to half, not the
     whole viewport"); app.js's SHEET_PEEK_VH/SHEET_FULL_VH/SHEET_TOP_GAP_PX
     mirror these numbers to compute map padding without measuring the DOM
     mid-transition - keep them in sync if any of these change.
     --sheet-top-gap is the strip the sheet must never climb into: the top
     bar (8px + 40px) plus breathing room, so the filters and layers buttons
     stay reachable with the sheet fully expanded. On a short screen (a phone
     in landscape) 86vh alone would swallow them. */
  --sheet-top-gap: calc(64px + var(--safe-top));
  --sheet-peek: 44vh;
  --sheet-full: min(94vh, calc(100dvh + var(--app-shortfall) - var(--sheet-top-gap)));

  /* Saved to a home screen this runs edge to edge under the status bar and
     the home indicator (index.html's viewport-fit=cover). iOS 26 and later
     draw their own blurred material over the status bar strip whatever
     apple-mobile-web-app-status-bar-style says, so anything placed at a
     literal top:10px is UNDER it - which is exactly how the top bar, the
     profile toggle and the status banner ended up behind a blur on an
     iPhone. Every top-anchored control is offset from these two instead of
     from the viewport edge; the map itself still runs full-bleed beneath
     them, which is the point of covering the notch in the first place.
     env() is 0 in a normal browser tab, so these collapse to the old
     numbers on a desktop. */
  --safe-top: env(safe-area-inset-top, 0px);
  --bar-top: calc(var(--safe-top) + 18px);   /* the top bar's own offset */
  --below-bar: calc(var(--safe-top) + 66px); /* everything that sits under it */

  /* How far the page falls short of the screen, measured by app.js's
     syncAppHeight and 0 everywhere except an iOS 26/27 home-screen app -
     see that function for why the viewport is short in the first place. */
  --app-shortfall: 0px;
}

* { box-sizing: border-box; }
/* dvh, not vh: the dynamic viewport is the one that matches what is actually
   on screen when the OS insets a web app. Plus the shortfall, because on iOS
   26/27 even the dynamic viewport is short of the screen. */
html, body { margin: 0; height: 100%; height: calc(100dvh + var(--app-shortfall)); }
body {
  font-family: var(--font);
  font-size: 14px;
  color: var(--text);
  background: var(--bg);
  overflow: hidden;
  -webkit-text-size-adjust: 100%;
}
button, select, input { font: inherit; color: inherit; }
button { cursor: pointer; }

/* fixed, and deliberately hanging below the viewport by the shortfall: the
   viewport itself ends above the bottom of the screen on an iOS home-screen
   app, so pinning to it is not enough - the map has to run past it. */
#map { position: fixed; top: 0; left: 0; right: 0; bottom: calc(-1 * var(--app-shortfall)); }

/* ---------------------------------------------------------------- top bar */

#topBar {
  position: absolute;
  top: var(--bar-top);
  left: 10px;
  right: 10px;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 8px;
}
#pickerHost { flex: 1; min-width: 0; }

/* Wordmark plus the profile toggle, in one panel-coloured bar - the
   application's identity and its single most consequential control sitting
   together, since changing profile rescores every spot on the map. */
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  width: fit-content;
  max-width: 100%;
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 8px;
  padding: 4px 4px 4px 12px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
}
/* The wordmark is an <img>, so it cannot inherit currentColor - the file
   carries its own fill in this palette's --text. Sized by height with width
   auto: the mark is 8.2:1, and pinning the height is what keeps it on the
   same optical line as the toggle beside it whatever the viewport does.
   width/height on the tag itself reserve the box so the bar does not reflow
   when the SVG lands. */
.brand__mark {
  flex: none;
  display: block;
  height: 15px;
  width: auto;
}
.ptoggle {
  display: flex;
  flex: none;
  border: 1px solid var(--line2);
  border-radius: 6px;
  overflow: hidden;
}
.ptoggle__btn {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 10px;
  background: transparent;
  border: 0;
  border-right: 1px solid var(--line2);
  color: var(--text-faint);
}
.ptoggle__btn:last-child { border-right: 0; }
.ptoggle__btn:hover { background: var(--panel2); color: var(--text); }
.ptoggle__btn[aria-pressed="true"] { background: var(--accent); color: var(--accent-ink); }
.ptoggle__btn svg {
  width: 19px;
  height: 19px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ptoggle__lab { font-size: 12px; font-weight: 600; }
.picker {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 8px;
  padding: 6px 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
}
.picker__label {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  flex: none;
}
.picker select {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
}
/* .rb: the floating square icon button - the layers control's own trigger
   today, matching the static map's build_map.py .rb (the same restyle-away-
   from-defaults treatment this file already gives MapLibre's own controls
   below). */
.rb {
  flex: none;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 8px;
  color: var(--text);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
}
.rb:hover { background: var(--panel2); }
.rb[aria-expanded="true"] { background: var(--accent-bg); border-color: var(--accent-bd); color: var(--accent); }
.rb svg { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
/* `display: flex` above outranks the UA stylesheet's own [hidden] rule, so
   a .rb with the hidden attribute would otherwise stay on screen - the same
   trap #cardHost fell into. */
.rb[hidden] { display: none; }
/* The needle turns with the map; only its rotation animates, so tapping it
   does not make the button itself move. */
.rb--compass svg { transition: transform 0.2s linear; }
@media (prefers-reduced-motion: reduce) {
  .rb--compass svg { transition: none; }
}

/* ------------------------------------------------------------ locate rail */

/* Under the top bar on the right rather than the static map's bottom-right
   corner: at the bottom it would sit under the card sheet on a phone, and
   "where am I" is not a control you want to have to close something to
   reach. */
#locRail {
  position: absolute;
  top: var(--below-bar);
  right: 10px;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-end;
}
/* Following is the one state of this button that persists while the rider is
   not looking at it, so it is marked the same way the layers trigger marks
   being open - and the icon changes shape as well, since colour alone is not
   a reliable signal on a phone screen in daylight. */
#locBtn.is-on { background: var(--accent-bg); border-color: var(--accent-bd); color: var(--accent); }
#locBtn.is-on svg { fill: currentColor; }
/* .rb svg sets no display, so the UA [hidden] rule reaches it - but the
   button's own .rb[hidden] rule exists because display:flex outranked it
   there, and stating this one avoids the same trap if .rb svg ever gains a
   display. */
.rb svg[hidden] { display: none; }
#locBtn.is-busy { color: var(--accent); }
#locBtn.is-busy svg { animation: locspin 1.1s linear infinite; }
#locBtn.is-bad { color: var(--bad); border-color: var(--bad); }
#folBtn[aria-pressed="true"] { background: #1d3550; border-color: #2f9bff; color: #7cc0ff; }
@keyframes locspin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  #locBtn.is-busy svg { animation: none; }
}

/* ---------------------------------------------------------------- layers panel */

#layersPanel {
  position: absolute;
  top: var(--below-bar);
  right: 10px;
  /* Above #cardHost (30): these are the controls you reach for WHILE a spot
     is open, so the card must never be able to cover them. */
  z-index: 35;
  width: 240px;
  max-width: calc(100vw - 20px);
  max-height: min(70vh, 520px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 10px;
  box-shadow: 0 3px 16px rgba(0, 0, 0, 0.55);
}
.panel-sheet__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 8px 8px 14px;
  border-bottom: 1px solid var(--line);
}
.panel-sheet__title {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}
.panel-sheet__x {
  margin-left: auto;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 20px;
  line-height: 1;
}
.panel-sheet__x:hover { color: var(--text); }
.panel-sheet__body { padding: 4px 8px 10px; }
#filtersBody { padding: 6px 12px 14px; }

.lgrp {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 12px 6px 2px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--line);
}
.lgrp:first-child { margin-top: 4px; }
.lrow {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 40px;
  padding: 0 6px;
  font-size: 13px;
  text-align: left;
  color: var(--text-dim);
  background: transparent;
  border: none;
  border-radius: 5px;
}
.lrow:hover { color: var(--text); background: var(--panel2); }
.lrow[aria-checked="true"] { color: var(--text); }
/* Exclusive choice (base map) reads as a radio dot; independent choice
   (an overlay) reads as a checkbox tick - a real distinction, not a
   styling flourish (matches build_map.py's own .rad/.chk convention). */
.rad {
  width: 16px;
  height: 16px;
  flex: none;
  position: relative;
  border: 1.5px solid var(--line2);
  border-radius: 50%;
}
.lrow[aria-checked="true"] .rad { border-color: var(--accent); }
.lrow[aria-checked="true"] .rad::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--accent);
}
.chk {
  width: 16px;
  height: 16px;
  flex: none;
  position: relative;
  border: 1.5px solid var(--line2);
  border-radius: 3px;
}
.lrow[aria-checked="true"] .chk { background: var(--accent); border-color: var(--accent); }
.lrow[aria-checked="true"] .chk::after {
  content: "";
  position: absolute;
  left: 4.5px;
  top: 1px;
  width: 4px;
  height: 8px;
  border: solid var(--accent-ink);
  border-width: 0 2px 2px 0;
  transform: rotate(42deg);
}

/* ---------------------------------------------------------------- status stack
   Small, left-aligned chips - never a full-width bar (coordinator
   feedback: match out/map.html, which has no full-width banner bars; "200
   spots loaded" was pure noise once the spots are visibly on the map). Each
   chip sizes to its own content (align-items: flex-start), and a transient
   status disappears entirely on success rather than turning into an
   announcement - setTileStatus("") in app.js. */

/* Right margin leaves the locate rail its 40px column: a status line and a
   button competing for the same pixels is how the layers button ended up
   under the zoom control. */
#statusStack {
  position: absolute;
  top: var(--below-bar);
  left: 10px;
  right: 60px;
  z-index: 15;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  pointer-events: none;
}
#statusStack > * { pointer-events: auto; }

.installbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 6px 8px 6px 12px;
  border-radius: 999px;
  font-size: 12px;
  background: var(--panel);
  border: 1px solid var(--accent-bd);
  color: var(--text);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  max-width: 100%;
}
.installbar[hidden] { display: none; }
.installbar__text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.installbar__yes {
  flex: none;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--accent-bd);
  background: var(--accent-bg);
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
}
.installbar__no {
  flex: none;
  padding: 4px 6px;
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 12px;
}

.tile-status {
  margin: 0;
  padding: 5px 10px;
  border-radius: 999px;
  font-size: 11.5px;
  background: var(--panel);
  border: 1px solid var(--line2);
  color: var(--text-dim);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.tile-status--building { color: var(--accent); border-color: var(--accent-bd); }
.tile-status--failed { color: var(--bad); border-color: #6b3530; }

/* The degradation banner is a real warning (spec 7.3.1's rung >= 2), not
   routine chrome - it keeps a background fill, but held to the same "one
   line, the rest behind a tap" standard as everything else here
   (controls.js's renderDegradationBanner already puts dropped_reqs behind
   a <details>/<summary>, never inline). */
.banner {
  margin: 0;
  max-width: min(360px, calc(100vw - 20px));
  padding: 7px 10px;
  border-radius: 6px;
  font-size: 12px;
  line-height: 1.4;
  background: #2b2410;
  border: 1px solid var(--accent-bd);
  color: #f0d999;
}
.banner--severe {
  background: #2e1a16;
  border-color: #7a3a33;
  color: #f0b0a8;
  font-weight: 600;
}
.banner__text { margin: 0; }
.banner__details { margin-top: 4px; font-weight: 400; }
.banner__details summary { cursor: pointer; font-size: 11px; color: inherit; opacity: 0.85; }
.banner__details ul { margin: 4px 0 0; padding-left: 18px; font-size: 11px; }

/* The hidden-count line (spec 9: "hidden counts always visible with a
   one-tap escape"), collapsed to one small chip - tapping it acts (fetches
   more, app.js's showMoreSpots), it does not open an explanation. Renders
   nothing at all when the cap truncated nothing (controls.js). */
.capline-btn {
  margin: 0;
  padding: 5px 10px;
  border-radius: 999px;
  font-size: 11.5px;
  color: var(--text-dim);
  background: var(--panel);
  border: 1px solid var(--line2);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.capline-btn:hover { background: var(--panel2); color: var(--text); border-color: var(--accent-bd); }

/* ---------------------------------------------------------------- sliders */

#sliderPanel {
  position: absolute;
  top: var(--below-bar);
  right: 10px;
  z-index: 12;
  width: 210px;
  max-width: calc(100vw - 20px);
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 8px;
  padding: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  display: none;
}
#sliderPanel:has(#sliderHost .slider) { display: block; }
.panel__title {
  margin: 0 0 6px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}
.sliders-empty { display: none; }
.slider { display: block; margin-bottom: 8px; }
.slider__label {
  display: block;
  font-size: 11.5px;
  color: var(--text-dim);
  margin-bottom: 3px;
  text-transform: capitalize;
}
.slider input[type="range"] { width: 100%; }

/* ---------------------------------------------------------------- card */

/* The card is a bottom sheet on a phone (task 3: "the card covers
   everything" - it must not, so the map stays checkable against the
   terrain underneath). #cardHost is the fixed-position shell; --sheet-peek
   is its default open height (roughly a third to half the viewport, never
   the whole thing), .is-full raises that to --sheet-full. #cardGrab is the
   drag/tap handle (build_map.py's own .grab), #card is the independently
   scrolling content pane, so every factor is always reachable - "never
   capped" - even while the sheet itself is short. */
#cardHost {
  /* fixed and dropped past the viewport for the same reason #map is - the
     sheet's own bottom padding below grows by the same amount, so its
     content stays clear of the home indicator rather than riding down with
     the box. */
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(-1 * var(--app-shortfall));
  z-index: 30;
  display: flex;
  flex-direction: column;
  max-height: var(--sheet-peek);
  background: var(--panel);
  border-top: 1px solid var(--line2);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.55);
  transform: translateY(0);
  will-change: transform;
}
#cardHost.is-anim { transition: transform 0.22s cubic-bezier(0.22, 0.61, 0.36, 1), max-height 0.22s cubic-bezier(0.22, 0.61, 0.36, 1); }
#cardHost.is-full { max-height: var(--sheet-full); }
/* The `display: flex` above has higher specificity than the UA stylesheet's
   own `[hidden] { display: none }`, so it must be restated here explicitly
   - otherwise the hidden attribute (app.js sets it on close) stops hiding
   the sheet at all. */
#cardHost[hidden] { display: none; }

.grab {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 20px;
  flex: none;
  cursor: grab;
  touch-action: none;
}
.grab::before {
  content: "";
  width: 34px;
  height: 4px;
  border-radius: 2px;
  background: var(--line2);
}

#card {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  min-height: 0;
  /* Vertical gestures reach the sheet's own drag handler when the content is
     already at the top; the browser keeps horizontal ones. */
  touch-action: pan-y;
  padding: 4px 16px calc(16px + env(safe-area-inset-bottom, 0px) + var(--app-shortfall));
}

/* Score, place and close on one baseline; everything else moved to the row
   below it, so the head is two lines instead of four. */
.card__head {
  display: flex;
  align-items: center;
  gap: 10px;
  touch-action: none;
}
.card__score {
  flex: none;
  font-size: 27px;
  font-weight: 700;
  line-height: 1;
  min-width: 38px;
  font-variant-numeric: tabular-nums;
}
.card__place {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.card__handle {
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-faint);
}
.card__close {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 26px;
  line-height: 1;
  width: 44px;
  height: 44px;
  margin: -6px -8px -6px 0;
}
.card__close:hover { color: var(--text); }

.nearby-line, .unproven-line {
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--text-dim);
  display: flex;
  align-items: flex-start;
  gap: 6px;
}
.unproven-dot {
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: 3px;
  border-radius: 50%;
  border: 1.5px dashed var(--accent);
  background: transparent;
}

.card__section {
  margin: 16px 0 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  border-bottom: 1px solid var(--line);
  padding-bottom: 4px;
}

.factor-list { list-style: none; margin: 0; padding: 0; }
.factor {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
  font-size: 13px;
}
.factor:last-child { border-bottom: none; }
.factor__label { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* The measurement, after the factor's name: quieter, and the part that gives
   way first when the row runs out of width - the name has to survive. */
.factor__ev {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-faint);
}
.factor__impact {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.factor--good .factor__impact { color: var(--text-faint); font-weight: 500; }
.factor--bad .factor__impact { color: var(--bad); }

/* Inference vs fact: solid/filled = measured, dashed/hollow = inferred via
   declared Unknown handling - never the same visual (task instructions). */
.basistag {
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 1.5px 5px;
  border-radius: 3px;
  line-height: 1.5;
}
.basistag--unproven {
  border: 1px dashed var(--accent);
  color: var(--accent);
  background: transparent;
  font-weight: 600;
}

/* Visually hidden, still read aloud: the word a head glyph stands for. */
.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* One wrapping row under the name: three distances as glyph-and-number, then
   the yes/no facts as chips. Four stacked lines became two. */
.card__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px 10px;
  margin-top: 5px;
}
.cstat {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--text-faint);
}
.cstat__i { display: flex; }
.cstat__i svg {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.cstat b {
  font-family: var(--font-mono);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
}
.ctag {
  padding: 1.5px 7px;
  border-radius: 9px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.ctag--good { background: #123527; color: #7ad3a4; }
.ctag--maybe { background: #2b2a16; color: #cfc27a; }
.ctag--info { background: #14303f; color: #7cd0f0; }
.ctag--bad { background: #3a1c1c; color: #f0a49c; }
.ctag--dim { background: var(--panel2); color: var(--text-faint); }
/* An emoji chip is the glyph and nothing else: no letter-spacing to fight,
   and sized so it reads at the same weight as the numbers beside it. */
.ctag--emoji {
  padding: 1px 5px;
  font-size: 13px;
  line-height: 1.25;
}

/* Score, close and copy never shrink. They are the three controls a rider
   reaches for without looking, and a flex row will happily squash any of
   them to nothing to fit a long place name or a wide link row. */
.card__score, .card__close { flex: none; }
/* Copy keeps its own column in the grid, so it can never be squeezed out. */
/* The approach line: amber, matching the dashed line it describes on the
   map, and quieter than a flag - it is information, not a warning. */
.card__approach {
  margin-top: 8px;
  padding: 6px 10px;
  border-left: 2px dashed #f8ca4d;
  color: var(--text-dim);
  font-size: 11.5px;
  line-height: 1.4;
}
.card__approach[hidden] { display: none; }

.card__flag {
  margin-top: 8px;
  padding: 7px 10px;
  border: 1px solid var(--bad);
  border-radius: 6px;
  background: #33191b;
  color: #f0a49c;
  font-size: 11.5px;
  line-height: 1.4;
}

/* "Why this score" collapses: the breakdown is the answer to a question you
   only sometimes ask, and it was pushing the measurements - the numbers you
   check every time - off the first screen. The summary carries the total
   even shut. */
.card__spoiler { margin-top: 4px; }
.card__section--sum {
  display: flex;
  align-items: baseline;
  gap: 8px;
  cursor: pointer;
  list-style: none;
}
.card__section--sum::-webkit-details-marker { display: none; }
.card__section--sum::after {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin-left: 2px;
  border-right: 1.5px solid var(--text-faint);
  border-bottom: 1.5px solid var(--text-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.15s;
}
.card__spoiler[open] .card__section--sum::after { transform: rotate(-135deg) translate(-1px, -1px); }
.card__section--sum:hover { color: var(--text); }
.card__gap {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-faint);
}

/* The measurement table: one fact per line, key left, value right. */
.drows { margin-top: 4px; }
.drow {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 5px 0;
  border-bottom: 1px solid var(--line);
  font-size: 12.5px;
}
.drow:last-child { border-bottom: 0; }
/* Both sides must be able to shrink. The value used to be flex:none, which
   does not shrink and does not wrap - so a long one (a protected area's full
   name and designation) overflowed its own column and printed on top of the
   key beside it. */
.drow__k { flex: 0 1 auto; min-width: 0; color: var(--text-dim); }
.drow__v {
  flex: 1 1 auto;
  min-width: 0;
  text-align: right;
  color: var(--text);
  overflow-wrap: anywhere;
}
.v--mono { font-family: var(--font-mono); font-size: 11.5px; }
.v--dim { color: var(--text-faint); }
/* Three states, three renderings: a measured number is plain, a searched
   nothing is quieter, and an unmeasured column is quieter still and italic -
   never the same dash for the last two. */
.v--none { color: var(--text-faint); }
.v--unknown { color: var(--text-faint); font-style: italic; }
.fchip {
  margin-left: 5px;
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.fchip--wet { background: #12304a; color: #7cc0ff; }
.fchip--dry { background: #3a2a12; color: #e0954a; }

/* Actions: the three things a rider does with a spot that this map cannot
   do itself. */
/* The actions sit above the breakdown, not under the table: they are what a
   rider does next, and they were the last thing on a long scroll. Filled
   rather than outlined so they read as controls at a glance. */
/* Two rows, fixed: the heart across the top, three equal buttons under it.
   Five buttons wrapping by content width made three ragged rows that changed
   shape from spot to spot. */
.dact {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  margin-top: 12px;
}
.dact__link {
  flex: 1 1 0;
  min-width: 0;
  min-height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 10px;
  background: var(--panel2);
  border: 1px solid var(--line2);
  border-radius: 8px;
  color: var(--text);
  font-size: 12.5px;
  font-weight: 600;
  text-decoration: none;
}
.dact__link:hover { background: var(--accent-bg); border-color: var(--accent-bd); color: var(--accent); }
.dact__heart {
  grid-column: 1 / -1;
  min-height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: var(--panel2);
  border: 1px solid var(--line2);
  border-radius: 8px;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 600;
}
.dact__heart svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linejoin: round; }
.dact__heart:hover { color: #ff9aab; border-color: #6b2f3a; }
.dact__heart[aria-pressed="true"] { background: #3a1c24; border-color: #ff6b81; color: #ff9aab; }
.dact__heart[aria-pressed="true"] svg { fill: #ff6b81; stroke: #ff6b81; }

.lnote { font-size: 11px; line-height: 1.45; color: var(--text-faint); padding: 4px 0 2px; }
.lattrib { font-size: 10px; line-height: 1.5; color: var(--text-faint); padding: 4px 0 2px; }
.lattrib a { color: var(--text-dim); }

.card__meta {
  margin: 14px 0 0;
  font-size: 10.5px;
  color: var(--text-faint);
}

/* A layers row that carries a second line of detail (the loaded route's
   name and size) plus the panel's one action button. */
.lrow__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; text-align: left; }
.lrow__sub { font-size: 10.5px; color: var(--text-faint); }
.lbtn {
  display: block;
  width: 100%;
  margin-top: 8px;
  min-height: 34px;
  background: transparent;
  border: 1px solid var(--line2);
  border-radius: 6px;
  color: var(--text-dim);
  font-size: 12px;
}
.lbtn:hover { background: var(--panel2); color: var(--text); border-color: var(--accent-bd); }
.lbtn:disabled { opacity: 0.45; cursor: default; }
/* The one button in a panel that produces something, rather than changing
   what is shown. */
.lbtn--go { border-color: var(--accent-bd); color: var(--accent); font-weight: 600; margin-bottom: 4px; }
.lbtn--go:hover:not(:disabled) { background: var(--accent-bg); color: var(--accent); }

/* A downloaded-area row: the name takes the free space, two small actions
   stay fixed on the right. min-width: 0 on the name is what lets flex
   actually shrink it - without it the ellipsis rule is inert and the name
   pushes the buttons into a second line instead. */
.orow {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 40px;
  padding: 4px 6px;
}
.orow__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; text-align: left; }
.orow__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--text-dim);
}
.obtn {
  flex: none;
  min-height: 30px;
  padding: 0 8px;
  background: transparent;
  border: 1px solid var(--line2);
  border-radius: 6px;
  color: var(--text-dim);
  font-size: 11.5px;
}
.obtn:hover { background: var(--panel2); color: var(--text); border-color: var(--accent-bd); }
.obtn--danger { color: var(--bad); }
.obtn--danger:hover { background: #3a1c1c; border-color: var(--bad); color: var(--bad); }

/* The download's own progress, on the row it belongs to. Deliberately thin:
   it is a second line of the status, not a component - the number beside it
   is what actually says how far along the download is, and the bar is there
   to be read without reading. */
.orow--busy { align-items: flex-start; }
.obar {
  margin-top: 5px;
  height: 3px;
  width: 100%;
  background: var(--line2);
  border-radius: 2px;
  overflow: hidden;
}
.lnote--build { opacity: 0.45; font-variant-numeric: tabular-nums; }
.lnote--preview { color: var(--text); }
.lnote--preview strong { color: var(--accent); font-weight: 600; }
.obar__fill {
  height: 100%;
  background: var(--accent);
  border-radius: 2px;
  transition: width 240ms linear;
}

/* A note that needs to read as a caution, not just faint detail - reuses the
   accent colour rather than a one-off hex, same as the tile-status "building"
   state. */
.lnote--warn { color: var(--accent); }

/* ---------------------------------------------------------------- filters */

/* Same visual vocabulary as the static map's own filter sheet
   (build_map.py's .fgrp/.f/.seg/.countline), restated in this file's tokens
   rather than imported, so the two can drift apart deliberately. */

#filtersPanel {
  position: absolute;
  top: var(--below-bar);
  right: 10px;
  z-index: 35;
  width: 290px;
  max-width: calc(100vw - 20px);
  max-height: min(78vh, 640px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 10px;
  box-shadow: 0 3px 16px rgba(0, 0, 0, 0.55);
}

/* Same floating-sheet chrome as #filtersPanel - it shares the corner and is
   opened from within the layers sheet ("Downloaded areas…"), not from its
   own toolbar button. */
#offlinePanel {
  position: absolute;
  top: var(--below-bar);
  right: 10px;
  z-index: 35;
  width: 290px;
  max-width: calc(100vw - 20px);
  max-height: min(78vh, 640px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 10px;
  box-shadow: 0 3px 16px rgba(0, 0, 0, 0.55);
}

/* The active-filter count on the toolbar button: with the sheet closed it is
   the only thing saying the map is showing a subset. */
.rb { position: relative; }
.rb__badge {
  position: absolute;
  top: -5px;
  right: -5px;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 10.5px;
  font-weight: 700;
  line-height: 17px;
  text-align: center;
}

.countline {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 0 8px;
}
.countline .num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.countline b { font-size: 21px; font-weight: 600; color: var(--accent); }
.countline > span { font-size: 12px; color: var(--text-faint); }
.countline button {
  margin-left: auto;
  min-height: 32px;
  padding: 0 10px;
  background: transparent;
  border: 1px solid var(--line2);
  border-radius: 6px;
  color: var(--text-dim);
  font-size: 12px;
}
.countline button:hover { background: var(--panel2); color: var(--text); }

.countnote {
  margin: 0 0 6px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-faint);
}

.fgrp {
  margin: 14px 0 2px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--line);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.f__top { display: flex; align-items: baseline; gap: 8px; padding-top: 6px; }
.f__top label, .f__top > span:first-child { flex: 1; color: var(--text-dim); font-size: 12.5px; }
.f__v { flex: none; font-family: var(--font-mono); font-size: 13px; color: var(--text-faint); }
.f--on .f__v { color: var(--accent); }
.f--on .f__top label, .f--on .f__top > span:first-child { color: var(--text); }

#filtersBody input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 34px;
  background: transparent;
  margin: 0;
  display: block;
}
#filtersBody input[type="range"]::-webkit-slider-runnable-track { height: 3px; background: var(--line2); border-radius: 2px; }
#filtersBody input[type="range"]::-moz-range-track { height: 3px; background: var(--line2); border-radius: 2px; }
#filtersBody input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  margin-top: -7.5px;
  border-radius: 50%;
  background: var(--text);
  border: 3px solid var(--panel);
  box-shadow: 0 0 0 1px var(--line2);
  cursor: pointer;
}
#filtersBody input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--text);
  border: 3px solid var(--panel);
  box-shadow: 0 0 0 1px var(--line2);
  cursor: pointer;
}
.f--on input[type="range"]::-webkit-slider-thumb { background: var(--accent); box-shadow: 0 0 0 1px var(--accent-bd); }
.f--on input[type="range"]::-moz-range-thumb { background: var(--accent); box-shadow: 0 0 0 1px var(--accent-bd); }

.seg {
  display: flex;
  width: 100%;
  margin-top: 4px;
  border: 1px solid var(--line2);
  border-radius: 6px;
  overflow: hidden;
}
.seg button {
  flex: 1;
  min-height: 34px;
  padding: 0 4px;
  background: transparent;
  border: 0;
  border-right: 1px solid var(--line2);
  color: var(--text-faint);
  font-size: 11.5px;
}
.seg button:last-child { border-right: 0; }
.seg button:hover { background: var(--panel2); color: var(--text); }
.seg button[aria-pressed="true"] { background: var(--accent); color: var(--accent-ink); font-weight: 600; }

/* ---------------------------------------------------------------- MapLibre chrome */

/* The attribution widget is gone from the map (app.js sets
   attributionControl: false); credit lives in the layers panel. This stays as
   a guard in case a control ever re-adds one. */
.maplibregl-ctrl-attrib { font-size: 10px; }
/* Popups are for map context - a picnic table, a hut - never for a scored
   spot, which gets the card sheet instead. The blanket display:none this rule
   used to be also killed the POI tooltips, so it is scoped to the default
   class and the one we actually use is styled below. */
.maplibregl-popup:not(.poipopup) { display: none; }
.poipopup .maplibregl-popup-content {
  padding: 7px 11px;
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 8px;
  box-shadow: 0 3px 16px rgba(0, 0, 0, 0.55);
  color: var(--text);
}
.poipopup .maplibregl-popup-tip { display: none; }
.poipop { display: flex; align-items: center; gap: 8px; }
.poipop__dot { flex: none; width: 9px; height: 9px; border-radius: 2px; }
.poipop__text { display: flex; flex-direction: column; line-height: 1.3; }
.poipop__text b { font-size: 12.5px; font-weight: 600; }
.poipop__text i { font-size: 10.5px; font-style: normal; color: var(--text-faint); }

/* Restyle MapLibre's own zoom control away from its light-theme default,
   the same "controls restyled away from defaults" treatment build_map.py
   gives Leaflet's zoom widget. */
.maplibregl-ctrl-group {
  background: var(--panel) !important;
  border: 1px solid var(--line2) !important;
  border-radius: 8px !important;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45) !important;
  overflow: hidden;
}
.maplibregl-ctrl-group button {
  background: var(--panel) !important;
  width: 34px !important;
  height: 34px !important;
}
.maplibregl-ctrl-group button:hover { background: var(--panel2) !important; }
.maplibregl-ctrl-group button:not(:last-child) { border-bottom: 1px solid var(--line) !important; }
.maplibregl-ctrl-icon { filter: invert(0.9) brightness(1.3); }

/* ---------------------------------------------------------------- >= 700px: room for a side panel instead of a full-width sheet */

@media (min-width: 700px) {
  /* The side panel starts BELOW the top bar rather than at the top of the
     viewport: at top:10px it sat exactly on the filters and layers buttons,
     and with a higher z-index than either panel it covered them even when
     they were open. */
  #cardHost {
    left: auto;
    right: 10px;
    width: 380px;
    max-height: calc(100dvh + var(--app-shortfall) - var(--below-bar) - 10px) !important;
    top: var(--below-bar);
    bottom: 10px;
    border-radius: 12px;
    border: 1px solid var(--line2);
    transform: none !important;
  }
  /* A side panel already shows everything in one scrolling column at
     nearly full height - the phone's peek/full toggle has nothing to do
     here, so its handle is hidden rather than left inert. */
  #cardGrab { display: none; }
  #card { padding-top: 14px; }
  #sliderPanel { top: var(--below-bar); }
}

/* ---------------------------------------------------------------- narrow phones: verified at 390px */

@media (max-width: 420px) {
  /* The chrome tightens up at this width; the status-bar strip above it does
     not, so the two offsets are re-stated rather than the safe area dropped. */
  :root {
    --bar-top: calc(var(--safe-top) + 16px);
    --below-bar: calc(var(--safe-top) + 60px);
  }
  #topBar { left: 8px; right: 8px; gap: 6px; }
  .picker__label { display: none; }
  .picker { padding: 5px 8px; }
  /* The pictogram is the point at this width; the word beside it is not. */
  .ptoggle__lab { display: none; }
  .ptoggle__btn { padding: 0 9px; }
  .brand { gap: 7px; padding-left: 9px; }
  .brand__mark { height: 12.5px; }
  #layersPanel { top: var(--below-bar); right: 8px; }
  #filtersPanel { top: var(--below-bar); right: 8px; left: 8px; width: auto; }
  #offlinePanel { top: var(--below-bar); right: 8px; left: 8px; width: auto; }
  #locRail { top: var(--below-bar); right: 8px; }
  #statusStack { top: var(--below-bar); left: 8px; right: 56px; }
  #sliderPanel { display: none !important; } /* no room at this width when a profile ever ships one; sliders remain reachable at >= 700px */
  .card__score { font-size: 26px; min-width: 40px; }
  .card__place { font-size: 14px; }
  #card { padding: 4px 12px calc(14px + env(safe-area-inset-bottom, 0px) + var(--app-shortfall)); }
}
