/*
  One stylesheet. No preprocessor, no build step, no framework.

  The exhibit. One continuous deep, quiet green stage — the map, edge to edge —
  and on it a set of objects: white placards with thick black frames and hard
  offset shadows, skewed signs for anything that can be pressed, small tickets
  for anything that merely says something. Cobalt and hot pink sit behind and
  beside them as cut paper. Everything is set in Oswald, numbers included, with
  tabular figures where they have to line up.

  The map's own colours are not here. Those come from the style documents
  internal/style generates, and the choropleth ramp is pushed into --ramp by
  main.js from the same metadata, so the legend and the map cannot disagree
  about what pink means.
*/

/* ----------------------------------------------------------------- fonts */

@font-face {
  font-family: "Oswald";
  font-style: normal;
  font-weight: 200 700;
  font-display: swap;
  src: url("/vendor/fonts/oswald-latin.woff2") format("woff2");
}

/* ---------------------------------------------------------------- tokens */

:root {
  --font: "Oswald", "Arial Narrow", "Helvetica Neue", Arial, sans-serif;

  --canvas: #F1F1EE;
  --paper: #FFFFFF;
  --ink: #000000;
  --accent: #FF2E9F;
  --cobalt: #0023D1;
  --text: #111827;
  --text-2: #3D4652;
  --text-3: #6B7480;
  --success: #16A34A;
  --warn: #D97706;
  --danger: #DC2626;
  --warn-soft: rgba(217, 119, 6, 0.14);
  --cobalt-soft: rgba(0, 35, 209, 0.10);

  --b2: 2px solid var(--ink);
  --b3: 3px solid var(--ink);
  --b4: 4px solid var(--ink);

  --shadow-sm: 3px 3px 0 var(--ink);
  --shadow-md: 6px 6px 0 var(--ink);
  --shadow-lg: 10px 10px 0 var(--ink);

  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s6: 24px;
  --s8: 32px;

  --gap: 16px;
  --panel-width: 408px;

  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
  --snap: cubic-bezier(0.34, 1.4, 0.64, 1);

  --ramp: linear-gradient(90deg, #2f9e4f 0 50%, #e8a6a6 50% 100%);

  /* The chrome's scale. Everything on the stage is sized in CSS pixels, and
     on a wide screen that shrinks it all against the map; this ramps it up
     with the viewport and is applied as `zoom` to every object — never to
     the map, whose pixel-to-coordinate mapping has to stay exact. */
  --ui-zoom: 1;
}

/* Before the script has measured the viewport, the value it will set for
   anything wider than a phone, so the chrome does not draw large and then
   snap down. Its source of truth is ui.zoomFor. */
@media (min-width: 700px) {
  :root { --ui-zoom: 0.85; }
}

/* After hours the stage is ink and the paper is turned down a shade — the
   same objects, the same ink frames, under less light. White placards on a
   black stage glared; DC is paper by a reading lamp. The third text tone
   darkens a step to keep its contrast on the dimmer paper. */
:root[data-theme="dark"] {
  --canvas: #161616;
  --paper: #DCDCD8;
  --text-3: #596270;
}

* { box-sizing: border-box; }

/* The hidden attribute wins over any display a class sets — a ticket that is
   hidden is not on the stage, whatever a ticket normally looks like. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.35;
  color: var(--text);
  background: var(--canvas);
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
}

::selection { background: var(--accent); color: var(--paper); }

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

:focus-visible {
  outline: 3px solid var(--cobalt);
  outline-offset: 3px;
}

/* ----------------------------------------------------------------- stage */

#app {
  position: fixed;
  inset: 0;
  background: var(--canvas);
}

#stage {
  position: relative;
  height: 100%;
  isolation: isolate;
  opacity: 0;
  transition: opacity 700ms var(--ease);
}

#app.is-ready #stage { opacity: 1; }

#map {
  position: absolute;
  inset: 0;
  background: var(--canvas);
  transition: opacity 280ms var(--ease);
}

/* The dip through a theme swap. */
#stage.is-restyling #map { opacity: 0.45; }

/* Stacking, in one place. The chrome and the place card sit on the map; the
   panel sits over both; and anything transient — a search dropdown, the status
   ticket — sits over everything. */
.maplibregl-control-container { z-index: 3; }

/* --------------------------------------------------------------- objects

   Three kinds of thing sit on the stage.

   A placard is where reading happens: paper, a 3px ink frame, a hard shadow.
   A sign is something to press: the same, skewed, and it moves when you touch it.
   A ticket is a small remark: paper, a 2px frame, the smallest shadow.
*/

.placard {
  position: relative;
  background: var(--paper);
  border: var(--b3);
  box-shadow: var(--shadow-md);
  color: var(--text);
}

/* A cut-paper block behind a placard, as a stack of hard shadows: the ink
   shadow, then a pink block further out with an ink rim behind it. Shadows
   paint beneath the box by definition, so nothing can end up over the text —
   which an element with z-index: -1 did, the moment the placard gained a
   transform and with it a stacking context of its own. */
.placard-backed {
  box-shadow:
    6px 6px 0 var(--ink),
    16px 16px 0 -3px var(--accent),
    16px 16px 0 0 var(--ink);
}

.ticket {
  display: inline-block;
  padding: 3px 9px;
  background: var(--paper);
  border: var(--b2);
  box-shadow: var(--shadow-sm);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text);
}

.sign {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px 14px;
  background: var(--paper);
  border: var(--b3);
  box-shadow: var(--shadow-sm);
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1.2;
  cursor: pointer;
  transform: skew(-6deg);
  transition: transform 120ms var(--ease), box-shadow 120ms var(--ease), background 120ms;
}

.sign > * {
  display: inline-block;
  transform: skew(6deg);
}

.sign:hover {
  box-shadow: 5px 5px 0 var(--ink);
  transform: skew(-6deg) translate(-2px, -2px);
}

.sign:active {
  box-shadow: 1px 1px 0 var(--ink);
  transform: skew(-6deg) translate(2px, 2px);
}

.sign-ink {
  background: var(--ink);
  color: var(--paper);
}

