/* =============================================================================================
   Design tokens
   =============================================================================================
   Two themes, one set of names. Nothing below this block may state a colour, a size, a radius or
   a duration in literal units: a value that appears twice is a value that will drift.

   Hues are declared as channels so every tint, scrim and shadow derives from the same colour.
   A theme is then one block rather than a search-and-replace. */
:root {
  color-scheme: dark;

  /* --- palette channels ------------------------------------------------------------------- */
  --canvas-rgb: 7 26 43;
  --ink-rgb: 248 252 255;
  --primary-rgb: 247 199 107;
  --success-rgb: 49 183 177;
  --danger-rgb: 255 154 138;
  --shadow-rgb: 0 0 0;

  /* --- surfaces and text ------------------------------------------------------------------ */
  --canvas: #071a2b;
  --canvas-soft: #0d2940;
  --surface: #102f48;
  --surface-strong: #153b57;
  --text: #f8fcff;
  --body: #d6e4ee;
  --muted: #9db9c8;
  /* Two borders, because they do different jobs: `line` separates, `line-strong` is the edge of a
     control and therefore has to clear 3:1 on its own (WCAG 1.4.11). */
  --line: #28516a;
  --line-strong: #5e8098;

  /* --- roles ------------------------------------------------------------------------------- */
  --primary: #f7c76b;
  --primary-soft: #ffd989;
  --primary-strong: #dfa84a;
  --on-primary: #31240c;
  --accent: #f7c76b;
  --success: #31b7b1;
  --danger: #ff9a8a;

  /* --- derived surfaces -------------------------------------------------------------------- */
  --scrim: rgb(2 11 18 / 80%);
  --overlay: rgb(var(--canvas-rgb) / 94%);
  --overlay-strong: rgb(var(--canvas-rgb) / 96%);
  --overlay-soft: rgb(var(--canvas-rgb) / 72%);
  --veil: rgb(13 41 64 / 96%);
  --card-sheen: linear-gradient(145deg, rgb(21 59 87 / 96%), rgb(13 41 64 / 98%));
  --glow: radial-gradient(circle at 82% 3%, rgb(22 125 154 / 25%), transparent 30rem);
  --shimmer: rgb(var(--ink-rgb) / 8%);
  --ring-primary: rgb(var(--primary-rgb) / 12%);
  --tint-success: rgb(var(--success-rgb) / 10%);
  --line-success: rgb(var(--success-rgb) / 34%);
  --line-danger: rgb(var(--danger-rgb) / 45%);

  /* --- type -------------------------------------------------------------------------------- */
  --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-display: Georgia, "Times New Roman", serif;
  /* A 1.2 minor third from a 16px base. Thirteen ad-hoc sizes became nine steps, and nothing a
     traveller has to read at arm's length is below 13px. */
  --text-2xs: 12px;
  --text-xs: 13px;
  --text-sm: 15px;
  --text-md: 16px;
  --text-lg: 18px;
  --text-xl: 21px;
  --text-2xl: 26px;
  --text-3xl: 32px;
  --text-4xl: 40px;
  --leading-tight: 1.15;
  --leading-snug: 1.35;
  --leading-normal: 1.55;
  --tracking-caps: .14em;

  /* --- space ------------------------------------------------------------------------------- */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  /* --- shape and elevation ------------------------------------------------------------------ */
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 22px;
  --radius-full: 999px;
  --elev-1: 0 1px 2px rgb(var(--shadow-rgb) / 14%);
  --elev-2: 0 8px 24px rgb(var(--shadow-rgb) / 26%);
  --elev-3: 0 24px 70px rgb(var(--shadow-rgb) / 34%);

  /* The map is a lit rectangle in a dark room, and this app gets used at night in a moving car.
     Dimmed and desaturated it stops being a headlight without losing a road name. */
  --map-tone: brightness(.78) saturate(.78) contrast(1.06);

  /* --- motion ------------------------------------------------------------------------------- */
  --ease: cubic-bezier(.2, .8, .2, 1);
  --dur-1: 120ms;
  --dur-2: 200ms;
  --dur-3: 320ms;

  /* --- layout ------------------------------------------------------------------------------- */
  --max: 1120px;
  --gutter: var(--space-5);
  --control-h: 44px;
  --control-h-sm: 36px;

  /* The basemap is daylight in both themes, so its own chrome does not follow the app. */
  --map-chrome: #ffffff;
  --map-chrome-ink: #12242f;
}

/* Daylight. Placecast is used outdoors, where a dark interface is a mirror. The identity is the
   same - warm sand on a calm ground - with the sand darkened enough to hold AA on paper. */