.sign-ink:hover { background: #1a1a1a; }

.eyebrow {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.num {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

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

#chrome {
  position: absolute;
  /* Above the legend: the form's controls open downward over it. */
  z-index: 5;
  top: var(--gap);
  left: var(--gap);
  display: flex;
  flex-direction: column;
  /* One column, one width: the brand, the search and the controls all span
     it, so the stack reads as a masthead over a form and not as a staircase
     of four different widths. Only the timetable ticket is content-sized. */
  align-items: stretch;
  gap: 14px;
  width: min(380px, calc(100% - 2 * var(--gap)));
}


#brand {
  /* Room under it for the sticker, which hangs below the bottom corner. */
  margin: 0 0 38px;
  padding: 8px 18px 9px 14px;
  /* The word sits centred on its placard; the placard sits on the column. */
  text-align: center;
  transform: rotate(-1.5deg);
  transform-origin: left bottom;
  white-space: nowrap;
  /* Two cards, not three. The stack used to be an ink shadow at 6px and a
     pink block at 16px with its own ink rim — five near-parallel edges,
     which at a degree and a half of tilt is a muddle of aliased slivers
     rather than a stack. One pink block with one rim, directly behind the
     paper: the same idea, half the lines. */
  box-shadow:
    11px 11px 0 -3px var(--accent),
    11px 11px 0 0 var(--ink);
}

.brand-space { display: inline-block; width: 0.28em; }

/* The city: a cobalt sticker tucked onto the placard's bottom right corner,
   overlapping it and hanging a little off both edges. It sits below the
   wordmark's baseline rather than beside it — at this size the word fills
   the column, and the corner under it is the room there is. One day a
   menu. */
.brand-sticker {
  position: absolute;
  right: -14px;
  bottom: -34px;
  padding: 5px 14px 6px;
  background: var(--cobalt);
  color: var(--paper);
  border: var(--b3);
  box-shadow: 4px 4px 0 var(--ink);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1.2;
  /* Skewed like every other sign, but not tilted against the placard: it
     inherits the placard's own rotation, so its edges run parallel to the
     card and the pink block rather than crossing them. */
  transform: skew(-6deg);
  transform-origin: center;
}

.brand-sticker:hover { transform: skew(-6deg); box-shadow: 4px 4px 0 var(--ink); }
.brand-sticker > span { display: inline-block; transform: skew(6deg); }

/* The wordmark is set letter by letter so the letters can rise in one after
   another. The word clips them until they do. */
.brand-word { overflow: hidden; padding-bottom: 0.06em; margin-bottom: -0.06em; }
.brand-letter { display: inline-block; }

.brand-word {
  display: block;
  font-size: 34px;
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}


/* Which day is on screen. Quiet until it is telling you the answer is for a day
   other than today, which is worth a colour. */
#service-date.is-stale {
  background: var(--warn);
  color: var(--paper);
  border-color: var(--ink);
}

/* The top-right corner: the theme sign. The corner is positioned and the
   sign is not, so anything added later is a sibling and not a second set
   of coordinates to keep in step. */
#corner {
  position: absolute;
  z-index: 4;
  top: var(--gap);
  right: var(--gap);
  display: flex;
  align-items: flex-start;
  gap: 12px;
  transition: right 340ms var(--ease);
}

#theme-toggle {
  padding: 6px 12px;
  text-decoration: none;
}

/* The panel opens over the top-right corner, so what lives there steps left
   with it rather than vanishing under it — the corner's signs, the readout,
   and MapLibre's own zoom and attribution controls. */
#stage:has(#panel.is-open) #corner,
#stage:has(#panel.is-open) #readout {
  right: calc(var(--panel-width) + var(--gap) + 16px);
}

#stage:has(#panel.is-open) .maplibregl-ctrl-bottom-right {
  right: calc(var(--panel-width) + 16px);
}

.maplibregl-ctrl-bottom-right { transition: right 340ms var(--ease); }

/* Light and dark: a disc, paper on one side and ink on the other, in an
   ink rim. The same in both themes — the label says where it takes you. A
   crescent drawn with an offset inset shadow came out lopsided at 14px. */
.theme-icon {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid var(--ink);
  background: linear-gradient(90deg, var(--paper) 50%, var(--ink) 50%);
}

/* ----------------------------------------------------------------- search */

#search {
  position: relative;
  width: 100%;
}

/* The form's rows. The placard is #form; the rows carry no frame of their
   own, only a rule between them, and the focused row is marked by a cobalt
   bar down its left edge rather than a ring around it. */
#form { overflow: visible; }
#form > * + * { border-top: var(--b2); }
#form > [hidden] + * { border-top: var(--b2); }

.search-field {
  display: block;
  background: var(--paper);
}

.search-eyebrow {
  display: block;
  padding: 9px 12px 0;
}

.search-input {
  display: block;
  width: 100%;
  height: 44px;
  padding: 0 12px 4px;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 22px;
  font-weight: 500;
  outline: none;
}

.search-input::placeholder { color: var(--text-3); font-weight: 400; }
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; }

.search-field:focus-within { box-shadow: inset 5px 0 0 var(--cobalt); }

.search-row {
  position: relative;
  display: block;
}

.search-clear {
  position: absolute;
  top: 50%;
  right: 8px;
  width: 26px;
  height: 26px;
  padding: 0;
  margin-top: -15px;
  border: var(--b2);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  box-shadow: 2px 2px 0 var(--ink);
}

.search-clear:hover { background: var(--accent); color: var(--paper); }

.search-input { padding-right: 44px; }

/* The list. A placard hung under the field, and each row a two-line entry —
   the name with the typed part inked heavier, and beneath it what the thing
   is and where — so the eye can tell a station from a street from a postcode
   without reading either line. */
.search-results {
  position: absolute;
  z-index: 7;
  top: calc(100% + 10px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  max-height: min(58vh, 520px);
  overflow-y: auto;
  background: var(--paper);
  border: var(--b3);
  box-shadow: var(--shadow-md);
  animation: rise 180ms var(--ease) both;
}

.search-head {
  padding: 8px 12px 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}

.search-result {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px 9px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
  cursor: pointer;
  animation: rise 220ms var(--ease) both;
  animation-delay: calc(var(--i, 0) * 18ms);
}

.search-result:last-of-type { border-bottom: 0; }

.search-result.is-active {
  background: var(--ink);
  color: var(--paper);
}

.search-result.is-active .search-secondary { color: rgba(255, 255, 255, 0.72); }
.search-result.is-active mark { color: var(--accent); }

.search-result.is-outside {
  cursor: not-allowed;
  color: var(--text-3);
}

.search-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.search-primary {
  font-size: 17px;
  font-weight: 400;
  line-height: 1.15;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-primary mark {
  background: none;
  color: inherit;
  font-weight: 700;
}

.search-secondary {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The mark: a small closed set of shapes, one per kind. Ink outlines, so a
   list of eight does not become a row of colour swatches; the station is the
   one exception, because it is the one most people are looking for. */
.search-kind {
  flex: none;
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  box-sizing: border-box;
}

.search-kind-station {
  border-radius: 50%;
  border-width: 3px;
  background: var(--accent);
}

.search-kind-locality { background: currentColor; }
.search-kind-street { width: 16px; height: 0; border-width: 0 0 3px; transform: rotate(-30deg); }
.search-kind-postcode { border-color: var(--cobalt); background: var(--cobalt); transform: skewX(-12deg); }
.search-kind-poi { border-radius: 50% 50% 50% 0; transform: rotate(-45deg); }
.search-kind-park { border-radius: 50% 50% 0 0; }
.search-kind-place { border-radius: 2px; }
.search-result.is-active .search-kind-postcode { border-color: var(--paper); background: var(--paper); }

.search-outside {
  flex: none;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--warn);
}

.search-empty {
  padding: 12px 12px;
  font-size: 15px;
  color: var(--text-3);
}

.search-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 5px 12px 6px;
  border-top: var(--b2);
  background: var(--ink);
  color: var(--paper);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.search-pending::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 6px;
  background: var(--accent);
  animation: blink 900ms steps(2, start) infinite;
}

/* The second workplace: the same field, and a row to ask for it. */
.form-add {
  display: block;
  width: 100%;
  padding: 6px 12px 7px;
  border: 0;
  background: var(--paper);
  color: var(--text-2);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
}

.form-add:hover { background: var(--ink); color: var(--paper); }

#search2 { position: relative; }

/* The timetable's date: the form's small print. */
.form-foot {
  margin: 0;
  padding: 5px 12px 6px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-3);
  cursor: help;
}