:root[data-theme="light"] {
  color-scheme: light;

  --canvas-rgb: 246 241 232;
  --ink-rgb: 18 36 47;
  --primary-rgb: 232 163 60;
  --success-rgb: 15 111 107;
  --danger-rgb: 168 58 34;
  --shadow-rgb: 32 26 16;

  --canvas: #f6f1e8;
  --canvas-soft: #fffdf9;
  --surface: #ffffff;
  --surface-strong: #f2ece1;
  --text: #12242f;
  --body: #334c5e;
  --muted: #54697b;
  --line: #e2d9ca;
  --line-strong: #78868f;

  --primary: #e8a33c;
  --primary-soft: #f5b757;
  --primary-strong: #c9821c;
  --on-primary: #31240c;
  --accent: #8a5a10;
  --success: #0f6f6b;
  --danger: #a83a22;

  --scrim: rgb(24 20 12 / 46%);
  --overlay: rgb(255 253 249 / 96%);
  --overlay-strong: rgb(255 253 249 / 98%);
  --overlay-soft: rgb(255 253 249 / 78%);
  --veil: rgb(255 253 249 / 97%);
  --card-sheen: linear-gradient(145deg, #ffffff, #fbf7ef);
  --glow: radial-gradient(circle at 82% 3%, rgb(232 163 60 / 14%), transparent 30rem);
  --shimmer: rgb(var(--ink-rgb) / 7%);
  --elev-1: 0 1px 2px rgb(var(--shadow-rgb) / 8%);
  --elev-2: 0 8px 24px rgb(var(--shadow-rgb) / 10%);
  --elev-3: 0 20px 56px rgb(var(--shadow-rgb) / 14%);
  --map-tone: none;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  min-width: 320px;
  /* The canvas is painted from this box, so a document shorter than the window left a band of
     bare gradient below the footer - on the first screen anyone ever sees. */
  min-height: 100vh;
  min-height: 100dvh;
  overflow-x: hidden;
  color: var(--text);
  background:
    var(--glow),
    var(--canvas);
  font: 16px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button, input, select { font: inherit; }
/* Every control that does something carries a mark, so a row of buttons can be glanced at rather
   than read. The icon inherits the text colour, so it is correct in every button state for free. */
.ic { flex: none; display: block; }
button[data-icon] { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); }
.seg-btn[data-icon] { gap: var(--space-2); }
button { min-height: 44px; cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible {
  outline: 3px solid var(--primary);
  outline-offset: 3px;
}

header { max-width: var(--max); margin: auto; padding: var(--space-5) var(--space-5) var(--space-2); }
.brand { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
/* The mark carries the brand's own navy and teal, so it needs a version for each ground: the
   off-white pin at night, the navy pin on paper. One element, two sources. */
.logo {
  display: block;
  width: 40px;
  height: 53px;
  background: url("/brand/logomark-dark.png") no-repeat center / contain;
}
:root[data-theme="light"] .logo { background-image: url("/brand/logomark-light.png"); }
.brand .logo { width: 40px; height: 53px; }
.wordmark { font-size: var(--text-2xl); font-weight: 800; letter-spacing: -.04em; }
.header-actions { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }
.lang-btn {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  padding: var(--space-2) var(--space-4) var(--space-2) var(--space-3);
  color: var(--text);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.lang-btn:hover { border-color: var(--line-strong); background: var(--surface); }
.lang-globe { color: var(--accent); font-size: var(--text-sm); line-height: 1; }
.lang-caption { color: var(--muted); font-size: var(--text-2xs); font-weight: 700; letter-spacing: .04em; }
.lang-name { font-size: var(--text-xs); font-weight: 800; }
.lang-code { display: none; font-size: var(--text-xs); font-weight: 800; letter-spacing: .04em; }

/* The one shape an icon-only control may take, and only for marks that need no learning. */
.icon-btn {
  display: grid;
  place-items: center;
  width: var(--control-h-sm);
  height: var(--control-h-sm);
  min-height: var(--control-h-sm);
  padding: 0;
  color: var(--body);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.icon-btn:hover { color: var(--text); background: var(--surface); border-color: var(--line-strong); }
/* A compact control inside a thumb-sized hit area: the pill stays the size the layout wants, the
   target is the size a finger needs. MapLibre already positions a pin absolutely, so `.pc-pin` is
   deliberately left out of the `position: relative` list - setting it there detaches every marker
   from the map's origin. */
/* MapLibre already positions its credit toggle absolutely, so it needs the expander and not the
   `position: relative` - overriding that turned a 24px absolute mark into a block that pushed the
   credit onto a second line. */
.icon-btn, .seg-btn, .map-shell .map-toggle, .site-footer a { position: relative; }
.icon-btn::before, .seg-btn::before, .map-shell .map-toggle::before, .pc-pin::before, .site-footer a::before, .maplibregl-ctrl-attrib-button::before {
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100%, var(--control-h));
  height: max(100%, var(--control-h));
  transform: translate(-50%, -50%);
  content: "";
}
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

main { width: 100%; max-width: var(--max); margin: auto; padding: var(--space-4) var(--space-5) var(--space-8); }
/* The now-playing bar is fixed, so whatever it covers has to make room for it rather than hope. */
body:has(#nowbar:not([hidden])) main { padding-bottom: 120px; }
.site-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  width: 100%;
  max-width: var(--max);
  margin: auto;
  padding: var(--space-5) var(--space-5) var(--space-7);
  color: var(--muted);
  border-top: 1px solid var(--line);
  font-size: var(--text-xs);
}
.site-footer nav { display: flex; align-items: center; gap: var(--space-5); }
.site-footer a, .legal-page a { color: var(--accent); }
/* Installing is a real capability but not one anyone reaches for mid-journey, so it sits where the
   rest of the "about this app" links live rather than floating over the map. */
.footer-btn {
  min-height: var(--control-h);
  padding: var(--space-2) var(--space-4);
  color: var(--body);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 700;
}
.footer-btn:hover { color: var(--text); border-color: var(--primary); }
.legal-page { width: min(100% - var(--space-6), 760px); margin: var(--space-7) auto var(--space-8); }
.legal-page .logo { width: 44px; height: 58px; }
.legal-page h1 { margin: var(--space-6) 0 var(--space-3); font-size: clamp(var(--text-3xl), 7vw, 56px); }
.legal-page h2 { margin-top: var(--space-6); color: var(--accent); }
.legal-page p, .legal-page li { color: var(--muted); }
.legal-page strong { color: var(--text); }
.view-tabs {
  display: inline-flex;
  gap: var(--space-1);
  margin: var(--space-2) 0 var(--space-5);
  padding: var(--space-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  background: var(--overlay-soft);
  max-width: 100%;
}
.view-tab {
  min-width: 0;
  min-height: var(--control-h);
  padding: var(--space-2) var(--space-4);
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 700;
}
.view-tab.active { color: var(--canvas); background: var(--text); }
.view.active { animation: rise .35s var(--ease); }
@keyframes rise { from { opacity: 0; transform: translateY(8px); } }

/* One label style, in sentence case. Shouting a category above a heading that already said it was
   noise on every screen; what is left names a state the heading cannot. */
.eyebrow {
  margin: 0 0 var(--space-2);
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 800;
}
h1, h2, p { margin-top: 0; }
h1 {
  margin-bottom: var(--space-4);
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(32px, 5vw, 48px);
  font-weight: 400;
  letter-spacing: -.05em;
  line-height: .96;
}
h1 span { color: var(--accent); font-style: italic; }
h2 { margin-bottom: var(--space-1); font-family: Georgia, "Times New Roman", serif; font-size: var(--text-2xl); font-weight: 400; letter-spacing: -.025em; }

.card {
  min-width: 0;
  max-width: 100%;
  padding: var(--space-5);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--card-sheen);
  box-shadow: var(--elev-3);
}
.search-card { margin-bottom: var(--space-4); }
.searchbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  background: var(--canvas-soft);
  transition: border-color var(--dur-2), box-shadow var(--dur-2);
  width: 100%;
  min-width: 0;
}
.searchbar:focus-within { border-color: var(--primary); box-shadow: 0 0 0 4px var(--ring-primary); }
.search-ic { color: var(--accent); display: grid; place-items: center; }
.searchbar input {
  flex: 1;
  width: 0;
  min-width: 0;
  padding: var(--space-3) 2px;
  color: var(--text);
  background: transparent;
  border: 0;
  outline: 0;
}
.searchbar input::placeholder { color: var(--muted); }
.primary, .secondary, .search-go {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  font-weight: 800;
  transition: transform var(--dur-1) var(--ease), background var(--dur-1), border-color var(--dur-1);
}
.primary, .search-go { color: var(--on-primary); background: var(--primary); }
.primary:hover, .search-go:hover { background: var(--primary-soft); transform: translateY(-1px); }
.primary:active, .search-go:active { background: var(--primary-strong); transform: translateY(0); }
/* A control's edge is the only thing telling a traveller it is a control, so it clears 3:1 alone. */
.secondary { color: var(--text); background: transparent; border-color: var(--line-strong); }
.secondary:hover { border-color: var(--success); background: var(--tint-success); }
.secondary:active { background: var(--surface); transform: translateY(1px); }
/* A toggle that is on has to look on. Colour alone would not survive a monochrome eye, so the
   border thickens with it. */
.secondary[aria-pressed="true"] { color: var(--success); border-color: var(--success); background: var(--tint-success); box-shadow: inset 0 0 0 1px var(--success); }
/* A dimmed fill of a warm colour over a cool ground is not a disabled state, it is mud. Not-yet is
   an outline of the button it will become, and the hint beside it says what is missing. */
button:disabled {
  color: var(--muted);
  background: var(--surface);
  border-color: var(--line);
  cursor: not-allowed;
  transform: none !important;
}
button:disabled:hover { background: var(--surface); }
.wide { width: 100%; }

.search-results { display: grid; gap: var(--space-2); margin-top: var(--space-2); }
.suggest-anchor { position: relative; }
/* The suggestion list floats over the card, so a long list never pushes the map and the results
   down the page while the traveller is still choosing where to look. */
.suggest-anchor .search-results {
  position: absolute;
  z-index: 30;
  top: 0;
  right: 0;
  left: 0;
  max-height: min(52vh, 420px);
  overflow-y: auto;
  padding: var(--space-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-strong);
  box-shadow: var(--elev-3);
  scrollbar-width: thin;
}
.suggest-anchor .search-results:not(.open) { display: none; }
.search-results.pending { opacity: .72; }
.geo-heading { margin: var(--space-1) var(--space-2) var(--space-2); color: var(--muted); font-size: var(--text-xs); font-weight: 800; }
.geo-item {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 46px;
  padding: var(--space-2) var(--space-3);
  color: var(--text);
  text-align: left;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
}
.geo-item:hover, .geo-item.active { background: var(--canvas-soft); border-color: var(--primary); }
.geo-ic { color: var(--accent); display: grid; place-items: center; }
.geo-copy { display: grid; overflow: hidden; }
.geo-copy strong { overflow: hidden; font-size: var(--text-sm); text-overflow: ellipsis; white-space: nowrap; }
.geo-copy span { overflow: hidden; color: var(--muted); font-size: var(--text-2xs); text-overflow: ellipsis; white-space: nowrap; }
.geo-dist { color: var(--muted); font-size: var(--text-2xs); font-weight: 800; white-space: nowrap; }
.geo-skel { height: 46px; border-radius: var(--radius-sm); background: var(--canvas-soft); overflow: hidden; }
.geo-skel::after { display: block; width: 55%; height: 100%; background: linear-gradient(90deg, transparent, var(--shimmer), transparent); content: ""; animation: shimmer 1.2s infinite; }
.search-message { margin: var(--space-2) var(--space-3); color: var(--muted); font-size: var(--text-xs); }
.search-message.error { color: var(--danger); }

/* The two shortcuts beside the search field are the same weight as each other and share one row.
   `1 1 auto` lets them keep their labels and wrap only if a viewport really cannot hold both; the
   cap stops them stretching to half a 1070px card on the first visit, where there is no map column
   to share the width with. */
.start-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-4); }
.start-row > button { flex: 1 1 auto; max-width: 260px; justify-content: center; }
#autoStatus { margin: var(--space-2) 0 0; }

#locstatus, #autoStatus { color: var(--muted); font-size: var(--text-2xs); }
.status { color: var(--muted); font-size: var(--text-2xs); }

/* The tone is applied to the tile canvas, not the container, so pins, controls and the toggle keep
   their real colours on top of a dimmed map. */
#map .maplibregl-canvas-container, #routeMap .maplibregl-canvas-container { filter: var(--map-tone); }
.map-shell { position: relative; min-width: 0; max-width: 100%; margin: var(--space-4) 0; }
#map {
  height: 390px;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--elev-3);
}
/* The map is the fourth thing on this screen after the story, the list and the search field, so on
   a phone it shows the shape of the cluster rather than filling half the viewport. Growing it is a
   visible, labelled control on the map itself - not a floating mark somewhere else.
   Scoped to the shell so it outranks `button[data-icon]`, which is element+attribute. */
.map-shell .map-toggle {
  position: absolute;
  z-index: 2;
  top: var(--space-3);
  left: var(--space-3);
  display: none;
  min-height: var(--control-h-sm);
  padding: var(--space-2) var(--space-3);
  color: var(--text);
  background: var(--overlay);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 800;
  backdrop-filter: blur(10px);
}
.iconbtn {
  width: 44px;
  padding: 0;
  color: var(--text);
  background: var(--overlay);
  border: 1px solid var(--line);
  border-radius: 50%;
  backdrop-filter: blur(10px);
}
.maplibregl-ctrl-top-right { top: 8px; right: 8px; }
.maplibregl-ctrl-group { overflow: hidden; border-radius: var(--radius-md) !important; background: var(--map-chrome) !important; }
/* MapLibre ships 29px buttons. On the expanded phone map they are the only zoom control besides
   pinch, so they meet the same target our own controls do. */
.maplibregl-ctrl-group button { width: var(--control-h); height: var(--control-h); color: var(--map-chrome-ink); }
.maplibregl-popup-content { color: var(--map-chrome-ink); background: var(--map-chrome); border: 1px solid var(--line); border-radius: var(--radius-md); }
/* MapLibre positions a marker by writing `position: absolute` and a `transform` onto the very
   element it was handed. `position: relative` here therefore beat its rule - our stylesheet loads
   second - and every pin fell back into normal flow, stacking 26px apart. Each one was then
   translated from its own flow offset instead of the map's origin, so stop 8 sat 182 screen pixels
   below its coordinate and the whole set slid as the map zoomed. Position is MapLibre's to set. */
.pc-pin {
  width: 22px;
  height: 22px;
  border: 3px solid var(--canvas);
  border-radius: 50% 50% 50% 0;
  background: var(--primary);
  box-shadow: var(--elev-2);
  cursor: pointer;
}
.pc-pin.me { background: var(--success); }

.section-heading, .list-heading, .route-summary-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }

/* The results column is narrow by design, so its switch and hint stack rather than squeezing a
   three-way control in beside a sentence. */
.list-heading { align-items: stretch; flex-direction: column; gap: var(--space-3); margin-bottom: var(--space-3); }
.list-subheading { display: grid; gap: var(--space-3); margin-bottom: var(--space-3); }
.list-subheading .field-hint { margin: 0; }
/* A three-way switch stretched across a tablet puts its options a hand-span apart. It grows to fill
   a phone and stops there. */
.list-heading .seg, .list-subheading .seg { justify-content: stretch; max-width: 30rem; }
.list-heading .seg-btn, .list-subheading .seg-btn { flex: 1; white-space: nowrap; }
.seg { display: flex; gap: 3px; padding: 3px; border: 1px solid var(--line); border-radius: var(--radius-full); }
.seg-btn { min-height: 34px; padding: var(--space-1) var(--space-3); color: var(--muted); background: transparent; border: 0; border-radius: var(--radius-full); font-size: var(--text-2xs); font-weight: 700; }
.seg-btn.active { color: var(--canvas); background: var(--text); }
#list { display: grid; gap: var(--space-2); }
.poi {
  display: grid;
  grid-template-columns: 54px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-4);
  color: var(--text);
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}
.poi:hover { border-color: var(--primary); transform: translateX(2px); }
/* Without a photograph this was a bordered empty square, which reads as an image that failed. The
   frame appears only when there is something in it; otherwise the numeral just sets the rhythm. */
.poi-thumb {
  position: relative;
  display: grid;
  place-items: center;
  width: 54px;
  height: 54px;
  overflow: hidden;
  border-radius: var(--radius-sm);
}
.poi-thumb.has-photo { background: var(--canvas-soft); border: 1px solid var(--line); }
/* The number stays underneath, so a place with no photo still reads as an ordered list. */
.poi-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.poi-index { color: var(--accent); font: italic var(--text-md) var(--font-display); }
.poi-copy { display: grid; overflow: hidden; }
/* A name that ends in an ellipsis is a name you cannot read. Two lines is enough for every place
   in the catalogue; the one-line description underneath may still be trimmed. */
.poi-copy strong {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-height: var(--leading-snug);
}
/* Two lines, then stop. One ellipsised line turned "Palace built in the form of a high wall to
   screen the women of the court" into a sentence that never reaches its point. */