/* --------------------------------------------------------------- controls */

.controls {
  position: relative;
  width: 100%;
  background: var(--paper);
}

.controls-summary {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 10px 12px;
  cursor: pointer;
  list-style: none;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-2);
}

.controls-summary::-webkit-details-marker { display: none; }

.controls-summary > span:first-child { white-space: nowrap; }

.ctl-summary {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 15px;
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.controls-summary::after {
  content: "";
  margin-left: auto;
  width: 9px;
  height: 9px;
  border-right: 3px solid var(--ink);
  border-bottom: 3px solid var(--ink);
  transform: translateY(-3px) rotate(45deg);
  transition: transform 160ms var(--ease);
}

.controls[open] .controls-summary::after { transform: translateY(2px) rotate(-135deg); }

.controls-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px 14px;
  padding: 12px 12px 14px;
  border-top: var(--b3);
  animation: rise 220ms var(--ease) both;
}

/* The rule as a switch, across the whole grid. Checked, the fields under it
   are hidden; the reader is asking for good commutes and nothing else. */
.ctl-good {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: var(--b2);
  cursor: pointer;
  user-select: none;
}

.ctl-good-box {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.ctl-good-mark {
  flex: none;
  width: 18px;
  height: 18px;
  border: var(--b2);
  background: var(--paper);
  box-shadow: 2px 2px 0 var(--ink);
}

.ctl-good:has(input:checked) { background: var(--ink); color: var(--paper); }
.ctl-good:has(input:checked) .ctl-good-mark { background: var(--accent); }
.ctl-good:has(input:focus-visible) { outline: 3px solid var(--cobalt); outline-offset: 2px; }

.ctl-good-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.ctl-good-title {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.ctl-good-rule {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-3);
}

.ctl-good:has(input:checked) .ctl-good-rule { color: rgba(255, 255, 255, 0.7); }

.controls-locked { display: contents; }

/* Rent: the other axis, in its own row under the routing controls. */
.ctl-rent {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px 14px;
  padding-top: 12px;
  border-top: var(--b2);
}

.ctl-rent-note {
  grid-column: 1 / -1;
  font-size: 11px;
  line-height: 1.4;
  color: var(--text-3);
}


.ctl-field { display: flex; flex-direction: column; gap: 5px; }
.ctl-field-modes { grid-column: 1 / -1; }

.ctl-input {
  height: 38px;
  padding: 0 8px;
  border: var(--b2);
  background: var(--paper);
  color: var(--text);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  min-width: 0;
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
}

select.ctl-input {
  padding-right: 28px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink) 50%),
    linear-gradient(135deg, var(--ink) 50%, transparent 50%);
  background-position: calc(100% - 15px) 15px, calc(100% - 10px) 15px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.ctl-modes { display: flex; flex-wrap: wrap; gap: 8px; }

/* A mode is a chip you press, with the box hidden and the state carried by the
   chip's own colour, border and the tick — never colour alone. */
.ctl-mode {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px 5px 9px;
  border: var(--b2);
  background: var(--paper);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-2);
  cursor: pointer;
  user-select: none;
  transition: background 120ms, color 120ms, transform 120ms var(--ease), box-shadow 120ms;
}

.ctl-mode input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
}

.ctl-mode::before {
  content: "";
  width: 10px;
  height: 10px;
  border: 2px solid currentColor;
  background: transparent;
  transition: background 120ms;
}

.ctl-mode:has(input:checked) {
  background: var(--ink);
  color: var(--paper);
  box-shadow: var(--shadow-sm);
  transform: translate(-1px, -1px);
}

.ctl-mode:has(input:checked)::before {
  background: var(--accent);
  border-color: var(--paper);
}

.ctl-mode:has(input:focus-visible) {
  outline: 3px solid var(--cobalt);
  outline-offset: 3px;
}

/* --------------------------------------------------------- status, readout */

#status {
  position: absolute;
  z-index: 7;
  top: var(--gap);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  animation: drop 260ms var(--snap) both;
}

#status.is-out { animation: lift 200ms var(--ease) both; }

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

#status.is-busy::before {
  content: "";
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 8px;
  border: 2px solid var(--ink);
  background: var(--accent);
  animation: blink 900ms steps(2, end) infinite;
}

/* The hover readout. Follows nothing — it sits in one place, because a label
   chasing the cursor is unreadable at exactly the moment it matters. */
#readout {
  position: absolute;
  z-index: 4;
  top: calc(var(--gap) + 58px);
  right: var(--gap);
  min-width: 128px;
  padding: 8px 14px 10px;
  text-align: right;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-4px);
  transition: right 340ms var(--ease), opacity 160ms var(--ease), transform 160ms var(--ease);
}

#readout.is-shown { opacity: 1; transform: none; }

.readout-num {
  display: block;
  font-size: 40px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

/* Two workplaces: the combined figure, and the split as the caption. */
.readout-split { display: block; margin-top: 2px; font-size: 12px; font-weight: 600; color: var(--text-2); }

.readout-cap {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

#readout.is-unreachable .readout-num { color: var(--text-3); }

/* ----------------------------------------------------------------- legend */

#legend {
  position: absolute;
  z-index: 4;
  /* Bottom-left, level with the scale and zoom buttons on the right. */
  left: var(--gap);
  bottom: 38px;
  margin: 0;
  padding: 8px 12px 10px;
  width: 224px;
}

#legend figcaption {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 7px;
}