.poi-copy small { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; color: var(--muted); font-size: var(--text-2xs); line-height: var(--leading-snug); }
.dist { display: grid; gap: 1px; justify-items: end; color: var(--body); font-size: var(--text-2xs); font-weight: 800; white-space: nowrap; text-align: right; }
.dist strong { font-size: var(--text-xs); }
/* The caption is what makes the number mean something, so it is quiet but never absent. */
.dist small { color: var(--muted); font-size: var(--text-2xs); font-weight: 700; letter-spacing: .01em; }
.day-summary { margin: 0 0 var(--space-1); color: var(--muted); font-size: var(--text-2xs); line-height: 1.6; }
.day-leg {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding-left: var(--space-5);
  color: var(--success);
  font-size: var(--text-2xs);
  font-weight: 700;
}
.day-leg::before { width: 2px; height: 18px; background: var(--line); content: ""; }
.day-stop { grid-template-columns: 66px minmax(0, 1fr) auto; }
.day-time { display: grid; gap: 1px; }
.day-time strong { color: var(--accent); font-size: var(--text-xs); }
.day-time small { color: var(--muted); font-size: var(--text-2xs); }
.day-skipped { margin-top: var(--space-2); border-top: 1px solid var(--line); }
.day-skipped summary { padding: var(--space-3) 0; color: var(--muted); cursor: pointer; font-size: var(--text-2xs); }
.day-skipped p { margin: 0 0 var(--space-1); color: var(--muted); font-size: var(--text-2xs); }

.empty { display: grid; justify-items: center; gap: var(--space-2); padding: var(--space-7) var(--space-5); color: var(--muted); text-align: center; }
.empty strong { color: var(--text); font-size: var(--text-lg); }
.empty-icon { display: grid; place-items: center; color: var(--accent); }
.skel { height: 70px; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); }
.skel::after { display: block; width: 60%; height: 100%; background: linear-gradient(90deg, transparent, var(--shimmer), transparent); content: ""; animation: shimmer 1.2s infinite; }
@keyframes shimmer { from { transform: translateX(-100%); } to { transform: translateX(220%); } }

.player-card { margin-bottom: var(--space-6); }
.player-card .title { font: var(--text-3xl)/var(--leading-tight) var(--font-display); }
.audio-state { display: flex; align-items: center; gap: var(--space-2); min-height: 24px; margin-top: var(--space-2); color: var(--muted); font-size: var(--text-2xs); }
#audioStateIcon { width: 9px; height: 9px; flex: none; border: 2px solid var(--muted); border-radius: 50%; }
#audioStateIcon.checking, #audioStateIcon.loading, #audioStateIcon.generating { border-color: var(--line); border-top-color: var(--primary); animation: spin .8s linear infinite; }
#audioStateIcon.ready { border-color: var(--primary); background: var(--primary); }
#audioStateIcon.playing { border-color: var(--success); background: var(--success); box-shadow: 0 0 0 5px var(--tint-success); }
#audioStateIcon.error { border-color: var(--danger); background: var(--danger); }

/* --- Place guide: photographs, practical panel and cited facts --- */
.place-guide { margin-top: var(--space-4); }
.place-detail { display: grid; }
.pg-hero {
  position: relative;
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--canvas-soft);
}
/* A 3:2 hero at column width was 500px of photograph between the name of a place and its story.
   The picture earns its place by saying what this looks like, which it does at a third of that. */
.pg-hero-img { display: block; aspect-ratio: 16 / 7; max-height: 260px; background: var(--canvas-soft); }
.pg-hero-img img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pg-hero figcaption {
  display: grid;
  gap: 2px;
  padding: var(--space-7) var(--space-4) var(--space-3);
  background: linear-gradient(to top, var(--overlay-strong) 30%, var(--overlay-soft) 62%, transparent);
  position: absolute;
  inset: auto 0 0;
}
/* A Commons description is often a paragraph. Two lines say what the photograph is; the rest ran
   under the frame and was cut mid-word, which reads as broken rather than as abbreviated. */
.pg-caption {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  color: var(--text);
  font-size: var(--text-2xs);
  line-height: 1.45;
}
.pg-credit, .pg-credit a { color: var(--body); font-size: var(--text-2xs); line-height: 1.5; }
.pg-credit a { color: var(--success); }
/* No scrollbar: the half-visible thumbnail at the right edge already says there are more, and a
   second affordance for the same thing is a line that looks like a mistake. */
.pg-thumbs { display: flex; gap: var(--space-2); margin-top: var(--space-2); overflow-x: auto; scrollbar-width: none; }
.pg-thumbs::-webkit-scrollbar { display: none; }
.pg-thumb {
  flex: none;
  width: 66px;
  height: 50px;
  min-height: 0;
  padding: 0;
  overflow: hidden;
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  opacity: .62;
  transition: opacity var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease);
}
.pg-thumb img { width: 100%; height: 100%; object-fit: cover; }
.pg-thumb:hover, .pg-thumb.active { opacity: 1; border-color: var(--primary); }
.pg-meta { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.pg-tag {
  padding: var(--space-1) var(--space-3);
  color: var(--body);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  font-size: var(--text-2xs);
  font-weight: 700;
}
.pg-tag.rated { color: var(--accent); border-color: var(--primary-strong); }
.pg-tag.heritage { color: var(--on-success); background: var(--success); border-color: var(--success); }
.pg-tag.context { color: var(--muted); }
.pg-context-note {
  margin: var(--space-3) 0 0;
  padding: var(--space-3) var(--space-3);
  color: var(--body);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: var(--text-2xs);
  line-height: 1.6;
}
.pg-context-note strong { color: var(--accent); }
.pg-summary { margin: var(--space-3) 0 0; color: var(--body); font-size: var(--text-sm); line-height: 1.7; }
.pg-summary p { margin: 0 0 var(--space-3); }
.pg-summary p:last-child { margin-bottom: 0; }
.pg-summary:empty { display: none; }
.pg-facts { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-1) var(--space-4); margin: var(--space-4) 0 0; }
.pg-facts:empty { display: none; }
.pg-facts dt { color: var(--muted); font-size: var(--text-2xs); font-weight: 700; }
.pg-facts dd { margin: 0; color: var(--text); font-size: var(--text-xs); }
.pg-facts dd::first-letter { text-transform: uppercase; }
.pg-practical { display: grid; gap: var(--space-2); margin-top: var(--space-4); }
.pg-practical:empty { display: none; }
.pg-stat {
  display: grid;
  grid-template-columns: 108px minmax(0, 1fr);
  align-items: baseline;
  gap: 2px var(--space-3);
  padding: var(--space-3) var(--space-3);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.pg-stat-label { color: var(--muted); font-size: var(--text-2xs); font-weight: 800; }
.pg-stat-value { color: var(--text); font-size: var(--text-sm); }
.pg-stat-note { grid-column: 2; color: var(--muted); font-size: var(--text-2xs); line-height: 1.5; }
.pg-hours summary { padding: var(--space-2) 0; color: var(--muted); cursor: pointer; font-size: var(--text-2xs); }
.pg-hours ul { margin: 0 0 var(--space-2); padding-left: var(--space-4); color: var(--body); font-size: var(--text-2xs); line-height: 1.8; }
.pg-address { margin: 0; color: var(--muted); font-size: var(--text-2xs); line-height: 1.5; }
.pg-link { color: var(--success); font-size: var(--text-xs); font-weight: 700; }

.script-wrap { position: relative; margin: var(--space-4) 0; }
/* Clipped, not scrollable. A scroll region nested inside a scrolling page is a coin toss about
   which one moves, and the labelled button below is already the way to see the rest. */
#script { max-height: 220px; overflow: hidden; color: var(--body); font-size: var(--text-sm); line-height: 1.75; }
/* Fade the last line so it reads as "there is more", not as a sentence that stops mid-word. */
.script-wrap[data-overflow="true"] #script { mask-image: linear-gradient(to bottom, #000 78%, transparent); }
.script-wrap[data-overflow="true"].expanded #script { max-height: none; mask-image: none; }
.script-more {
  min-height: var(--control-h-sm);
  margin-top: var(--space-1);
  padding: var(--space-2) 0;
  color: var(--accent);
  background: none;
  border: 0;
  font-size: var(--text-xs);
  font-weight: 800;
}
.script-wrap:not([data-overflow="true"]) .script-more { display: none; }
.audioplayer { display: flex; align-items: center; gap: var(--space-4); margin: var(--space-4) 0; }
.playbtn, .nb-btn { flex: none; width: 52px; height: 52px; padding: 0; color: var(--on-primary); background: var(--primary); border: 0; border-radius: 50%; font-weight: 900; }
.seekwrap { flex: 1; min-width: 0; }
/* The track is drawn rather than left to the browser: Chrome derives an `accent-color` track from
   the accent's own luminance, which on the cream ground made the one near-black object in the
   whole light theme, inside the component that matters most. `--pct` comes from the player. */
.seek {
  width: 100%;
  height: 18px;
  padding: 0;
  appearance: none;
  background: transparent;
  cursor: pointer;
}
.seek::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: var(--radius-full);
  background: linear-gradient(to right, var(--primary) var(--pct, 0%), var(--line-strong) var(--pct, 0%));
}
.seek::-moz-range-track {
  height: 5px;
  border-radius: var(--radius-full);
  background: linear-gradient(to right, var(--primary) var(--pct, 0%), var(--line-strong) var(--pct, 0%));
}
.seek::-webkit-slider-thumb {
  width: 14px;
  height: 14px;
  margin-top: -4.5px;
  appearance: none;
  border: 0;
  border-radius: var(--radius-full);
  background: var(--primary);
}
.seek::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: 0;
  border-radius: var(--radius-full);
  background: var(--primary);
}
.times { display: flex; justify-content: space-between; color: var(--muted); font-size: var(--text-2xs); }
.prow { display: flex; gap: var(--space-2); flex-wrap: wrap; }
/* One home for lookup: facts, visiting notes and attribution. A disclosure this important has to
   read as a control, not as fine print, so it fills the width and meets the 44px target. */