/* The bands: one cell each, inked as a row, the boundary minutes beneath
   each cell's right edge. */
.legend-bands {
  display: flex;
  height: 14px;
  border: var(--b2);
}

.legend-band { flex: 1; }
.legend-band + .legend-band { border-left: 2px solid var(--ink); }

.legend-labels {
  display: flex;
  margin-top: 3px;
}

.legend-label {
  flex: 1;
  font-size: 11px;
  font-weight: 600;
  text-align: right;
  color: var(--text-2);
  white-space: nowrap;
}

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

   What a locality is like, beside its name on the map. A MapLibre popup whose
   *content* animates and whose wrapper never does: MapLibre positions the
   wrapper with an inline transform, and a keyframe on the same property would
   strand the card at the page's origin. Photo, commute, name, stations, the
   good things. Nothing about legs — that is the panel's.
*/

.pc {
  position: absolute;
  z-index: 5;
  overflow: hidden;
  animation: rise 200ms var(--ease) both;
  cursor: default;
}

.pc-body { padding: 10px 14px 12px; }

.pc-commute {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 0;
}

.pc-commute-part { margin-bottom: 2px; }

/* The cost under the commute, in the selection's terms. */
.pc-est {
  margin: 4px 0 2px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
}
.pc-commute-part .pc-min { font-size: 24px; }
.pc-commute-part.is-out .pc-min { color: var(--text-3); }
.pc-who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-2);
}

.pc-min {
  font-size: 34px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--accent);
}

.pc-min-unit {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}

.pc-how {
  margin-left: auto;
  padding: 2px 8px 3px;
  background: var(--ink);
  color: var(--paper);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pc-name {
  margin: 4px 0 0;
  font-size: 26px;
  font-weight: 700;
  line-height: 1.05;
}

.pc-sub {
  margin: 2px 0 8px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.03em;
  color: var(--text-2);
}

.pc-pending { color: var(--text-3); font-weight: 400; }

.pc-block {
  padding: 8px 0 9px;
  border-top: var(--b2);
}

.pc-block-title { display: block; margin-bottom: 5px; }

.pc-none {
  margin: 0;
  font-size: 13px;
  color: var(--text-3);
}

.pc-stations {
  margin: 0;
  padding: 0;
  list-style: none;
}

.pc-station {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 2px 0;
}

.pc-walk {
  flex: none;
  width: 40px;
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  font-size: 17px;
  font-weight: 700;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

.pc-walk-unit {
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.pc-station-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 3px 7px;
}

.pc-station-name { font-size: 15px; font-weight: 500; }

.pc-chips { display: inline-flex; flex-wrap: wrap; gap: 3px; }

.pc-chip {
  display: inline-block;
  padding: 1px 5px 2px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.3;
  color: var(--line-text, #fff);
  background: var(--line, var(--ink));
  border: 2px solid var(--ink);
  white-space: nowrap;
}

.pc-rents {
  display: flex;
  gap: 14px;
  margin: 0 0 4px;
}

.pc-rent { display: flex; flex-direction: column; }

.pc-rent-num {
  font-size: 17px;
  font-weight: 700;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.pc-rent-cap {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.pc-rent-src {
  margin: 0;
  font-size: 11px;
  color: var(--text-3);
}

.pc-foot {
  margin: 0;
  padding-top: 8px;
  border-top: var(--b2);
}

.pc-hint {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--text-3);
}

/* No hover, no card: a tap on a name asks for the journey instead. */
@media (hover: none) {
  .pc { display: none; }
}

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

   The panel answers "how do you get there". It slides in from the right, and
   below 700px it is a bottom sheet — the same nodes in a different box, never a
   second implementation.
*/

#panel {
  position: absolute;
  z-index: 6;
  top: 0;
  right: 0;
  bottom: 0;
  width: var(--panel-width);
  max-width: 100%;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border-left: var(--b4);
  color: var(--text);
  transform: translateX(calc(100% + 12px));
  transition: transform 340ms var(--ease);
  /* Off-screen rather than hidden, so the slide is a slide. aria-hidden is what
     keeps it out of the accessibility tree while it is closed; pointer-events is
     what keeps a closed panel from swallowing a click meant for the map. */
  pointer-events: none;
}

/* The cobalt sheet behind the panel's edge: cut paper showing past the frame. */
#panel::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 18px;
  bottom: 18px;
  left: -16px;
  width: 16px;
  background: var(--cobalt);
  border: var(--b3);
  border-right: 0;
}

#panel.is-open {
  transform: none;
  pointer-events: auto;
}

.panel-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 18px 20px 14px;
  border-bottom: var(--b3);
}

.panel-grip { display: none; }

.panel-titles { flex: 1; min-width: 0; }

.panel-eyebrow {
  display: block;
  margin-bottom: 2px;
}

.panel-title {
  margin: 0;
  font-size: 24px;
  font-weight: 700;
  line-height: 1.05;
  text-transform: uppercase;
  letter-spacing: -0.005em;
}

.panel-close {
  flex: none;
  width: 38px;
  height: 38px;
  padding: 0;
  font-size: 22px;
  line-height: 1;
}

/* Which journey, when there are two. */
.panel-who {
  display: flex;
  gap: 10px;
  padding: 10px 20px 4px;
}

.panel-who-btn {
  flex: 1;
  min-width: 0;
  padding: 5px 10px;
  font-size: 13px;
  text-align: left;
  color: var(--text-2);
}

.panel-who-btn > span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.panel-who-btn.is-active { background: var(--ink); color: var(--paper); }
.panel-who-n { margin-right: 4px; font-weight: 700; color: var(--accent); }

.panel-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 18px 20px 32px;
  transition: opacity 200ms var(--ease);
}

/* The last journey, held while the next is fetched. */
.panel-body.is-busy { opacity: 0.45; pointer-events: none; }

.panel-hint, .panel-error {
  margin: 8px 0;
  font-size: 17px;
  color: var(--text-2);
}

.panel-error {
  padding: 10px 12px;
  border: var(--b3);
  border-left: 10px solid var(--danger);
}

/* --- summary --- */

.summary {
  padding-bottom: 18px;
  margin-bottom: 18px;
  border-bottom: var(--b3);
}

.summary.is-fresh { animation: rise 320ms var(--ease) both; }

/* Where the journey starts from: one line above the number. */
.summary-from {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px 10px;
  margin: 0 0 10px;
  padding-bottom: 8px;
  border-bottom: var(--b2);
}