.more-d { margin-top: var(--space-4); border-top: 1px solid var(--line); }
.more-d summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  min-height: var(--control-h); padding: var(--space-3) 0;
  color: var(--body); font-size: var(--text-xs); font-weight: 800;
  cursor: pointer; list-style: none;
}
.more-d summary::-webkit-details-marker { display: none; }
.more-d summary::after {
  flex: none; width: 8px; height: 8px;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur-2) var(--ease);
  content: "";
}
.more-d[open] summary::after { transform: translateY(2px) rotate(-135deg); }
.more-d summary:hover { color: var(--accent); }
.more-d > :not(summary) { margin-bottom: var(--space-4); }
.src, .src a { color: var(--success); font-size: var(--text-2xs); }

.route-grid { display: grid; gap: var(--space-4); }
.route-form { display: grid; gap: var(--space-3); align-content: start; }
/* Sentence case, like every other label. Small caps at 12px is a second reading task. */
.route-form label, .field-label { margin-top: var(--space-2); color: var(--muted); font-size: var(--text-xs); font-weight: 800; }
.field-label { margin-bottom: 0; }
/* Both hints sit under left-aligned form content, so a centred sentence was the only thing on
   either screen that did not start at the same edge as everything above it. */
.field-hint { margin: var(--space-2) 0 0; color: var(--muted); font-size: var(--text-2xs); }
.route-map-shell { grid-area: auto; margin: 0; }
#routeMap { height: 320px; }
/* Route stops read as an ordered list, so the number goes inside a plain round pin rather than as a
   badge on the rotated teardrop. */
.pc-pin[data-stop] {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-full);
}
.pc-pin[data-stop]::after {
  color: var(--canvas);
  content: attr(data-stop);
  font-size: var(--text-2xs);
  font-weight: 800;
  line-height: 1;
}
.map-unavailable { display: grid; place-items: center; padding: var(--space-5); color: var(--muted); text-align: center; font-size: var(--text-sm); }
.inline-retry { min-height: 30px; padding: 3px var(--space-3); color: var(--canvas); background: var(--primary); border: 0; border-radius: var(--radius-full); font-size: var(--text-2xs); font-weight: 800; }
.route-origin { padding: var(--space-3) var(--space-4); background: var(--tint-success); border: 1px solid var(--line-success); border-radius: var(--radius-sm); color: var(--text); font-size: var(--text-sm); }
.route-summary { min-height: 0; align-content: start; }
.offline-badge { padding: var(--space-2) var(--space-3); color: var(--muted); border: 1px solid var(--line); border-radius: var(--radius-full); font-size: var(--text-2xs); white-space: nowrap; }
.offline-badge.ready { color: var(--canvas); background: var(--success); border-color: var(--success); }
.route-metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-2); margin: var(--space-5) 0; }
.route-metrics span { display: grid; gap: 2px; color: var(--muted); font-size: var(--text-2xs); }
.route-metrics strong { color: var(--text); font: 23px Georgia, serif; }
.route-note { color: var(--muted); font-size: var(--text-xs); }
.route-loading { display: grid; place-items: center; gap: var(--space-3); min-height: 250px; color: var(--muted); text-align: center; }
.route-loading span { width: 34px; height: 34px; border: 3px solid var(--line); border-top-color: var(--primary); border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.modal, .backdrop {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--scrim);
  backdrop-filter: blur(8px);
}
.modal { display: grid; place-items: center; padding: var(--space-5); }
/* One rhythm inside every overlay, set here rather than by each child's own margins. */
.modal-card {
  position: relative;
  display: grid;
  gap: var(--space-4);
  width: min(100%, 420px);
  padding: var(--space-6) var(--space-5) var(--space-5);
  text-align: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--elev-3);
}
.modal-card > * { margin: 0; }
.modal-title + .modal-text { margin-top: calc(var(--space-4) * -1 + var(--space-2)); }
.modal-x {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: grid;
  place-items: center;
  width: var(--control-h);
  height: var(--control-h);
  padding: 0;
  color: var(--body);
  background: transparent;
  border: 0;
  border-radius: var(--radius-full);
}
.modal-x:hover { color: var(--text); background: var(--canvas-soft); }
.modal-ic { display: grid; place-items: center; color: var(--accent); }
.modal-title { padding-right: var(--space-6); font: var(--text-2xl)/var(--leading-tight) var(--font-display); }
.modal-text { color: var(--body); font-size: var(--text-sm); }
.modal-steps { padding: var(--space-4) var(--space-4) var(--space-4) var(--space-6); color: var(--muted); text-align: left; background: var(--canvas-soft); border-radius: var(--radius-sm); }
.modal-steps a { color: var(--success); word-break: break-all; }
.modal-btn { width: 100%; }
.voice-prompt {
  position: fixed;
  z-index: 120;
  right: 16px;
  bottom: 20px;
  left: 16px;
  width: min(calc(100% - 32px), 480px);
  margin: auto;
  padding: var(--space-5);
  text-align: center;
  border: 1px solid var(--primary);
  border-radius: var(--radius-lg);
  background: var(--surface-strong);
  box-shadow: var(--elev-3);
}
.voice-prompt h2 { font-size: var(--text-3xl); }
.voice-prompt > p:not(.eyebrow), .voice-prompt > span { color: var(--muted); font-size: var(--text-xs); }
.voice-actions { display: flex; justify-content: center; gap: var(--space-2); margin: var(--space-4) 0 var(--space-3); }
.voice-wave { display: flex; align-items: center; justify-content: center; gap: 3px; height: 30px; margin-bottom: var(--space-2); }
.voice-wave span { width: 4px; height: 9px; border-radius: var(--radius-sm); background: var(--primary); animation: wave .8s ease-in-out infinite alternate; }
.voice-wave span:nth-child(2) { animation-delay: .15s; }
.voice-wave span:nth-child(3) { animation-delay: .3s; }
.voice-wave span:nth-child(4) { animation-delay: .45s; }
@keyframes wave { to { height: 28px; } }

#toast {
  position: fixed;
  z-index: 150;
  bottom: 88px;
  left: 50%;
  max-width: calc(100% - 32px);
  padding: var(--space-3) var(--space-4);
  color: var(--canvas);
  background: var(--text);
  border-radius: var(--radius-full);
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, 12px);
  transition: var(--dur-2) var(--ease);
  font-size: var(--text-xs);
  font-weight: 800;
}
#toast.show { opacity: 1; transform: translate(-50%, 0); }
#netbadge { position: fixed; z-index: 140; top: 12px; right: 12px; padding: var(--space-2) var(--space-3); color: var(--canvas); background: var(--primary); border-radius: var(--radius-full); font-size: var(--text-2xs); font-weight: 800; }
/* Full width on a phone, because a 590px slab pinned to one edge of the screen reads as something
   that fell off rather than as the bar for the story you are hearing. */
#nowbar {
  position: fixed;
  z-index: 80;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom));
  background: var(--overlay);
  border-top: 1px solid var(--line);
  backdrop-filter: blur(14px);
  box-shadow: var(--elev-3);
}
.eq { display: flex; align-items: center; gap: 2px; height: 20px; }
.eq span { width: 3px; height: 6px; background: var(--primary); }
.playing .eq span { animation: wave .7s ease-in-out infinite alternate; }
.nb-title { flex: 1; overflow: hidden; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
.nb-btn { width: 42px; height: 42px; }


/* --- What else is at a stop ------------------------------------------------------------------ */
.near-panel { margin: -2px 0 2px var(--space-5); padding-left: var(--space-3); border-left: 1px solid var(--line); }
.near-panel > summary { padding: var(--space-2) 0; color: var(--muted); cursor: pointer; font-size: var(--text-2xs); font-weight: 800; letter-spacing: .04em; }
.near-panel[open] > summary { color: var(--accent); }
.near-tabs { display: inline-flex; margin-bottom: var(--space-2); }
/* Capped so an expanded stop cannot swallow the whole results column. */
.near-body { display: grid; gap: var(--space-1); max-height: 210px; overflow-y: auto; padding-bottom: var(--space-2); scrollbar-width: thin; }
.near-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  color: var(--text);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  text-decoration: none;
}
.near-row:hover { border-color: var(--primary); }
.near-copy { display: grid; overflow: hidden; }
.near-copy strong { overflow: hidden; font-size: var(--text-xs); text-overflow: ellipsis; white-space: nowrap; }
.near-copy small { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; color: var(--muted); font-size: var(--text-2xs); line-height: var(--leading-snug); }
.near-dist { color: var(--muted); font-size: var(--text-2xs); font-weight: 800; white-space: nowrap; }
.near-empty { margin: var(--space-2) 2px; color: var(--muted); font-size: var(--text-2xs); }
.near-empty.error { color: var(--danger); }

/* --- Route planning progress --------------------------------------------------------------- */
.plan-progress { display: grid; gap: var(--space-3); padding: var(--space-5) var(--space-1); }
.plan-step { display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: var(--space-3); color: var(--muted); font-size: var(--text-sm); }
.plan-dot { width: 12px; height: 12px; border: 2px solid var(--line); border-radius: 50%; }
.plan-step[data-state="active"] { color: var(--text); }
.plan-step[data-state="active"] .plan-dot { border-color: var(--line); border-top-color: var(--primary); animation: spin .8s linear infinite; }
.plan-step[data-state="done"] { color: var(--body); }
.plan-step[data-state="done"] .plan-dot { border-color: var(--success); background: var(--success); }
.plan-step[data-state="failed"] .plan-dot { border-color: var(--danger); background: var(--danger); }

/* --- Route downloads ------------------------------------------------------------------------ */
.dl-panel { display: grid; gap: var(--space-3); margin: var(--space-5) 0 var(--space-3); }
.dl-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.dl-head .eyebrow { margin: 0; }
.dl-all { min-height: 34px; padding: var(--space-1) var(--space-3); font-size: var(--text-2xs); }
/* No inner scroll region: a list that scrolls inside a page that scrolls is a coin toss about which
   one moves, and the fade cut a row in half so it read as broken rather than as "there is more".
   Eight stops is a list you scroll past, not a pane you scroll through. */
.dl-list { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
/* Four columns in 343px turned every name into "Rambagh ...". The name gets the width; where the
   stop comes up and whether it is saved read underneath it, which is also their reading order. */
.dl-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.dl-pick { display: flex; align-items: center; gap: var(--space-2); min-height: var(--control-h-sm); cursor: pointer; }
.dl-pick input { width: 20px; height: 20px; accent-color: var(--primary); }
.dl-num { color: var(--accent); font: italic var(--text-xs) var(--font-display); }
.dl-copy { display: grid; gap: 1px; overflow: hidden; }
.dl-copy strong { font-size: var(--text-sm); line-height: var(--leading-snug); }
/* Two lines, then stop. A one-line ellipsis turned "Historic military fortress in Rajasthan, India"
   into a sentence that never says where. */
.dl-copy small { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; color: var(--muted); font-size: var(--text-2xs); line-height: var(--leading-snug); }
.dl-foot { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); grid-column: 2; }
.dl-state { color: var(--muted); font-size: var(--text-2xs); font-weight: 800; white-space: nowrap; }
.dl-where { color: var(--body); font-size: var(--text-2xs); font-weight: 800; white-space: nowrap; }
.dl-row[data-state="saving"] .dl-state { color: var(--accent); }
.dl-row[data-state="saved"] { border-color: var(--line-success); }
.dl-row[data-state="saved"] .dl-state { color: var(--success); }
.dl-row[data-state="failed"] { border-color: var(--line-danger); }
.dl-row[data-state="failed"] .dl-state { color: var(--danger); cursor: pointer; text-decoration: underline; }