.summary-from-text { min-width: 0; font-size: 14px; }
.summary-from-where { font-weight: 600; }
.summary-from-pc { color: var(--text-3); font-variant-numeric: tabular-nums; }
.summary-from-est { display: block; margin-top: 3px; font-size: 13px; font-weight: 600; color: var(--text-2); }

/* The verdict against the criteria: a tick in ink, a cross in the warning
   colour, and the reasons in plain words. */
.summary-criteria {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0 0 10px;
  padding: 6px 10px;
  border: var(--b2);
  font-size: 13px;
  font-weight: 500;
}

.summary-criteria-mark { font-weight: 700; }
.summary-criteria.is-ok { color: var(--text-2); }
.summary-criteria.is-out { border-color: var(--warn); color: var(--text); }
.summary-criteria.is-out .summary-criteria-mark { color: var(--warn); }
.summary-from-links { display: inline-flex; gap: 6px; }
.summary-from-links a { padding: 2px 7px; font-size: 11px; text-decoration: none; }

.summary-lead {
  position: relative;
  margin: 0 0 14px;
}

.summary-num {
  display: inline-block;
  font-size: 64px;
  font-weight: 700;
  line-height: 0.95;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  /* A hot-pink bar under the number, cut paper behind the headline. */
  background: linear-gradient(transparent 62%, var(--accent) 62%, var(--accent) 88%, transparent 88%);
  padding: 0 4px;
  margin-left: -4px;
}

.summary-cap {
  display: block;
  margin-top: 8px;
  font-size: 15px;
  font-weight: 500;
  color: var(--text-2);
}

/* No typical journey to quote, so the lead is a sentence and is sized like one.
   At 64px "Reachable for some arrivals" would set as a headline number and read
   as though it were one. */
.summary-lead.is-partial .summary-num {
  font-size: 24px;
  background: none;
  padding: 0;
  margin: 0;
  line-height: 1.15;
}

.summary-tiles {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin: 0 0 14px;
}

.tile {
  padding: 8px 8px 9px;
  border: var(--b2);
  text-align: center;
  min-width: 0;
}

.tile-num {
  display: block;
  font-size: 22px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.tile-cap {
  display: block;
  margin-top: 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tile-best { border-left: 8px solid var(--cobalt); }

/* The one concrete journey, set apart from the window statistics above it
   because it is measured differently: time in motion, not time from the door to
   the deadline. A ticket strip. */
/* What it costs: one line closed, the working open. A footnote to the
   journey, not a section above it. */
.fare {
  margin: 10px 0 0;
  border: var(--b2);
  border-left: 8px solid var(--cobalt);
}

.fare-summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
  cursor: pointer;
  list-style: none;
}

.fare-summary::-webkit-details-marker { display: none; }

.fare-summary::after {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin-left: 2px;
  border-right: 2px solid var(--ink);
  border-bottom: 2px solid var(--ink);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 180ms var(--ease);
}

.fare[open] .fare-summary::after { transform: translateY(2px) rotate(-135deg); }

.fare-title { flex: none; }

.fare-brief {
  flex: 1;
  min-width: 0;
  text-align: right;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.fare-body {
  padding: 4px 12px 10px;
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  animation: rise 200ms var(--ease) both;
}

.fare-lead {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0 0 6px;
}

.fare-num {
  font-size: 30px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.01em;
}

.fare-cap {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-2);
}

.fare-lines {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 13px;
  color: var(--text-2);
}

.fare-lines li {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 2px 0;
  border-top: 1px solid rgba(0, 0, 0, 0.08);
}

.fare-cost { color: var(--text); font-weight: 600; white-space: nowrap; }

.fare-total {
  margin: 8px 0 0;
  padding-top: 6px;
  border-top: var(--b2);
  font-size: 13px;
  color: var(--text-2);
}

.fare-total .num { font-size: 18px; font-weight: 700; color: var(--text); }

.fare-src { margin: 6px 0 0; font-size: 11px; color: var(--text-3); }

.fare-reason { margin: 0 0 8px; font-size: 13px; }
.fare-link { padding: 3px 9px; font-size: 12px; text-decoration: none; }
.fare-unpriced { border-left-color: var(--warn); }

.summary-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  margin: 0;
  padding: 8px 12px;
  border: var(--b2);
  border-left: 8px solid var(--accent);
  background: rgba(0, 0, 0, 0.03);
  font-size: 14px;
  color: var(--text-2);
}

.summary-strip .eyebrow { flex: 0 0 100%; }

.summary-strip .num {
  font-weight: 600;
  color: var(--text);
}

/* --- legs: a strip map --- */

.legs {
  list-style: none;
  margin: 0;
  padding: 0;
}

.leg-item { position: relative; }

/* The legs file in once, when the itinerary is new. Expanding a leg re-renders
   the list from the same itinerary and must not replay the entrance — a list
   that jumps every time it is touched is the opposite of tactile. */
.is-fresh .leg-item {
  animation: rise 360ms var(--ease) both;
  animation-delay: calc(var(--i, 0) * 55ms + 120ms);
}

/* The rail: a thick vertical band in the leg's own colour, inked, running the
   height of its row and into the next so a seven-leg journey reads as one line
   on a strip map rather than seven cards. --line is set on the row from the
   region's own table. */
.leg-rail {
  position: absolute;
  left: 15px;
  top: 0;
  bottom: -2px;
  width: 12px;
  background: var(--line, var(--ink));
  border-left: var(--b2);
  border-right: var(--b2);
  pointer-events: none;
}

.leg-item:first-child .leg-rail { top: 14px; }
.leg-item:last-child .leg-rail { bottom: auto; height: calc(100% - 24px); }

.leg-rail-walk {
  width: 12px;
  background:
    radial-gradient(circle at 50% 50%, var(--paper) 0 3px, var(--ink) 3px 5px, transparent 5px)
    50% 0 / 12px 14px repeat-y;
  border: 0;
}

.leg-rail-rail {
  background:
    repeating-linear-gradient(180deg, transparent 0 5px, var(--paper) 5px 8px, transparent 8px 13px),
    var(--line, var(--ink));
}

.leg-rail-bus {
  background:
    repeating-linear-gradient(180deg, var(--paper) 0 6px, transparent 6px 12px) 50% 0 / 2px 12px repeat-y,
    var(--line, var(--ink));
}

.leg-rail-tram {
  background:
    radial-gradient(circle at 50% 50%, var(--paper) 0 1.6px, transparent 1.9px) 50% 0 / 12px 9px repeat-y,
    var(--line, var(--ink));
}

.leg-rail-double::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 50%;
  width: 3px;
  margin-left: -1.5px;
  background: var(--paper);
}

/* The node at the top of each leg: a paper disc with an ink rim on the rail. */
.leg-node {
  position: absolute;
  left: 12px;
  top: 12px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--paper);
  border: var(--b3);
  pointer-events: none;
}