/* --- Language sheet ------------------------------------------------------------------------ */
.sheet-card { width: min(100%, 460px); max-height: min(86vh, 720px); padding: var(--space-5) var(--space-5) var(--space-4); text-align: left; display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); gap: var(--space-3); }
.sheet-card .modal-text { margin: 0; font-size: var(--text-xs); }
.sheet-search { margin: 0; }
.lang-list { display: grid; gap: var(--space-1); overflow-y: auto; padding: 0 var(--space-2) var(--space-2) 0; scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
/* Fade the bottom edge so a scrollable shelf reads as one, rather than as a row cut in half. */
.lang-list { mask-image: linear-gradient(to bottom, #000 calc(100% - var(--space-5)), transparent); }
.lang-group { position: sticky; top: 0; z-index: 1; margin: var(--space-2) 0 2px; padding: var(--space-2) var(--space-1); color: var(--muted); background: var(--surface); font-size: var(--text-xs); font-weight: 800; }
.lang-option {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: baseline;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  color: var(--text);
  text-align: left;
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.lang-option:hover { border-color: var(--primary); }
.lang-option.active { border-color: var(--success); background: var(--tint-success); }
.lang-native { overflow: hidden; font-size: var(--text-sm); font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
.lang-english { color: var(--muted); font-size: var(--text-2xs); }
.lang-tick { color: var(--success); font-size: var(--text-xs); font-weight: 900; opacity: 0; }
.lang-option.active .lang-tick { opacity: 1; }

/* --- First run ----------------------------------------------------------------------------- */

/* --- Already heard -------------------------------------------------------------------------- */
.poi.listened { border-color: var(--line-success); }
.poi.listened .poi-index { color: var(--success); }
.listened-badge {
  justify-self: start;
  margin-top: 3px;
  padding: 2px var(--space-2);
  color: var(--success);
  background: var(--tint-success);
  border-radius: var(--radius-full);
  font-size: var(--text-2xs);
  font-weight: 800;
  letter-spacing: .04em;
  white-space: nowrap;
}

@media (min-width: 900px) {
  .view-tabs { margin-bottom: var(--space-5); }
  /* Search, results and map all sit above the fold: the three things a traveller is actually
     looking at should never require scrolling past each other. */
  #exploreView {
    grid-template-columns: minmax(360px, .82fr) minmax(0, 1.18fr);
    grid-template-areas: "controls map" "list map" "player player";
    align-content: start;
    gap: var(--space-4) var(--space-4);
  }
  #exploreView:not([hidden]) { display: grid; }
  /* No map yet means no map column: a 1.18fr hole beside a 360px list is worse than one column. */
  #exploreView.no-map {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "controls" "list" "player";
  }
  #controls { grid-area: controls; margin: 0; align-self: start; }
  /* The map follows you down the results instead of scrolling off the top of them.
     A sticky grid *item* cannot do this: its containing block is the whole grid, so it travelled
     down over the full-width player row and the place photograph painted straight through it. The
     shell still stretches to the two rows it was given, and the map sticks inside the shell -
     capped to it, so a short list can never push the map past the area it belongs to. */
  .map-shell { grid-area: map; margin: 0; align-self: stretch; min-height: 0; }
  .route-map-shell { grid-area: auto; }
  #map {
    position: sticky;
    top: var(--space-4);
    height: 100%;
    max-height: clamp(440px, calc(100dvh - var(--space-8) * 2), 820px);
  }
  .list-column { grid-area: list; min-width: 0; }
  /* The results are the page. Clipping them to 56vh to keep the map above the fold hid nine of
     fourteen behind a scroll region nested in a scrolling page - a coin toss about which one
     moves, with no cue that either does. The list runs at its own length and the map comes with
     you instead. */
  /* The player row spans both columns, which stretched a narration to a 1900 px line and blew the
     place photograph up to the width of the page. Text wants a column, not a wall. */
  .player-column { grid-area: player; width: 100%; max-width: 940px; min-width: 0; margin: var(--space-3) auto 0; }
  .player-card { margin: 0; }
  /* D4: a story used to open in a full-width band below the map, so tapping a place changed nothing
     a traveller could see without scrolling. While one is playing the map steps back to a strip and
     the story takes the column beside the list, where the tap happened. */
  #exploreView.is-playing {
    grid-template-areas: "controls map" "list player";
  }
  #exploreView.is-playing #map { height: clamp(220px, 26vh, 320px); }  /* Both columns run at their own length. Clipping each to the viewport and fading its edge made
     the cut legible but left three scroll surfaces on one screen; the now-playing bar is the one
     affordance for "you have scrolled away from the story", and it needs the page to be what
     scrolls. */
  #exploreView.is-playing .player-column {
    grid-area: player;
    max-width: none;
    margin: 0;
  }
  /* A now-playing bar centred across a two-column layout straddles the boundary and covers a
     result row in each. On a wide screen it belongs beside the results it came from. */
  /* On a wide screen it aligns to the content column rather than the browser window. */
  #nowbar {
    right: auto;
    bottom: var(--space-5);
    left: 50%;
    width: min(var(--max), 100vw - var(--space-8));
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--radius-full);
    transform: translateX(-50%);
  }
  /* The form is six short rows and the summary is a long list, so stacking them in one column put
     the map two screens down. The form and the map share the left column - which was otherwise
     empty - and the summary gets its own. */
  .route-grid {
    grid-template-columns: minmax(320px, .82fr) minmax(0, 1.18fr);
    grid-template-areas: "form summary" "map summary";
    /* The summary is far taller than the form and the map together. Row 1 hugs the form so no gap
       opens under it, and row 2 takes the slack - which the map is the one element that gains from.
       `minmax` rather than a bare `1fr`, because the map's own height is a percentage of this row
       and a track sized only by its contents would resolve that circle to zero. */
    grid-template-rows: min-content minmax(300px, 1fr);
    align-content: start;
  }
  /* No summary yet means no summary column, the same rule the Explore map follows: a 1.18fr hole
     beside a 320px form is worse than one column. The form keeps a form's width when it has the
     page to itself - a 1070px-wide button with its label in the middle is not a control anyone
     reads as one. */
  .route-grid.no-summary {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "form" "map";
  }
  .route-grid.no-summary .route-form { max-width: 560px; }
  .route-form { grid-area: form; }
  .route-map-shell { grid-area: map; margin: 0; align-self: stretch; min-height: 0; }
  .route-summary { grid-area: summary; }
  /* The same rule as the Explore map: the shell takes the row, the map sticks inside it, capped to
     the shell so it can never overflow the grid. It is the spatial context for the stop list. */
  #routeMap {
    position: sticky;
    top: var(--space-4);
    height: 100%;
    max-height: clamp(300px, calc(100dvh - var(--space-8) * 2), 620px);
  }

}

@media (max-width: 900px) {
  /* Peek by default: enough to read the spread of the pins, at 40% of what it used to cost.
     The height follows the width, or a 700px-wide map at 150px is a letterbox with nothing in it.
     The height snaps and the camera flies - animating a tiled canvas while it re-renders is two
     things moving against each other, and the camera is the one that carries the meaning. */
  #map { height: max(150px, min(30vw, 260px)); }
  .map-shell.expanded #map { height: min(72vh, 560px); }
  .map-shell .map-toggle { display: inline-flex; }
  /* While a story is playing you are listening, not navigating. The map steps aside completely and
     the now-playing bar is how you get back to it. */
  #exploreView.is-playing .map-shell { display: none; }
}

/* A pair of zoom buttons is 90px of a 150px peek, on a device where pinch is the gesture and the
   only labelled control that matters is the one that makes the map bigger. They come back the
   moment the map does. */
@media (hover: none) and (pointer: coarse) {
  .map-shell:not(.expanded) .maplibregl-ctrl-top-right { display: none; }
}