.leg-node-walk {
  width: 14px;
  height: 14px;
  left: 14px;
  top: 14px;
  background: var(--accent);
}

.leg {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 12px 12px 48px;
  margin-bottom: 2px;
  border: 3px solid transparent;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: transform 140ms var(--ease), box-shadow 140ms var(--ease), border-color 140ms;
}

.leg:hover { background: rgba(0, 0, 0, 0.035); }

.leg.is-expanded {
  border-color: var(--ink);
  background: var(--paper);
  box-shadow: var(--shadow-sm);
  transform: translate(-1px, -1px);
}

.leg-walk { cursor: default; }
.leg-walk:hover { background: none; }

.leg-head {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* The mode chip: a small sign in the line's own colour carrying its name. The
   shape is the region's own mode symbol — plain geometry, never a roundel —
   drawn as the chip's corner treatment. */
.leg-mode {
  display: inline-block;
  flex: none;
  width: 16px;
  height: 16px;
  background: var(--line, var(--ink));
  border: var(--b2);
}

.leg-mode-walk {
  background: var(--paper);
  border-style: dashed;
  border-radius: 50%;
}

.leg-mode-circle { border-radius: 50%; }
.leg-mode-square { border-radius: 0; }
.leg-mode-rounded-square { border-radius: 4px; }
.leg-mode-diamond { border-radius: 2px; transform: rotate(45deg) scale(0.86); }

.leg-name {
  flex: 1;
  min-width: 0;
  font-size: 20px;
  font-weight: 600;
  line-height: 1.1;
}

.leg-name b { font-weight: 700; }

.leg-headsign {
  display: block;
  font-size: 14px;
  font-weight: 400;
  color: var(--text-3);
}

.leg-time {
  flex: none;
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.leg-time small {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-left: 2px;
}

.leg-where {
  display: block;
  margin-top: 6px;
  font-size: 15px;
  color: var(--text-2);
}

.leg-when {
  display: block;
  margin-top: 2px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
}

.leg-wait {
  display: block;
  margin-top: 4px;
  font-size: 13px;
  color: var(--text-3);
}

.leg-service,
.leg-next {
  display: block;
  margin-top: 6px;
  font-size: 14px;
  color: var(--text-2);
}

/* Two adjacent lines drawn from different populations: the frequency describes
   every service between the leg's two stops, the departures below it describe
   the one the rider is on. Naming the population is the only thing stopping them
   being read as one claim. */
.leg-scope {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.leg-scope-value {
  font-size: 14px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* No service in a band is the one thing on the service line that changes a
   decision, so it is the one thing that does not blend into it. */
.leg-noservice,
.alt-noservice {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  border: 2px solid var(--warn);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--warn);
  background: var(--warn-soft);
}

.alt-noservice { flex: 0 0 auto; margin-left: 0; }

/* The dashed flag matches the dashed line on the map. */
.leg-flag {
  display: block;
  margin-top: 6px;
  padding-left: 18px;
  font-size: 13px;
  color: var(--warn);
  position: relative;
}

.leg-flag::before {
  content: "";
  position: absolute;
  left: 0;
  top: 9px;
  width: 12px;
  border-top: 3px dashed var(--warn);
}

/* An unreachable point is an answer and is styled as one: ordinary ink, no
   error colour, no retry. It is a fact about the place. */
.unreachable { padding: 4px 0 8px; animation: rise 320ms var(--ease) both; }

.unreachable-lead {
  margin: 0 0 10px;
  font-size: 30px;
  font-weight: 700;
  line-height: 1;
  text-transform: uppercase;
}

.unreachable-body {
  margin: 0 0 12px;
  font-size: 16px;
  color: var(--text-2);
}

.unreachable-hint {
  margin: 0;
  padding: 10px 12px;
  border: var(--b2);
  border-left: 8px solid var(--cobalt);
  font-size: 14px;
  color: var(--text-2);
}

/* --- alternatives --- */

/* The stops along an expanded line: one row each, the commute from there
   first, the leg's own stretch marked, its two ends named. */
.along {
  margin: 2px 0 10px 48px;
  padding: 0;
  list-style: none;
  border-left: 3px solid var(--ink);
}

.along-label { padding: 2px 0 4px 10px; }

.along-stop { margin: 0; }

.along-btn {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  padding: 2px 8px 2px 10px;
  border: 0;
  background: none;
  color: var(--text-2);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}

.along-btn:hover { background: var(--ink); color: var(--paper); }

.along-min {
  flex: none;
  width: 26px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.along-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.along-stop.is-on-leg .along-btn { color: var(--text); font-weight: 500; }
.along-stop.is-on-leg { background: rgba(0, 0, 0, 0.05); }

.along-end {
  flex: none;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}

.along-btn:hover .along-end, .along-btn:hover .along-min { color: var(--paper); }

.alts {
  list-style: none;
  margin: 2px 0 10px;
  padding: 0 0 0 48px;
}

.alts-label {
  padding: 2px 0 6px;
}

.alt {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  width: 100%;
  text-align: left;
  padding: 6px 10px;
  margin-bottom: 6px;
  border: var(--b2);
  background: var(--paper);
  font: inherit;
  font-size: 14px;
  color: var(--text-2);
  cursor: pointer;
  transition: transform 120ms var(--ease), box-shadow 120ms var(--ease);
}

.alt:hover {
  box-shadow: var(--shadow-sm);
  transform: translate(-1px, -1px);
}

.alt.is-expanded {
  border-color: var(--ink);
  box-shadow: var(--shadow-sm);
  background: var(--cobalt-soft);
  color: var(--text);
}

.alt-swatch {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: var(--b2);
  vertical-align: middle;
  flex: none;
}

.alt > b { flex: 0 0 auto; font-size: 15px; }
.alt-headsign { flex: 1 1 auto; min-width: 0; color: var(--text-3); }
.alt-time { flex: 0 0 auto; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text); }
.alt-service { flex: 0 0 auto; font-size: 13px; font-variant-numeric: tabular-nums; color: var(--text-3); }

/* ---------------------------------------------------------- map furniture */

/* The workplace: a paper tag reading WORK on a pin. */
.dest-marker {
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
}

.dest-tag {
  padding: 3px 9px 4px;
  background: var(--paper);
  border: var(--b3);
  box-shadow: var(--shadow-sm);
  font-family: var(--font);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
  transform: skew(-6deg);
  animation: settle 600ms var(--snap) both;
}

.dest-pin {
  width: 14px;
  height: 14px;
  margin-top: 4px;
  background: var(--accent);
  border: var(--b3);
  transform: rotate(45deg);
}

/* The clicked point: a hot-pink disc with an ink rim and a slow pulse. */
.sel-marker {
  position: relative;
  width: 22px;
  height: 22px;
  pointer-events: none;
}

.sel-dot {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--accent);
  border: var(--b3);
  animation: settle 360ms var(--snap) both;
}

.sel-pulse {
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 3px solid var(--accent);
  animation: pulse 1600ms var(--ease) infinite;
}

/* START, above the disc, in the WORK tag's voice. */
.sel-tag {
  position: absolute;
  bottom: calc(100% + 9px);
  left: 50%;
  transform: translateX(-50%) skew(-6deg);
  padding: 3px 9px 4px;
  background: var(--paper);
  border: var(--b3);
  box-shadow: var(--shadow-sm);
  font-family: var(--font);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
  white-space: nowrap;
  animation: settle-tag 600ms var(--snap) both;
}

@keyframes settle-tag {
  from { opacity: 0; transform: translateX(-50%) skew(-6deg) scale(0.8) translateY(6px); }
  to { opacity: 1; transform: translateX(-50%) skew(-6deg); }
}

/* MapLibre's controls, made into objects of the same system. */
.maplibregl-ctrl-group {
  border-radius: 0 !important;
  border: var(--b3);
  box-shadow: var(--shadow-sm) !important;
  background: var(--paper) !important;
}

.maplibregl-ctrl-group button {
  width: 34px !important;
  height: 34px !important;
  border-radius: 0 !important;
}

.maplibregl-ctrl-group button + button { border-top: var(--b2) !important; }
.maplibregl-ctrl-group button:hover { background: var(--accent) !important; }

.maplibregl-ctrl-scale {
  border: var(--b2) !important;
  border-top: 0 !important;
  background: var(--paper) !important;
  color: var(--text) !important;
  font-family: var(--font) !important;
  font-size: 12px !important;
  padding: 1px 6px !important;
}

.maplibregl-ctrl-attrib {
  background: var(--paper) !important;
  border: var(--b2);
  border-right: 0;
  border-bottom: 0;
  font-family: var(--font) !important;
  font-size: 11px !important;
  color: var(--text-2) !important;
}

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

.maplibregl-ctrl-attrib-button {
  background-color: var(--paper) !important;
  border-radius: 0 !important;
}

.maplibregl-ctrl-bottom-right { margin-bottom: 0; }

/* The corner is a row: the scale beside the button group, both sitting on
   the same baseline, the scale an object of the same system. */
/* MapLibre prepends into a bottom corner, so the children are scale, buttons,
   attribution. The scale and the buttons are a row whose bottom is level
   with the legend's; the attribution is taken out of the flow and sits
   beneath them along the edge. */
.maplibregl-ctrl-bottom-right {
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  gap: 10px;
  padding: 0 10px 38px 0;
}

.maplibregl-ctrl-bottom-right .maplibregl-ctrl { margin: 0; }

.maplibregl-ctrl-bottom-right .maplibregl-ctrl-attrib {
  position: absolute;
  right: 10px;
  bottom: 6px;
  margin: 0;
  width: max-content;
  max-width: calc(100vw - 40px);
  white-space: nowrap;
}

.maplibregl-ctrl-bottom-right .maplibregl-ctrl-scale {
  height: 34px;
  box-sizing: border-box;
  padding: 0 10px !important;
  line-height: 30px !important;
  border: var(--b3) !important;
  border-top: var(--b3) !important;
  box-shadow: var(--shadow-sm);
  font-size: 13px !important;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.recentre-btn { display: flex !important; align-items: center; justify-content: center; }
.recentre-btn svg { display: block; }

.fatal {
  position: fixed;
  inset: auto 0 0 0;
  margin: 0;
  padding: 14px;
  text-align: center;
  background: var(--danger);
  color: var(--paper);
  border-top: var(--b4);
  font-size: 16px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: center;
}

/* Reload rather than a retry of the failed step: whatever wedged the style load
   is upstream of anything this page can re-run. */
.fatal-retry {
  flex: 0 0 auto;
  padding: 5px 14px;
  border: 3px solid var(--paper);
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
}

.fatal-retry:hover { background: var(--paper); color: var(--danger); }

/* ---------------------------------------------------------------- motion */

@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

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

@keyframes settle {
  from { opacity: 0; transform: rotate(-2deg) scale(0.86); }
  to { opacity: 1; }
}

.dest-tag { animation-name: settle-skew; }

@keyframes settle-skew {
  from { opacity: 0; transform: skew(-6deg) scale(0.8) translateY(6px); }
  to { opacity: 1; transform: skew(-6deg); }
}

.sel-dot { animation-name: settle-plain; }

@keyframes settle-plain {
  from { opacity: 0; transform: scale(0.4); }
  to { opacity: 1; transform: none; }
}

@keyframes pulse {
  0% { transform: scale(0.7); opacity: 0.9; }
  100% { transform: scale(1.9); opacity: 0; }
}

@keyframes blink {
  0% { opacity: 1; }
  100% { opacity: 0.25; }
}

/* ----------------------------------------------------------------- mobile

   Below 700px the panel is a bottom sheet. Hover does not exist here, so the
   readout is hidden and the panel's headline carries the same number.
*/

@media (max-width: 700px) {
  :root { --gap: 12px; }

  #chrome {
    width: calc(100% - 2 * var(--gap) - 62px);
    gap: 10px;
  }

  #brand {
    /* Room under it for the sticker, which on a phone hangs below the
       corner rather than beside the word: there is no width to its right,
       the theme toggle is there. */
    margin: 0 10px 32px 0;
    padding: 4px 10px 5px 8px;
    box-shadow: 8px 8px 0 -2px var(--accent), 8px 8px 0 0 var(--ink);
  }

  .brand-sticker { right: -10px; bottom: -30px; font-size: 14px; padding: 3px 9px 4px; }

  .brand-word { font-size: 18px; line-height: 1; }

  .search-eyebrow { padding-top: 4px; font-size: 11px; }
  .search-input { font-size: 18px; height: 36px; }

  .controls-summary { padding: 7px 10px; font-size: 13px; white-space: nowrap; }
  .ctl-summary { font-size: 13px; overflow: hidden; text-overflow: ellipsis; min-width: 0; }


  #theme-toggle { padding: 5px 9px; font-size: 13px; }
  .theme-label { display: none; }

  #readout { display: none; }

  #legend {
    bottom: 44px;
    width: 156px;
    padding: 6px 10px 8px;
  }

  #legend figcaption { white-space: nowrap; font-size: 11px; }

  /* The attribution collapses to its button on a phone; the text is a tap away
     and the legend is not under it. */
  .maplibregl-ctrl-attrib.maplibregl-compact:not(.maplibregl-compact-show) .maplibregl-ctrl-attrib-inner { display: none; }

  #panel {
    top: auto;
    left: 0;
    right: 0;
    width: 100%;
    max-height: 60%;
    border-left: 0;
    border-top: var(--b4);
    transform: translateY(calc(100% + 12px));
  }

  #panel::before {
    top: -16px;
    bottom: auto;
    left: 18px;
    right: 18px;
    width: auto;
    height: 16px;
    border: var(--b3);
    border-bottom: 0;
  }

  #panel.is-open { transform: none; }

  /* With the sheet up there is no room for the search and the controls too;
     the sheet is what the reader asked for, and closing it brings them back.
     The brand stays, small, so the page still says what it is. */
  #stage:has(#panel.is-open) #form { display: none; }

  .search-results { max-height: 50vh; }
  .search-primary { font-size: 16px; }

  #stage:has(#panel.is-open) #theme-toggle { right: var(--gap); }
  #stage:has(#panel.is-open) .maplibregl-ctrl-bottom-right { right: 0; }

  .panel-header { padding: 14px 16px 12px; }

  .panel-grip {
    display: block;
    position: absolute;
    top: 6px;
    left: 50%;
    transform: translateX(-50%);
    width: 40px;
    height: 5px;
    background: var(--ink);
  }

  .panel-body { padding: 14px 16px 28px; }

  .summary-num { font-size: 52px; }
  .summary-tiles { grid-template-columns: repeat(4, 1fr); gap: 6px; }
  .tile { padding: 7px 4px 8px; }
  .tile-num { font-size: 19px; }

  /* Thumb-sized targets. The leg row is the interaction that has to survive a
     thumb, so it gets the space rather than the tightest possible layout. */
  .leg { padding-top: 12px; padding-bottom: 14px; }
  .alt { padding: 9px 10px; }
  .panel-close { width: 42px; height: 42px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .sel-pulse { display: none; }
}

/* ------------------------------------------------------------ wide screens

   Bigger chrome on a bigger screen. The map keeps its pixels; the objects on
   it do not. --ui-zoom is set by ui.applyUiZoom from the viewport width —
   0.85 from a phone's width up to 1440px, about 1.0 at 1920, about 1.25 at
   2560, and 1 on a phone — and applied here to
   every object. JS that mixes screen pixels with an object's own divides by
   it (ui.uiZoom), and the map's own text and roads are scaled in the style. */

#chrome, #corner, #status, #readout, #panel, #legend, .pc,
.maplibregl-ctrl-bottom-right, .maplibregl-ctrl-bottom-left {
  zoom: var(--ui-zoom);
}

/* --------------------------------------------------------------- entrance

   The page arrives in an order, once, keyed off #app.is-ready so nothing
   plays before the map is there to play over. The map dollies in (main.js).
   The brand placard stamps down; its letters rise up out of it one by one;
   the ink and pink backplates slide out behind it. Then the search, the
   controls, the theme sign, the legend and the zoom buttons file in, each a
   beat after the last. Reduced motion collapses all of it to an appearance
   (see the block above). */

/* Hidden until ready, then animated with a backwards fill: the "from" frame
   holds through each delay and the natural state is the end state, so no
   animation leaves a transform pinned on an element that hovers. */