@media (max-width: 560px) {
  header { padding-top: var(--space-4); }
  /* Three header controls, a mark and a wordmark do not fit in 343px, and the wordmark is the one
     the browser tab, the home screen icon and the mark itself are all already saying. The language
     shrinks to its code beside a globe, which is what a code is for. */
  .wordmark { display: none; }
  .lang-caption, .lang-name { display: none; }
  .lang-code { display: inline; }
  .lang-btn { padding: var(--space-2) var(--space-3); }
  main { padding-right: var(--space-4); padding-left: var(--space-4); }
  .view-tabs { width: 100%; margin-bottom: var(--space-6); }
  .view-tab { flex: 1; }
  .header-actions { margin-left: auto; }
  /* On a phone the headline was a third of the first screen before anything usable appeared. */
  h1 { font-size: var(--text-3xl); line-height: 1.14; margin-bottom: var(--space-3); }
  .card { padding: var(--space-4); border-radius: var(--radius-lg); }
  /* 343px of card holds both shortcuts on one row only once they stop paying desktop padding. */
  .start-row > button { padding-right: var(--space-3); padding-left: var(--space-3); }
  /* A 16:7 hero is 136px tall at phone width, and a two-line caption laid over it covered four
     fifths of the photograph. The picture gets a shape it can be read at, and its words move off
     it - where they are also easier to read, on the card rather than over a sky. */
  .pg-hero-img { aspect-ratio: 16 / 10; }
  .pg-hero figcaption {
    position: static;
    padding: var(--space-3) var(--space-4) var(--space-2);
    background: none;
  }
  /* D1: at 375px the labelled button squeezed the field to 148px and clipped its own placeholder.
     A magnifier beside a search field needs no word - it is one of the few marks that never does. */
  .search-go { padding: var(--space-3); }
  .search-go-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .route-metrics strong { font-size: var(--text-lg); }
  .voice-actions { flex-direction: column; }
  .site-footer { align-items: flex-start; flex-direction: column; }
}

/* --- Signing in ------------------------------------------------------------------------------
   Optional by design: Placecast works signed out, so the control is quiet and never blocks a
   story. It only appears at all once the server says an instance is configured. */
.auth-btn { padding-left: var(--space-2); }
.auth-avatar { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
.auth-label { max-width: 12ch; overflow: hidden; font-size: var(--text-xs); font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
.auth-card { display: grid; gap: var(--space-3); width: min(100%, 400px); text-align: left; }
.auth-providers { display: grid; gap: var(--space-2); }
.provider-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 50px;
  padding: var(--space-3) var(--space-4);
  color: var(--text);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 700;
}
.provider-btn:hover { border-color: var(--primary); background: var(--surface); }
.provider-mark { flex: none; }
.auth-note { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-2xs); line-height: 1.5; text-align: center; }

/* --- Waiting, honestly --------------------------------------------------------------------- */
/* Every control that starts network work looks busy while it is. One rule, so no control can be
   forgotten the way the floating Locate button was. */
.is-busy { position: relative; cursor: progress; opacity: .75; }
.is-busy::after {
  position: absolute;
  top: 50%;
  right: 10px;
  width: 14px;
  height: 14px;
  margin-top: -7px;
  border: 2px solid var(--line-strong);
  border-top-color: currentcolor;
  border-radius: 50%;
  animation: spin .7s linear infinite;
  content: "";
}
/* Text controls make room for the spinner so it never sits on top of the label. */
.is-busy:not(.playbtn) { padding-right: var(--space-6); }
.playbtn.is-busy::after { top: 6px; right: 6px; margin: 0; }
/* Naming the step and showing the distance covered is the difference between "working" and
   "hung". Each width below is a step the server has actually reported finishing. */
.prep-bar {
  height: 3px;
  margin-top: var(--space-2);
  overflow: hidden;
  background: var(--line);
  border-radius: var(--radius-full);
}
/* The fill is always full width and slid into view, so the gradient keeps its true ramp and the
   animation runs on the compositor instead of relaying out the card on every progress event. */
.prep-bar::after {
  display: block;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, var(--primary-strong), var(--primary));
  border-radius: inherit;
  transform: translateX(calc(var(--p, 0%) - 100%));
  transition: transform var(--dur-3) var(--ease);
  content: "";
}

/* The story you are listening to keeps playing while the next one is prepared. */
#preparing {
  position: fixed;
  z-index: 81;
  right: 0;
  bottom: calc(60px + env(safe-area-inset-bottom));
  left: 0;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: 620px;
  margin: auto;
  padding: var(--space-2) var(--space-3) var(--space-3) var(--space-4);
  overflow: hidden;
  background: var(--veil);
  border: 1px solid var(--line);
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  backdrop-filter: blur(14px);
  animation: rise .25s var(--ease);
}
#preparing::before {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--primary);
  transform: translateX(calc(var(--p, 0%) - 100%));
  transition: transform var(--dur-3) var(--ease);
  content: "";
}
.prep-copy { display: grid; gap: 1px; min-width: 0; flex: 1; }
.prep-copy strong { overflow: hidden; font-size: var(--text-xs); text-overflow: ellipsis; white-space: nowrap; }
.prep-copy span { color: var(--muted); font-size: var(--text-2xs); }
.prep-x { display: grid; place-items: center; width: 36px; height: 36px; flex: none; color: var(--muted); background: none; border: 1px solid var(--line); border-radius: var(--radius-md); }
.prep-x:hover { color: var(--text); border-color: var(--primary); }

/* How far through the story you are, without opening the player. */
#nowbar::after {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--played, 0%);
  height: 2px;
  background: var(--success);
  border-radius: 0 var(--radius-full) var(--radius-full) 0;
  content: "";
}

@media (min-width: 900px) {
  #preparing { right: auto; bottom: calc(84px + env(safe-area-inset-bottom)); left: 20px; width: min(420px, 38vw); margin: 0; border-radius: var(--radius-md); }
}

/* --- Motion preference ------------------------------------------------------------------------
   One block, at the end, so it always wins and there is nowhere else to look. Animation that
   carries meaning - the busy spinner - slows rather than stops, because a frozen spinner reads as
   a frozen app. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
  .is-busy::after {
    animation-duration: 2.4s !important;
    animation-iteration-count: infinite !important;
  }
}

/* --- The allowance, and what lifts it ---------------------------------------------------------
   A number and a plan, said plainly. Nothing here counts down, and nothing shames a traveller who
   decides the answer is no. */
.quota-note { margin: 0 0 var(--space-3); color: var(--muted); font-size: var(--text-2xs); }
.pro-card { width: min(100%, 460px); text-align: left; display: grid; gap: var(--space-3); }
.pro-benefits { display: grid; gap: var(--space-2); margin: 0; padding-left: var(--space-5); color: var(--body); font-size: var(--text-sm); }
.pro-plans { display: grid; gap: var(--space-2); }
.pro-plan {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  background: var(--canvas-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}
.pro-plan strong { flex: 1; color: var(--text); }
.pro-plan span { color: var(--muted); font-weight: 800; }
/* Scoped to the row, or `.pro-plan span` outranks it on specificity and paints the badge muted
   grey on teal - which measured 1.2:1, a contrast failure invisible until you look for it. */
.pro-plan .pro-badge {
  padding: var(--space-1) var(--space-2);
  color: var(--canvas);
  background: var(--success);
  border-radius: var(--radius-full);
  font-size: var(--text-2xs);
  font-weight: 800;
}
.pro-note { margin: 0; color: var(--muted); font-size: var(--text-2xs); line-height: 1.5; }
.pro-actions { display: grid; gap: var(--space-2); }
.pro-actions a { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-height: var(--control-h); text-decoration: none; }