#app:not(.is-ready) :is(#brand, #form, #theme-toggle, #legend,
.maplibregl-ctrl-bottom-right, .maplibregl-ctrl-bottom-left) { opacity: 0; }
#app:not(.is-ready) .brand-letter { transform: translateY(115%); }

#app.is-ready #brand {
  animation:
    stamp 760ms var(--snap) 160ms backwards,
    backplate 640ms var(--ease) 760ms backwards;
}

#app.is-ready .brand-letter {
  animation: letter-rise 680ms var(--snap) calc(380ms + var(--i, 0) * 42ms) backwards;
}

/* The sticker slaps on after the word has risen. */
#app:not(.is-ready) .brand-sticker { opacity: 0; }
#app.is-ready .brand-sticker { animation: slap 520ms var(--snap) 1250ms backwards; }

@keyframes slap {
  from { opacity: 0; transform: skew(-6deg) scale(1.6); }
  70%  { opacity: 1; transform: skew(-6deg) scale(0.96); }
  to   { opacity: 1; transform: skew(-6deg); }
}

/* The form arrives as one placard, then its rows settle in one after
   another from the top; a row that appears later — the second workplace —
   rises the same way when it does. */
#app.is-ready #form { animation: file-in 640ms var(--ease) 820ms backwards; }
#app.is-ready #form > * { animation: row-in 560ms var(--ease) calc(1120ms + var(--row, 0) * 140ms) backwards; }
#form > :nth-child(1) { --row: 0; }
#form > :nth-child(2) { --row: 1; }
#form > :nth-child(3) { --row: 2; }
#form > :nth-child(4) { --row: 3; }
#form > :nth-child(5) { --row: 4; }
#search2:not([hidden]) > .search-field { animation: row-in 320ms var(--ease) both; }

@keyframes row-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
#app.is-ready #theme-toggle { animation: sign-drop 640ms var(--snap) 1100ms backwards; }
#app.is-ready #legend { animation: file-in 640ms var(--ease) 1400ms backwards; }
#app.is-ready .maplibregl-ctrl-bottom-right { animation: from-right 640ms var(--ease) 1560ms backwards; }
#app.is-ready .maplibregl-ctrl-bottom-left { animation: file-in 640ms var(--ease) 1700ms backwards; }

/* The placard's own transform is the rotation; every keyframe carries it. */
@keyframes stamp {
  from { opacity: 0; transform: rotate(-5deg) scale(1.16) translateY(-10px); }
  60%  { opacity: 1; transform: rotate(-1deg) scale(0.98); }
  to   { opacity: 1; transform: rotate(-1.5deg); }
}

@keyframes backplate {
  from { box-shadow: 0 0 0 -3px var(--accent), 0 0 0 0 var(--ink); }
  to   { box-shadow: 11px 11px 0 -3px var(--accent), 11px 11px 0 0 var(--ink); }
}

@keyframes letter-rise {
  from { transform: translateY(115%); }
  to   { transform: none; }
}

@keyframes file-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* The theme sign is skewed; the keyframe keeps the skew. */
@keyframes sign-drop {
  from { opacity: 0; transform: skew(-6deg) translateY(-18px); }
  to   { opacity: 1; transform: skew(-6deg); }
}

@keyframes from-right {
  from { opacity: 0; transform: translateX(18px); }
  to   { opacity: 1; transform: none; }
}
