/* ================================================================
 * style.css — Adur & Worthing Live Bus Tracker
 * Aesthetic: Coastal — warm sandy light mode, deep ocean dark mode
 * Fonts loaded via <link> in index.html
 * ================================================================ */

/* ===== DESIGN TOKENS — Coastal Sand (light, default) ===== */
:root {
  /* Typography */
  --font-brand:         'Fraunces', Georgia, serif;
  --font-family:        'Outfit', system-ui, sans-serif;
  --font-mono:          'JetBrains Mono', 'Courier New', monospace;

  /* Brand / interactive */
  --color-primary:        #1a4b82;
  --color-primary-dark:   #0f3264;
  --color-primary-light:  #e4edf7;
  --color-accent:         #c07808;

  /* Same two colours as bare channels, for translucent overlays and glows.
     Written out because rgba() cannot take a hex, and a glow that hardcodes
     its own copy of the accent stops following the token — which is exactly
     what had happened: four pulses kept the light ochre in dark mode while
     the thing they surrounded had turned bright amber. */
  --color-primary-rgb:    26, 75, 130;
  --color-accent-rgb:     192, 120, 8;

  /* The accent is a fill colour first. As *text* on a light surface it only
     reaches 3.23:1, so light mode needs a darker draw of the same hue; dark
     mode already clears AA comfortably and just reuses the accent. Measured
     against --color-bg rather than --color-surface, because the tab bar sits
     on the darker of the two and that is where it is tightest. */
  --color-accent-text:    #8a5808;

  /* ...and white on an accent fill is 3.54:1 here, 2.10:1 in dark mode. Dark
     text is the fix, which the codebase had already worked out for two of the
     five places it happens — this generalises it to all of them. */
  --color-on-accent:      #18202e;

  /* Link text. In light mode this is just the primary; in dark mode the
     primary sits at 3.4-3.7:1 on the panel and footer surfaces, so links get
     a lighter draw of it there. */
  --color-link:           #1a4b82;

  /* Destructive actions. */
  --color-danger:         #c0392b;
  --color-danger-tint:    rgba(192, 57, 43, 0.08);

  /* Status */
  --color-on-time:        #1c7844;
  --color-early:          #1a70b5;
  --color-late:           #bc3220;
  --color-unknown:        #6a5e58;

  /* Status chip fills. Light mode uses hand-picked opaque tints, dark mode
     lays the status colour over the surface at low alpha — the two are not
     interconvertible, so both are named here and the chip rules no longer
     need to know which technique is in play. */
  --tint-on-time-bg:       #ebf7f1;
  --tint-on-time-border:   #c2e8d5;
  --tint-early-bg:         #e3f3f9;
  --tint-early-border:     #b5d8ec;
  --tint-late-bg:          #fbecea;
  --tint-late-border:      #f2c0ba;
  --tint-scheduled-bg:     var(--color-surface-2);
  --tint-scheduled-border: var(--color-border);

  /* Neutrals — warm sandy palette */
  --color-bg:             #edeae3;
  --color-surface:        #f7f4ef;
  --color-surface-2:      #e8e4dc;
  --color-border:         #cfc9be;
  --color-text:           #18202e;
  --color-text-muted:     #6a625a;

  /* Text and iconography sitting on a saturated fill — the header bar, a
     service badge, an operator chip. White in BOTH themes, because those
     fills stay dark in both.

     There used to be a --color-text-inverse here, and it was the trap: it
     flips to the page background in dark mode, so anything reaching for it
     to label a badge would have gone black. Nothing ever used it — twenty
     sites hardcoded #fff instead, which is the tell. Removed, and replaced
     with a token that means what those sites needed. */
  --color-on-fill:        #ffffff;
  --color-on-fill-rgb:    255, 255, 255;

  /* Font sizes — 7-step scale. Components must use these, not one-off rems,
     so type lines up to a shared rhythm. Each step maps a cluster of the
     near-identical sizes the UI had drifted into. */
  --font-size-2xs:        0.65rem;   /* micro uppercase labels, table th, badges */
  --font-size-xs:         0.74rem;   /* small labels, chips, captions */
  --font-size-sm:         0.82rem;   /* secondary body, blurbs, small buttons */
  --font-size-md:         0.9rem;    /* body / inputs / card summary */
  --font-size-base:       1rem;      /* html/body default */
  --font-size-lg:         1.125rem;  /* panel + section titles */
  --font-size-xl:         1.35rem;

  /* Spacing. 2xs was 6px and xs was 4px, so the scale ran backwards and
     anyone reaching for a name got the wrong one. The names swapped; every
     usage kept the value it already rendered. */
  --space-2xs: 0.25rem;   /* 4px */
  --space-xs:  0.375rem;  /* 6px — the most common gap in the UI */
  --space-sm:  0.5rem;
  --space-md:  1rem;
  --space-lg:  1.5rem;
  --space-xl:  2rem;

  /* Radii */
  --radius-sm:   4px;
  --radius-md:   8px;
  --radius-lg:   14px;
  --radius-pill: 100px;

  /* Shadows */
  --shadow-sm: 0 1px 3px rgba(10,18,32,0.08);
  --shadow-md: 0 4px 14px rgba(10,18,32,0.12);
  --shadow-lg: 0 8px 28px rgba(10,18,32,0.16);

  --transition:      180ms ease;
  --transition-fast: 110ms ease;
  --transition-slow: 320ms cubic-bezier(0.34, 1.56, 0.64, 1);

  /* Focus ring — see the :focus-visible rule. Named so components that need
     to opt out of the offset (map controls, flush-edge buttons) can. */
  /* WCAG 2.5.8 sets the AA floor at 24x24. 44 is the AAA criterion and
     Apple's number, and it is what the controls here aim for — the site is
     lobbying bodies that are themselves legally bound to AA, so arriving
     under the usability numbers is a free reason to dismiss it. */
  --target-min: 44px;

  /* The three header controls — section nav, status pill, theme toggle — were
     52px, 44px and 44px, so a row meant to read as one set of controls was
     visibly ragged. One token gives them a single height; it is deliberately
     equal to --target-min, so unifying them does not quietly trade away the
     AAA target size above. --header-control-r is a little rounder than the
     --radius-md they used to share, and is scoped to this row rather than
     applied to the token, which the whole site draws on. */
  --header-control-h: 44px;
  --header-control-r: 10px;

  --focus-ring-width:  2px;
  --focus-ring-offset: 2px;
  --focus-ring-color:  var(--color-accent);

  --header-height: 60px;
  --panel-width:   360px;

  /* Bottom-sheet detents (mobile only).

     svh rather than dvh, deliberately: dvh recalculates as the browser's own
     chrome collapses and expands, which shifts the layout mid-scroll. svh is
     the small-viewport height and stays put. The plain vh line above each is
     the fallback for engines without svh — later declaration wins where it
     is understood.

     peek is measured, not guessed: the chrome above the board (handle, tab
     strip, stop header, meta row) comes to 197px and a departure row is
     45px, so 248 shows the stop name and the next departure — which is the
     whole question someone glances at a bus app to answer. */
  --sheet-peek: 248px;
  --sheet-half: 48vh;
  --sheet-half: 48svh;
  --sheet-full: 88vh;
  --sheet-full: 88svh;
  --sheet-handle-height: 44px;

  /* Match native UI (scrollbars, form controls, caret) to the theme. */
  color-scheme: light;
}

/* ===== DARK MODE — Coastal Night ===== */
html.dark-mode {
  color-scheme: dark;
  --color-primary:        #2872c2;
  --color-primary-dark:   #1a508e;
  --color-primary-light:  #091523;
  --color-accent:         #e6a820;
  --color-primary-rgb:    40, 114, 194;
  --color-accent-rgb:     230, 168, 32;
  --color-accent-text:    #e6a820;   /* 8.7:1 on the dark surface — no adjustment needed */
  --color-on-accent:      #090e1a;
  --color-link:           #4a8fd0;   /* --color-primary is 3.4:1 here */

  /* #c0392b measures 3.4:1 against the dark surface — under AA. This is the
     same red the "late" status already uses here, at 4.9:1. */
  --color-danger:         #e84e3c;
  --color-danger-tint:    rgba(232, 78, 60, 0.12);

  --color-on-time:        #34c06a;
  --color-early:          #28b4c4;
  --color-late:           #ee5f4d;   /* was #e84e3c — 4.36:1 on its own tint */
  --color-unknown:        #748ca2;   /* was #5a7488 — 3.40:1 on the scheduled chip */

  --tint-on-time-bg:       rgba(52, 192, 106, 0.12);
  --tint-on-time-border:   rgba(52, 192, 106, 0.28);
  --tint-early-bg:         rgba(40, 180, 196, 0.12);
  --tint-early-border:     rgba(40, 180, 196, 0.28);
  --tint-late-bg:          rgba(232, 78, 60, 0.12);
  --tint-late-border:      rgba(232, 78, 60, 0.28);
  --tint-scheduled-bg:     rgba(90, 116, 136, 0.1);
  --tint-scheduled-border: rgba(90, 116, 136, 0.22);

  --color-bg:             #090e1a;
  --color-surface:        #0d1525;
  --color-surface-2:      #111e30;
  --color-border:         #1a2c44;
  --color-text:           #c8d8ec;
  --color-text-muted:     #748ba9;   /* was #5a7898 — 3.97:1 on the surface, and this
                                        token carries 80 usages, so it was most of the
                                        dark-mode contrast failures on its own */

  --shadow-sm: 0 1px 4px rgba(0,0,0,0.45);
  --shadow-md: 0 4px 16px rgba(0,0,0,0.55);
  --shadow-lg: 0 8px 32px rgba(0,0,0,0.65);
}

/* ===== ICONS (Lucide via sprite) ===== */
.icon {
  width: 1.05em;
  height: 1.05em;
  flex-shrink: 0;
  display: inline-block;
  vertical-align: -0.15em;
  stroke-width: 2;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon-lg { width: 1.4em; height: 1.4em; }
.icon-xl { width: 2em;   height: 2em;   }

/* ===== RESET ===== */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* The UA stylesheet's [hidden] { display: none } is a plain author-level-zero
   rule, so ANY author `display` beats it. .btn-icon sets display:flex, which
   meant `el.hidden = true` did nothing: the bus and rail layer toggles have
   been sitting in the header in every view, including the three where they
   do nothing. Every reset worth the name carries this rule for that reason. */
[hidden] { display: none !important; }

/* Suppress the grey flash Android paints over any tapped element. The site
   has its own :active states, and the two together read as a glitch. */
button, a, [role="option"], [role="button"], input, select, textarea, summary {
  -webkit-tap-highlight-color: transparent;
}

html, body {
  height: 100%;
  overflow: hidden;
}

body {
  font-family: var(--font-family);
  font-size: var(--font-size-base);
  color: var(--color-text);
  background: var(--color-bg);
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--color-link); }
button { font-family: inherit; cursor: pointer; }

/* ===== HEADER ===== */
.site-header {
  height: var(--header-height);
  background: var(--color-primary);
  border-bottom: 1px solid var(--color-primary-dark);
  box-shadow: var(--shadow-md);
  z-index: 1000;
  flex-shrink: 0;
}

html.dark-mode .site-header {
  background: var(--color-surface);
  border-bottom-color: var(--color-border);
  box-shadow: var(--shadow-sm);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 100%;
  padding: 0 var(--space-lg);
}

.header-brand {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.header-icon {
  font-size: 1.3rem;
  line-height: 1;
}

.header-title {
  font-family: var(--font-brand);
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1;
  color: var(--color-on-fill);
  letter-spacing: -0.01em;
}

html.dark-mode .header-title {
  color: var(--color-accent);
}

.header-controls {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.last-updated {
  font-family: var(--font-mono);
  font-size: var(--font-size-2xs);
  font-weight: 600;
  color: rgba(var(--color-on-fill-rgb), 0.92);
  white-space: nowrap;
  display: inline-flex;
  align-items: baseline;
}

html.dark-mode .last-updated {
  color: var(--color-text);
}

/* Status pill: groups the connecting/updated label + bus-visibility toggle so
   they read as a single "live data" control. The bus toggle nests inside as a
   smaller circular dot; dark-mode toggle lives outside (intentionally distinct). */
.status-pill {
  position: relative;   /* anchors the bus filter popover */
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  /* 44px total, holding 44px toggles. The border is an inset shadow rather
     than a real border so it costs no box height: with a 1px border the
     content box would be 42px, and the toggles would have to shrink below
     the 44px target size the site holds. The pill used to be 52px for
     exactly that reason — 44px toggles plus padding plus border. */
  height: var(--header-control-h);
  padding: 0 4px 0 10px;
  background: rgba(var(--color-on-fill-rgb), 0.15);
  box-shadow: inset 0 0 0 1px rgba(var(--color-on-fill-rgb), 0.25);
  border-radius: var(--header-control-r);
}
html.dark-mode .status-pill {
  background: var(--color-surface-2);
  box-shadow: inset 0 0 0 1px var(--color-border);
}
.status-pill .btn-icon.status-pill-toggle {
  /* Full 44px: these are the targets, not the pill. Shrinking them to fit a
     shorter pill trades away the target size rather than the padding, which
     is the wrong half of the problem to solve. */
  width: var(--target-min);
  height: var(--target-min);
  border-radius: calc(var(--header-control-r) - 3px);
  background: transparent;
  border: none;
}
.status-pill .btn-icon.status-pill-toggle .icon {
  width: 14px;
  height: 14px;
}@media (hover: hover) {

.status-pill .btn-icon.status-pill-toggle:hover {
  background: rgba(var(--color-on-fill-rgb), 0.18);
}
}@media (hover: hover) {

html.dark-mode .status-pill .btn-icon.status-pill-toggle:hover {
  background: var(--color-border);
}
}

/* Cascading "., .., ..." loading dots — width-reveal in 4 discrete steps so
   the dots accumulate left-to-right ('' → . → .. → ...) instead of waving. */
.last-updated .loading-dots {
  display: none;
  margin-left: 2px;
  width: 3ch;
  overflow: hidden;
  vertical-align: baseline;
  white-space: nowrap;
}
.last-updated.is-loading .loading-dots { display: inline-block; }
.last-updated.is-loading .loading-dots::before {
  content: "...";
  display: inline-block;
  width: 0;
  overflow: hidden;
  animation: lu-dot-grow 1.4s steps(4, jump-none) infinite;
}
@keyframes lu-dot-grow {
  from { width: 0;   }
  to   { width: 3ch; }
}
@media (prefers-reduced-motion: reduce) {
  .last-updated.is-loading .loading-dots::before { animation: none; width: 3ch; }
}

/* Failure tint so "Update failed — retrying" doesn't look identical to a
   normal connect (both bold + animated). Amber = "still trying, but degraded". */
.last-updated.is-error .status-pill-label,
.last-updated.is-error .loading-dots { color: #ffcf80; }
html.dark-mode .last-updated.is-error .status-pill-label,
html.dark-mode .last-updated.is-error .loading-dots { color: #ffb84d; }

/* Section nav dropdown — replaces the segmented Live/Improvements toggle.
   Trigger is styled like the existing .btn-icon family so it reads as part
   of the same control row; menu hangs beneath, anchored to the trigger. */
.section-nav { position: relative; }

.section-nav-trigger {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  background: rgba(var(--color-on-fill-rgb), 0.15);
  border: 1px solid rgba(var(--color-on-fill-rgb), 0.25);
  border-radius: var(--header-control-r);
  color: var(--color-on-fill);
  height: var(--header-control-h);
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 6px 10px;
  cursor: pointer;
  transition: background var(--transition);
  white-space: nowrap;
}@media (hover: hover) {

.section-nav-trigger:hover { background: rgba(var(--color-on-fill-rgb), 0.25); }
}
.section-nav-chevron { width: 12px; height: 12px; transition: transform var(--transition); }
.section-nav-trigger[aria-expanded="true"] .section-nav-chevron { transform: rotate(180deg); }

.section-nav-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  margin: 0;
  padding: var(--space-2xs);
  list-style: none;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0,0,0,0.18);
  min-width: 160px;
  z-index: 1100;
}
.section-nav-menu.hidden { display: none; }

.section-nav-menu [role="option"] {
  display: block;
  padding: 7px 12px;
  border-radius: calc(var(--radius-md) - 4px);
  font-size: var(--font-size-sm);
  font-weight: 500;
  color: var(--color-text);
  cursor: pointer;
  user-select: none;
}
.section-nav-menu [role="option"]:focus {
  background: var(--color-surface-2);
  outline: none;
}
.section-nav-menu [role="option"][aria-selected="true"] {
  font-weight: 700;
  color: var(--color-primary);
}
html.dark-mode .section-nav-menu [role="option"][aria-selected="true"] {
  color: var(--color-accent);
}

html.dark-mode .section-nav-trigger {
  background: var(--color-surface-2);
  border-color: var(--color-border);
  color: var(--color-text);
}@media (hover: hover) {

html.dark-mode .section-nav-trigger:hover { background: var(--color-border); }
}

.btn-icon {
  background: rgba(var(--color-on-fill-rgb), 0.15);
  border: 1px solid rgba(var(--color-on-fill-rgb), 0.25);
  border-radius: var(--radius-md);
  color: rgba(var(--color-on-fill-rgb), 0.9);
  width: var(--target-min);
  height: var(--target-min);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-md);
  transition: background var(--transition), border-color var(--transition);
}@media (hover: hover) {


.btn-icon:hover {
  background: rgba(var(--color-on-fill-rgb), 0.25);
}
}
/* "Off" state for toggle buttons (e.g. buses hidden) — dimmed so the
   current state reads at a glance. */
.btn-icon[aria-pressed="false"] {
  opacity: 0.45;
}

html.dark-mode .btn-icon {
  background: var(--color-surface-2);
  border-color: var(--color-border);
  color: var(--color-text-muted);
}@media (hover: hover) {


html.dark-mode .btn-icon:hover {
  background: var(--color-border);
  color: var(--color-text);
}
}

/* Dark-mode toggle — circular, warm-tinted disc in light mode, cool disc in
   dark mode. Reads as a global preference control, not a sibling of the
   live-data icons inside the status pill. */
.btn-icon.btn-theme {
  /* Sized with the row so the three line up, but still a circle: it is a
     global preference, not a sibling of the live-data controls. */
  width: var(--header-control-h);
  height: var(--header-control-h);
  border-radius: 50%;
  background: linear-gradient(135deg,
              rgba(255,210,140,0.30),
              rgba(var(--color-on-fill-rgb), 0.05));
  border: 1px solid rgba(255,210,140,0.60);
  color: #fff8e6;
  box-shadow: 0 0 0 2px rgba(255,210,140,0.10);
}@media (hover: hover) {

.btn-icon.btn-theme:hover {
  background: linear-gradient(135deg,
              rgba(255,210,140,0.45),
              rgba(var(--color-on-fill-rgb), 0.10));
}
}
html.dark-mode .btn-icon.btn-theme {
  background: linear-gradient(135deg,
              rgba(120,180,255,0.22),
              rgba(var(--color-on-fill-rgb), 0.03));
  border-color: rgba(150,195,255,0.55);
  color: #e6f0ff;
  box-shadow: 0 0 0 2px rgba(120,180,255,0.10);
}@media (hover: hover) {

html.dark-mode .btn-icon.btn-theme:hover {
  background: linear-gradient(135deg,
              rgba(120,180,255,0.32),
              rgba(var(--color-on-fill-rgb), 0.06));
}
}

/* ===== MAIN LAYOUT ===== */
.main-layout {
  display: flex;
  flex: 1;
  overflow: hidden;
  position: relative;
}

/* ===== MAP SECTION ===== */
.map-section {
  flex: 1;
  position: relative;
  overflow: hidden;
}

#map {
  width: 100%;
  height: 100%;
}

/* Centered dialog card floating over a live, functional map. The map
   loads its stops/vehicles behind it; the dialog fades out once loaded. */
.map-loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  padding: var(--space-lg) calc(var(--space-lg) + 6px);
  z-index: 600;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  transition: opacity 400ms ease;
}

.map-loading.hidden {
  opacity: 0;
  pointer-events: none;
}

html.dark-mode .map-loading {
  background: var(--color-surface);
  border-color: var(--color-border);
  color: var(--color-text-muted);
}

/* ===== SPINNER ===== */
.spinner {
  width: 36px;
  height: 36px;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: spin 0.9s linear infinite;
}

.spinner.small {
  width: 22px;
  height: 22px;
  border-width: 2.5px;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ===== DEPARTURE PANEL ===== */
.departure-panel {
  width: var(--panel-width);
  background: var(--color-surface);
  border-left: 1px solid var(--color-border);
  box-shadow: var(--shadow-lg);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  z-index: 900;
  flex-shrink: 0;
}

@media (max-width: 900px) {
  .main-layout {
    flex-direction: column;
  }
  /* min-height:0 lets the map actually shrink; without it a flex item
     refuses to go below its content size and the sheet cannot grow. */
  .map-section {
    min-height: 0;
  }

  /* The panel becomes a bottom sheet with three detents, replacing a fixed
     45vh slab that could show 5 of 10 departures with no way to show more.

     It *overlays* the map rather than sitting below it. Two reasons: the
     translucent handle strip only means something if there is map behind it,
     and a sheet that shares the height with the map resizes the map on every
     detent change — which left an unpainted grey band until Leaflet was told
     about it. Out of flow, the map is always full height and never needs to
     be told anything. */
  .departure-panel {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 900;
    width: 100%;
    border-left: none;
    border-top: none;
    height: var(--sheet-h, var(--sheet-half));
    max-height: var(--sheet-full);
    /* Keeps the last row clear of the home indicator. Non-zero only because
       of viewport-fit=cover on the viewport meta. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    transition: height var(--transition);
  }

  /* On <body> rather than on the panel, so anything else that has to sit
     clear of the sheet — Leaflet's attribution, below — can read it too. */
  body[data-sheet="peek"] { --sheet-h: var(--sheet-peek); }
  body[data-sheet="half"] { --sheet-h: var(--sheet-half); }
  body[data-sheet="full"] { --sheet-h: var(--sheet-full); }

  /* Leaflet's attribution is a licence condition, so it may not be parked
     under the sheet. */
  .leaflet-bottom { bottom: var(--sheet-h, var(--sheet-half)); }

  /* During a drag the height follows the pointer, so it must not also be
     easing towards it. */
  body.sheet-dragging .departure-panel { transition: none; }
  body.sheet-dragging { cursor: grabbing; user-select: none; }
}

/* ===== DEPARTURE BOARD ON NARROW SCREENS =====
   Four columns do not fit a 390px phone. With table-layout:fixed at
   54/auto/82/86 the status chip landed 6px past the right edge on every
   single row, so the column a passenger most needs — is it on time — was
   the one getting clipped.

   Below the breakpoint the row becomes a grid and the chip moves underneath
   the destination, where there is room to read it. The badge and the due
   time span both rows, so the eye still runs service -> where -> when.

   Scoped to #departures-tbody on purpose: the rail board reuses
   .departures-table with five columns of its own. */
@media (max-width: 900px) {
  #departures-container .departures-table,
  #departures-tbody,
  #departures-tbody tr,
  #departures-tbody td {
    display: block;
    width: auto;
  }

  /* The header labelled a column order that no longer exists here. */
  #departures-container .departures-table thead { display: none; }

  #departures-tbody .departure-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      "badge dest due"
      "badge status due";
    align-items: center;
    column-gap: var(--space-sm);
    row-gap: 2px;
    padding: var(--space-2xs) var(--space-sm);
    border-bottom: 1px solid var(--color-border);
  }
  #departures-tbody .departure-row td {
    border-bottom: none;
    padding: 0;
  }
  #departures-tbody .departure-row td:nth-child(1) { grid-area: badge; }
  #departures-tbody .departure-row td:nth-child(2) { grid-area: dest;   min-width: 0; }
  #departures-tbody .departure-row td:nth-child(3) { grid-area: due;    text-align: right; }
  #departures-tbody .departure-row td:nth-child(4) { grid-area: status; }

  /* The chip is a qualifier on the destination now, not a column heading. */
  #departures-tbody .status-chip { padding: 1px 6px; }
}

/* ===== SHEET SPACE BUDGET =====
   On a 405px sheet the chrome above the first departure came to 246px:
   handle, tab strip, a two-line stop header and a meta row. The board is
   what people opened the panel for, so the chrome gives ground. */
@media (max-width: 900px) {
  /* The ATCO code is worth keeping — it is what a stop-fault report is keyed
     on — but it does not deserve its own line directly under the name. It
     goes beside it, right-aligned and quiet. */
  .panel-mode[data-mode="live"] .panel-header-text {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
  }
  .panel-mode[data-mode="live"] .panel-stop-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .panel-mode[data-mode="live"] .panel-stop-id {
    margin-top: 0;
    flex-shrink: 0;
    font-size: var(--font-size-2xs);
  }

  .panel-header   { padding: var(--space-sm) var(--space-md); }
  .departures-meta { padding: 2px var(--space-sm); }
}

/* ===== ROUTE VIEW FILTER DISCLOSURE ===== */
.filter-disclosure {
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
  flex-shrink: 0;
}
.filter-disclosure-summary {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--target-min);
  padding: 0 var(--space-md);
  cursor: pointer;
  list-style: none;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  user-select: none;
}
.filter-disclosure-summary::-webkit-details-marker { display: none; }
.filter-disclosure-summary .icon { width: 15px; height: 15px; flex-shrink: 0; }
.filter-disclosure-chevron {
  margin-left: auto;
  transition: transform var(--transition);
}
.filter-disclosure[open] .filter-disclosure-chevron { transform: rotate(180deg); }
.filter-disclosure[open] .filter-disclosure-summary { color: var(--color-text); }

/* ===== BOUNDARY EVIDENCE DIALOG =====
   What the line on the map actually means, with the numbers behind it. A
   native <dialog>, so Escape, the backdrop and focus containment come from the
   platform instead of being reimplemented badly. */
.evidence-dialog {
  width: min(560px, calc(100vw - 32px));
  max-height: min(80vh, 780px);
  /* A modal <dialog> is centred by the UA's own `margin: auto`, which the
     universal reset near the top of this file sets to 0 along with everything
     else. Without this both dialogs open pinned to the top-left corner. */
  margin: auto;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}
.evidence-dialog::backdrop { background: rgba(8, 14, 24, 0.55); }
.evidence-body {
  position: relative;
  max-height: min(80vh, 780px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-lg) var(--space-md) var(--space-md);
}
.evidence-close {
  position: absolute;
  top: 6px;
  right: 6px;
  width: var(--target-min);
  height: var(--target-min);
  appearance: none;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  font-size: 1.5rem;
  line-height: 1;
  color: var(--color-text-muted);
  cursor: pointer;
}
@media (hover: hover) {
  .evidence-close:hover { background: var(--color-surface-2); color: var(--color-text); }
}
.evidence-eyebrow {
  margin: 0 0 4px;
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.evidence-title {
  margin: 0 0 var(--space-xs);
  padding-right: var(--target-min);
  font-family: var(--font-brand, var(--font-family));
  font-size: var(--font-size-lg);
  font-weight: 800;
  line-height: 1.2;
}
.evidence-standfirst {
  margin: 0 0 var(--space-md);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-muted);
}
.evidence-loading {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

.evidence-days { display: flex; flex-direction: column; gap: var(--space-md); }
.evidence-day {
  padding: var(--space-sm);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.evidence-day-title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs);
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-md);
  font-weight: 700;
}
.evidence-day-ratio {
  font-size: var(--font-size-xs);
  font-weight: 700;
  color: var(--color-accent-text);
  font-variant-numeric: tabular-nums;
}

/* Bars run from zero and carry their own number. A truncated axis would turn
   two thirds into a visual wipeout, and the case does not need overstating.
   The value is text, so nothing here depends on seeing the bar at all. */
.evidence-bar-row {
  display: grid;
  grid-template-columns: 7.5rem 1fr auto;
  align-items: center;
  gap: var(--space-xs);
  margin-bottom: 5px;
}
.evidence-bar-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
}
.evidence-bar-track {
  display: block;
  height: 15px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 3px;
  overflow: hidden;
}
.evidence-bar { display: block; height: 100%; }
.evidence-bar--west { background: #1a4b82; }
.evidence-bar--east { background: #6a4ea3; }
.evidence-bar-value {
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
  min-width: 3.2rem;
  text-align: right;
}
.evidence-day-note {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* The place panel measures a different geography from the two above it — ONS
   ward and division polygons rather than a distance band — so it is marked as
   a different kind of thing rather than read as a third day. The rule is
   structural, not decorative: remove the panel and the rule goes with it. */
.evidence-day--place {
  border-left: 3px solid var(--color-accent);
  background: var(--color-surface);
}
.evidence-place-eyebrow {
  margin: 0 0 2px;
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.evidence-axis {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

.evidence-method {
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
  font-size: var(--font-size-sm);
}
.evidence-method summary {
  min-height: var(--target-min);
  display: flex;
  align-items: center;
  cursor: pointer;
  font-weight: 700;
  color: var(--color-link);
}
.evidence-method p, .evidence-method li { color: var(--color-text-muted); line-height: 1.55; }
.evidence-method ol { margin: 0 0 var(--space-sm); padding-left: 1.2rem; }
.evidence-caveats-title {
  margin: var(--space-sm) 0 var(--space-2xs);
  font-weight: 700;
  color: var(--color-text) !important;
}
.evidence-caveats { list-style: none; padding: 0; margin: 0; }
.evidence-caveat {
  padding: var(--space-xs) 0 var(--space-xs) var(--space-sm);
  border-left: 3px solid var(--color-border);
  margin-bottom: var(--space-xs);
  font-size: var(--font-size-xs);
}
/* The caveat that weakens our own case is the one worth seeing first, so it is
   the one that gets the accent — and it is labelled in words, not just tinted. */
.evidence-caveat--understates { border-left-color: var(--color-accent); }
.evidence-caveat-tag {
  display: block;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text);
  margin-bottom: 2px;
}
.evidence-caveat em { color: var(--color-text); font-style: normal; font-weight: 600; }
.evidence-provenance {
  margin-top: var(--space-sm);
  font-size: var(--font-size-xs);
  line-height: 1.55;
}
.evidence-provenance .mono, .evidence-method .mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.94em;
}

/* Tickets whose zone covers the ground but which no operator serving both ends
   accepts. Listed rather than hidden: "why isn't the Metrovoyager an option?"
   is a fair question, and the answer is the argument. Muted, not struck through
   — these are real tickets, just not for this journey. */
.journey-zones--dropped {
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px dashed var(--color-border);
  opacity: 0.85;
}
.journey-zones--dropped .journey-zones-title { color: var(--color-text-muted); }
.journey-zones--dropped li { color: var(--color-text-muted); }

/* ===== THE JOURNEY, DRAWN =====
   Route liveries on the map, so a two-operator trip is visibly two trips. */
.journey-marker-icon { background: none; border: 0; }
.journey-marker {
  display: inline-block;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: var(--font-size-xs);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* The divIcon has no size of its own, so the pill centres itself on the
     point rather than hanging off its top-left corner. */
  transform: translate(-50%, -50%);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9), 0 1px 3px rgba(0, 0, 0, 0.3);
}
.journey-marker--change {
  border: 1px solid var(--color-border);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 0.66rem;
}

.journey-itinerary {
  margin: var(--space-sm) 0;
  padding: var(--space-sm);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.journey-itinerary-line {
  margin: 0 0 var(--space-2xs);
  font-size: var(--font-size-sm);
  line-height: 1.7;
}
.journey-leg-chip {
  display: inline-block;
  padding: 1px 7px;
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

/* ===== JOURNEY PRESETS =====
   Worked examples above the checker's form. Chips rather than a list: they are
   shortcuts, and should not compete with the two fields underneath them. */
.jc-presets { margin: 0 0 var(--space-sm); }
.jc-presets-label {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.jc-presets-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}
.jc-preset {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  /* 2.5.8 AAA: the chip is the target, so it carries the full 44px. */
  min-height: var(--target-min);
  padding: 5px 11px;
  appearance: none;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-sm);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition);
}
@media (hover: hover) {
  .jc-preset:hover {
    background: var(--color-surface);
    border-color: var(--color-accent);
  }
}
.jc-preset-label {
  font-size: var(--font-size-sm);
  font-weight: 700;
  color: var(--color-text);
}
/* The note carries the point of the example, so it is text rather than a
   colour cue — nothing here depends on the accent stripe being seen. */
.jc-preset-note {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* ===== SERVICE SPAN =====
   When buses actually run from a stop, by day. Collapsed by default: the
   departure board is what most people came for, and this is the thing they
   look for once it tells them nothing is coming. */
.stop-span { padding: 0 var(--space-md) var(--space-xs); }

.stop-span-disclosure {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  overflow: hidden;
}
.stop-span-summary {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  /* 2.5.8: the whole summary row is the target, and tall enough to be one. */
  min-height: var(--target-min);
  padding: 0 var(--space-sm);
  cursor: pointer;
  list-style: none;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  user-select: none;
}
.stop-span-summary::-webkit-details-marker { display: none; }
.stop-span-chevron { margin-left: auto; transition: transform var(--transition); }
.stop-span-disclosure[open] .stop-span-chevron { transform: rotate(180deg); }
.stop-span-disclosure[open] .stop-span-summary {
  color: var(--color-text);
  border-bottom: 1px solid var(--color-border);
}

.stop-span-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}
.stop-span-caption {
  caption-side: bottom;
  padding: var(--space-xs) var(--space-sm) var(--space-sm);
  text-align: left;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.stop-span-table thead th {
  padding: 6px var(--space-sm);
  text-align: left;
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  border-bottom: 1px solid var(--color-border);
}
.stop-span-table tbody th,
.stop-span-table tbody td {
  padding: 7px var(--space-sm);
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
  text-align: left;
}
.stop-span-table tbody tr:last-child th,
.stop-span-table tbody tr:last-child td { border-bottom: 0; }
.stop-span-table tbody th {
  font-weight: 700;
  white-space: nowrap;
  color: var(--color-text);
}

.stop-span-hours { white-space: nowrap; }
.stop-span-time { font-variant-numeric: tabular-nums; }
.stop-span-dash { color: var(--color-text-muted); margin: 0 2px; }

/* A stop with no service on a day is the finding, not an empty cell. Said in
   words as well as in tone, because WCAG 1.4.1 does not accept colour alone —
   and because "No service" is more use than a blank either way. */
.stop-span-nil {
  font-weight: 700;
  color: var(--color-danger);
}
.stop-span-row--none { background: rgba(var(--color-accent-rgb), 0.07); }

.stop-span-night {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}

.stop-span-routes {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.stop-span-route {
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.stop-span-none {
  margin: 0;
  padding: var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* ===== BUS OPERATOR FILTER =====
   Hangs from the status pill, under the bus button that opens it. Right-
   aligned so it never runs off the screen edge on a phone, where the pill
   sits near the right of the header. */
.bus-filter {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 1200;          /* over the map and its Leaflet controls */
  min-width: 232px;
  max-width: min(300px, calc(100vw - 24px));
  padding: var(--space-sm);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md, 0 6px 20px rgba(10,18,32,0.18));
  color: var(--color-text);
  text-align: left;
}
.bus-filter-title {
  margin: 0 0 var(--space-xs);
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.bus-filter-row {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  /* 2.5.8 wants 24px of target; a checkbox alone is smaller than that, so the
     whole row is the target and the row is tall enough to be one. */
  min-height: 32px;
  padding: 2px 2px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--font-size-sm);
}
@media (hover: hover) {
  .bus-filter-row:hover { background: var(--color-surface-2); }
}
.bus-filter-row input { flex: none; width: 16px; height: 16px; cursor: pointer; }
.bus-filter-row--all {
  font-weight: 700;
  padding-bottom: var(--space-xs);
  margin-bottom: var(--space-xs);
  border-bottom: 1px solid var(--color-border);
}
.bus-filter-swatch {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  border: 1px solid rgba(0,0,0,0.2);
}
.bus-filter-name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bus-filter-count {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.bus-filter-list { max-height: 46vh; overflow-y: auto; }
.bus-filter-empty {
  margin: 0;
  padding: var(--space-xs) 2px;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
/* Buses off, or some operators hidden: the button says so without a word,
   because the label it carries is only read by a screen reader or on hover. */
.status-pill .btn-icon.status-pill-toggle.is-off { opacity: 0.45; }
.status-pill .btn-icon.status-pill-toggle.is-filtered { position: relative; }
.status-pill .btn-icon.status-pill-toggle.is-filtered::after {
  content: "";
  position: absolute;
  top: 7px;
  right: 7px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent, #d98324);
  border: 1px solid var(--color-surface);
}

/* ===== COUNCIL BOUNDARY LINE =====
   The visible line is a 5px dash, which is far too thin to hover and much too
   thin to hit. A second, invisible 22px line sits on top of it carrying the
   pointer events, and lights this one up through .is-hot — so the whole length
   of the boundary responds, not just the small floating label. */
.council-boundary-hit { cursor: pointer; }

.council-boundary-line.is-hot {
  /* Both weight and opacity, because the line is dashed: thickening alone
     reads as a blur at low zoom, and brightening alone is lost against a
     busy basemap. The glow is what makes it look picked up rather than
     merely redrawn. */
  stroke-width: 9;
  stroke-opacity: 1;
  filter: drop-shadow(0 0 4px currentColor);
}
@media (prefers-reduced-motion: no-preference) {
  .council-boundary-line {
    transition: stroke-width 120ms ease-out, stroke-opacity 120ms ease-out;
  }
}

/* Hovering the line lights its label too, so the two read as one object.
   Matches the label's own :hover treatment exactly — the source of the
   pointer should not change what the highlight looks like. */
.council-boundary-label.is-hot {
  background: color-mix(in srgb, var(--color-surface) 82%, transparent);
  border-color: var(--color-border);
}

/* ===== COUNCIL BOUNDARY LABEL =====
   The dashed line is drawn by Leaflet; this is the bit that says what it is.
   A permanent tooltip, so it stays put and needs no interaction, stripped of
   every default tooltip affordance — no card, no arrow, no pointer target.
   Legibility comes from a halo rather than a panel, so the map still shows
   through the way it does behind any place name. */
.leaflet-tooltip.council-boundary-tooltip {
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  white-space: nowrap;
  pointer-events: none;
  text-align: center;
  font-family: inherit;
  /* Leaflet hard-codes #222 on its tooltips, which is invisible against a
     dark basemap. Anything here that inherits its colour — the divider —
     needs a token instead. */
  color: var(--color-text-muted);
}
.leaflet-tooltip.council-boundary-tooltip::before { display: none; }  /* arrow */

/* The label is a button: it is the way into the evidence behind the line.
   Its pane sets pointer-events: none so the label never swallows a click meant
   for a bus; this element opts back in for itself alone. */
.council-boundary-label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 2px 6px;
  appearance: none;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.01em;
  cursor: pointer;
  pointer-events: auto;
  transition: background var(--transition), border-color var(--transition);
}
@media (hover: hover) {
  .council-boundary-label:hover {
    background: color-mix(in srgb, var(--color-surface) 82%, transparent);
    border-color: var(--color-border);
  }
}
.council-boundary-label:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  background: var(--color-surface);
}
.council-boundary-label .council-boundary-caption { flex-basis: 100%; }
.council-boundary-side {
  color: var(--side-colour, var(--color-text));
  /* A halo, not a box: four offsets of the map's own background, so the words
     stay readable over coastline, sea or a route line without hiding any of
     them. paint-order keeps the stroke behind the glyph where supported. */
  text-shadow:
     1px  1px 0 var(--color-surface),
    -1px  1px 0 var(--color-surface),
     1px -1px 0 var(--color-surface),
    -1px -1px 0 var(--color-surface),
     0 0 4px var(--color-surface);
}
.council-boundary-tick {
  width: 1px;
  height: 0.9em;
  background: currentColor;
  opacity: 0.6;
  flex: none;
  /* The same halo as the text: without it the divider disappears wherever it
     crosses the boundary line it is sitting on. */
  box-shadow: 0 0 0 1px var(--color-surface);
}
.council-boundary-caption {
  display: block;
  margin-top: 1px;
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  text-shadow:
     1px  1px 0 var(--color-surface),
    -1px  1px 0 var(--color-surface),
     1px -1px 0 var(--color-surface),
    -1px -1px 0 var(--color-surface);
}

/* ===== NETWORK UPDATES =====
   These are articles, so they are laid out like articles rather than like the
   proposal cards elsewhere: a photograph, a kicker line, a headline with real
   weight, a lede, then the body. The card itself recedes — no left stripe, no
   tinted fill — because on a news list the picture and the headline are what
   should carry it. */
.update-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;                       /* clips the hero to the radius */
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}
.update-card + .update-card { margin-top: var(--space-xs); }

.update-card-media {
  position: relative;
  margin: 0;
  /* A fixed band rather than the image's own shape, so a list of articles
     reads as a list rather than a ragged column. `focus` per item keeps the
     part that matters inside the crop. */
  aspect-ratio: 16 / 9;
  background: var(--color-surface-2);
}
.update-card-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.update-card-credit {
  position: absolute;
  right: 6px;
  bottom: 6px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 0.66rem;
  letter-spacing: 0.02em;
}

.update-card-text {
  display: flex;
  flex-direction: column;
  padding: var(--space-md) var(--space-md) var(--space-sm);
}

/* Kicker: topic, then date. Small, spaced, and the one place colour is used,
   so the eye lands on it before the headline and knows what it is reading. */
.update-card-kicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  margin: 0 0 6px;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.update-card-kicker:empty { display: none; }
.update-card-topic {
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(var(--color-accent-rgb), 0.16);
  color: var(--color-accent-text);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
html.dark-mode .update-card-topic {
  background: rgba(var(--color-accent-rgb), 0.22);
  color: var(--color-accent);
}
.update-card-date { font-variant-numeric: tabular-nums; }
.update-card-byline { font-style: italic; }

.update-card-title {
  margin: 0;
  font-size: var(--font-size-lg);
  font-weight: 750;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--color-text);
  /* Balanced wrapping stops a headline leaving one word on the last line.
     Ignored where unsupported, which costs nothing. */
  text-wrap: balance;
}

/* The lede is the sentence that decides whether the piece gets read, so it is
   set larger than the body and separated from it by a rule. */
.update-card-lede {
  margin: var(--space-xs) 0 0;
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-md);
  font-weight: 600;
  line-height: 1.5;
  color: var(--color-text);
}

.update-card-body {
  margin-top: var(--space-sm);
  font-size: var(--font-size-md);
  line-height: 1.62;
  color: var(--color-text);
  /* Prose reads badly across a wide panel; this caps the measure without
     capping the card. */
  max-width: 68ch;
}
/* An article is paragraphs, not one block — the renderer splits on blank
   lines and this is what gives them room to read as such. */
.update-card-body p { margin: 0; }
.update-card-body p + p { margin-top: var(--space-sm); }

.update-card-rest p:first-child { margin-top: var(--space-sm); }

.update-card-more {
  align-self: flex-start;
  margin-top: var(--space-sm);
  min-height: var(--target-min);
  padding: 0;
  appearance: none;
  background: none;
  border: 0;
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: 700;
  /* --color-link, not --color-primary. The primary is a fill colour and only
     measures 3.7:1 as text on the card in dark mode; the same trap the
     proposal links fell into. */
  color: var(--color-link);
  cursor: pointer;
}
.update-card-more:hover { text-decoration: underline; }

.update-card-sources {
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
}
.update-card-sources-title {
  margin: 0 0 var(--space-2xs);
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* ===== SHEET HANDLE =====
   Desktop has no sheet, so no handle. On mobile it is the primary way to
   resize the panel: drag for a continuous gesture, or click / Enter / Space
   to cycle peek -> half -> full, because the sheet has to be operable
   without a pointer. */
.sheet-handle { display: none; }

@media (max-width: 900px) {
  /* The panel's own background moves onto the panel modes, so the handle
     strip is the one part of the sheet the map shows through. */
  .departure-panel {
    background: transparent;
    border-top: none;
    box-shadow: none;
  }
  .panel-mode {
    background: var(--color-surface);
    box-shadow: var(--shadow-lg);
  }

  .sheet-handle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: var(--sheet-handle-height);
    min-height: var(--sheet-handle-height);
    flex-shrink: 0;
    padding: 0;
    /* A quarter opaque: enough to read the grip against, thin enough to see
       the map carry on underneath. The blur is what keeps the grip legible
       over a busy map; without it a 25% wash is not enough separation. */
    background: color-mix(in srgb, var(--color-surface) 25%, transparent);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: none;
    cursor: grab;
    /* The gesture is ours; without this the browser scrolls the page instead
       of giving us pointermove. */
    touch-action: none;
  }
  .sheet-handle-grip {
    width: 44px;
    height: 4px;
    border-radius: var(--radius-pill);
    background: var(--color-text-muted);
    box-shadow: 0 0 0 1px rgba(var(--color-on-fill-rgb), 0.35);
    transition: background var(--transition);
  }
  .sheet-handle:active { cursor: grabbing; }
  .sheet-handle:active .sheet-handle-grip,
  .sheet-handle:focus-visible .sheet-handle-grip {
    background: var(--color-text-muted);
  }
}

/* ===== PANEL TABS BAR ===== */
.panel-tabs-bar {
  display: flex;
  align-items: stretch;
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

.panel-tabs {
  display: flex;
  flex: 1;
  align-items: stretch;
}

.panel-tab {
  flex: 1;
  background: transparent;
  border: none;
  padding: 10px var(--space-md);
  min-height: var(--target-min);
  color: var(--color-text-muted);
  font-family: var(--font-family);
  font-size: var(--font-size-sm);
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  border-bottom: 2px solid transparent;
  transition: color var(--transition), border-color var(--transition);
  letter-spacing: 0.03em;
}@media (hover: hover) {


.panel-tab:hover {
  color: var(--color-text);
}
}

.panel-tab.active {
  color: var(--color-accent-text);
  border-bottom-color: var(--color-accent);
}

.panel-tab-icon {
  font-size: var(--font-size-sm);
}

.panel-tabs-bar .btn-close,
.panel-tabs-bar .btn-collapse-panel {
  background: transparent;
  border: none;
  border-left: 1px solid var(--color-border);
  border-radius: 0;
  width: var(--target-min);
  height: auto;
  min-height: var(--target-min);
  align-self: stretch;
  padding: 0;
  font-size: var(--font-size-md);
  margin: 0;
  color: var(--color-text-muted);
  transition: background var(--transition), color var(--transition);
  display: flex;
  align-items: center;
  justify-content: center;
}@media (hover: hover) {


.panel-tabs-bar .btn-close:hover,
.panel-tabs-bar .btn-collapse-panel:hover {
  background: var(--color-surface-2);
  color: var(--color-text);
}
}

/* Ticket view's strip is a label, not a control — there is only one view to
   be on. Styled as the active tab so the bar reads the same everywhere. */
.panel-tab-static {
  cursor: default;
}

/* Hide tab button only appears on mobile, where vertical space is at a
   premium and the user wants to swap the panel for the map. */
.panel-tabs-bar .btn-collapse-panel {
  display: none;
}

.panel-tabs-bar .btn-collapse-panel .icon {
  width: 16px;
  height: 16px;
  transition: transform var(--transition);
}

@media (max-width: 900px) {
  .panel-tabs-bar .btn-collapse-panel { display: flex; }
}

/* "Collapsed" now means the sheet is at its peek detent, not that the content
   is display:none. That distinction is the whole Phase 1b bug: hiding the
   content left Ticket view with nothing on screen to bring it back, and only
   a reload recovered it. A sheet at peek is still a sheet — the handle and
   the tab strip are always there, so the state cannot strand anyone.

   The rule is kept (rather than deleted) because the collapsed panel still
   needs to shrink on desktop, where there is no sheet and no handle. */
@media (min-width: 901px) {
  body.panel-collapsed .panel-mode > .panel-tab-content,
  body.panel-collapsed .panel-mode[data-mode="live"] > :not(.panel-tabs-bar) {
    display: none;
  }
}

/* Flip the chevron when collapsed so it visually invites re-expansion. */
body.panel-collapsed .btn-collapse-panel .icon {
  transform: rotate(180deg);
}

@media (max-width: 900px) {
  /* Collapsed is the peek detent; the height rules above already say so. */
  body.panel-collapsed { --sheet-h: var(--sheet-peek); }
}

/* Each tab's content area fills the panel and owns its own scrolling.
 *
 * This used to be `overflow: hidden`, on the assumption that every view would
 * nominate an inner `flex: 1; overflow-y: auto` child to do the scrolling —
 * `objectives-list`, `proposals-list`, `ticket-zones-list` all do. Three did
 * not, and a tab that forgets does not degrade to an awkward scroll: it
 * silently clips, and the content below the fold becomes unreachable. Network
 * Updates lost everything past the first article; the Ideas tab lost the
 * bottom of its form.
 *
 * Scrolling here instead makes clipping impossible by default. Views with an
 * inner scroller are unaffected — the inner one absorbs the overflow, so this
 * container never overflows and never shows a second scrollbar.
 */
.panel-tab-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* Reaching the end of the panel must not start scrolling the page behind
     it, which on mobile drags the whole sheet. */
  overscroll-behavior: contain;
}

.panel-tab-content.hidden {
  display: none !important;
}

/* ===== PANEL HEADER ===== */
.panel-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: var(--space-md) var(--space-md) 10px;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface-2);
  flex-shrink: 0;
}

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

.panel-stop-name {
  font-size: var(--font-size-lg);
  font-weight: 700;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.panel-stop-id {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  margin-top: 3px;
  letter-spacing: 0.05em;
}

/* ===== PANEL STATES ===== */
.panel-loading,
.panel-error,
.panel-prompt {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  padding: var(--space-xl);
  text-align: center;
  color: var(--color-text-muted);
}

.panel-prompt-icon {
  font-size: 2rem;
  opacity: 0.45;
}

.panel-error {
  color: var(--color-late);
}

.btn-retry {
  background: var(--color-primary);
  color: var(--color-on-fill);
  border: none;
  /* Only ever rendered in the error state, which is why it escaped the
     earlier target-size pass — nothing had exercised that state. */
  min-height: var(--target-min);
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-md);
  font-family: var(--font-family);
  font-size: var(--font-size-sm);
  font-weight: 600;
  transition: background var(--transition);
}@media (hover: hover) {


.btn-retry:hover {
  background: var(--color-primary-dark);
}
}

/* ===== DEPARTURES CONTAINER ===== */
.departures-container {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

.departures-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px var(--space-md);
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  flex-shrink: 0;
}

.departures-notice {
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  background: var(--color-surface-2);
  border-bottom: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
}

.btn-text {
  background: none;
  border: none;
  color: var(--color-primary);
  font-family: var(--font-family);
  font-size: var(--font-size-xs);
  font-weight: 600;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  transition: background var(--transition);
}@media (hover: hover) {


.btn-text:hover {
  background: var(--color-surface-2);
}
}

html.dark-mode .btn-text {
  color: var(--color-accent);
}

/* ===== DEPARTURES TABLE ===== */
/* Styled to evoke a real transit departure board */

.departures-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
  table-layout: fixed;
}

.departures-table th:nth-child(1),
.departures-table td:nth-child(1) { width: 54px; }   /* Service badge */
.departures-table th:nth-child(3),
.departures-table td:nth-child(3) { width: 82px; }   /* Due time */
.departures-table th:nth-child(4),
.departures-table td:nth-child(4) { width: 86px; }   /* Status chip */

.departures-table thead {
  background: var(--color-bg);
  position: sticky;
  top: 0;
  z-index: 1;
}

.departures-table th {
  text-align: left;
  padding: 6px var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-2xs);
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  border-bottom: 1px solid var(--color-border);
}

.departures-table td {
  padding: 8px var(--space-sm);
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
  overflow: hidden;
}

.departures-table tr:last-child td {
  border-bottom: none;
}

.departures-table tr.departure-row {
  cursor: pointer;
  transition: background var(--transition);
}@media (hover: hover) {


.departures-table tr.departure-row:hover td {
  background: var(--color-surface-2);
}
}

/* ===== SERVICE BADGE ===== */
.service-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--color-primary);
  color: var(--color-on-fill);
  font-family: var(--font-family);
  font-weight: 700;
  font-size: var(--font-size-xs);
  min-width: 36px;
  height: 22px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
  letter-spacing: 0.02em;
}

/* ===== DESTINATION TEXT ===== */
.destination-text {
  color: var(--color-text);
  font-weight: 500;
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ===== DUE TIME ===== */
.due-time {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: var(--font-size-sm);
  white-space: nowrap;
  color: var(--color-text);
  letter-spacing: -0.01em;
}

.due-time.due-imminent {
  color: var(--color-accent-text);
}

/* ===== STATUS CHIPS ===== */
.status-chip {
  display: inline-flex;
  align-items: center;
  padding: 3px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
  text-transform: uppercase;
  border: 1px solid transparent;
}

/* One set of rules for both themes — the tints themselves are theme tokens,
   because light mode uses opaque hand-picked colours and dark mode lays the
   status colour over the surface at low alpha. */
.status-on-time {
  background: var(--tint-on-time-bg);
  color: var(--color-on-time);
  border-color: var(--tint-on-time-border);
}

.status-early {
  background: var(--tint-early-bg);
  color: var(--color-early);
  border-color: var(--tint-early-border);
}

.status-late {
  background: var(--tint-late-bg);
  color: var(--color-late);
  border-color: var(--tint-late-border);
}

.status-scheduled {
  background: var(--tint-scheduled-bg);
  color: var(--color-unknown);
  border-color: var(--tint-scheduled-border);
}

.departures-footer {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-align: center;
  padding: var(--space-sm) var(--space-md);
  border-top: 1px solid var(--color-border);
}

/* ===== NO DEPARTURES STATE ===== */
.no-departures {
  padding: var(--space-xl);
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

/* ===== TOAST ===== */
.toast {
  position: fixed;
  bottom: var(--space-lg);
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-surface-2);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  font-size: var(--font-size-sm);
  z-index: 2000;
  max-width: 90vw;
  text-align: center;
  transition: opacity var(--transition);
}

.toast.hidden {
  opacity: 0;
  pointer-events: none;
}

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

/* ===== FOOTER ===== */
.site-footer {
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  padding: 5px var(--space-lg);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-align: center;
  flex-shrink: 0;
  z-index: 100;
  padding-bottom: calc(5px + env(safe-area-inset-bottom, 0px));
}

/* ===== STOP MARKERS ===== */
.stop-marker-icon {
  background: var(--color-primary);
  border: 2px solid var(--color-primary-dark);
  border-radius: 50%;
  width: 12px !important;
  height: 12px !important;
  transition: transform 0.15s;
}@media (hover: hover) {


.stop-marker-icon:hover {
  transform: scale(1.4);
}
}

/* ===== BUS VEHICLE MARKERS ===== */
.bus-marker-divicon {
  background: transparent !important;
  border: none !important;
}

.bus-marker-wrapper {
  position: relative;
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
}

.bus-icon-img {
  width: 56px;
  height: 56px;
  object-fit: contain;
  transform-origin: center center;
  transition: transform 0.4s ease-out;
  display: block;
  /* The drop shadow is baked into the PNG by scripts/build_icons.py, not drawn
     here. `filter: drop-shadow()` puts every marker on its own compositing
     layer, and there are two hundred buses on screen at the daytime peak — two
     hundred layers recomposited every frame for a shadow that never changes. */
}

.bus-icon-label {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: var(--color-on-fill);
  font-family: var(--font-family);
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
  z-index: 2;
  text-shadow:
    -1px -1px 0 #000,
     1px -1px 0 #000,
    -1px  1px 0 #000,
     1px  1px 0 #000,
     0    0   3px rgba(0,0,0,0.9);
}

.bus-icon-fallback {
  background: var(--color-accent);
  border: 2px solid rgba(0,0,0,0.2);
  border-radius: var(--radius-sm);
  color: var(--color-on-accent);
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  white-space: nowrap;
  padding: 0 4px;
  min-width: 26px;
  height: 18px;
  box-shadow: var(--shadow-sm);
}

/* ===== LEAFLET CUSTOMISATION ===== */
.leaflet-popup-content-wrapper {
  background: var(--color-surface) !important;
  border-radius: var(--radius-lg) !important;
  box-shadow: var(--shadow-lg) !important;
  font-family: var(--font-family) !important;
  border: 1px solid var(--color-border) !important;
  color: var(--color-text) !important;
}

.leaflet-popup-tip {
  background: var(--color-surface) !important;
}

.leaflet-popup-content {
  margin: 12px 14px !important;
  font-size: var(--font-size-sm) !important;
  color: var(--color-text) !important;
}

.popup-stop-name {
  font-weight: 700;
  color: var(--color-text);
  margin-bottom: 4px;
}

.popup-stop-id {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  margin-bottom: 8px;
  letter-spacing: 0.05em;
}

.popup-btn {
  background: var(--color-primary);
  color: var(--color-on-fill);
  border: none;
  padding: 6px 14px;
  border-radius: var(--radius-md);
  font-family: var(--font-family);
  font-size: var(--font-size-xs);
  font-weight: 600;
  width: 100%;
  transition: background var(--transition);
}@media (hover: hover) {


.popup-btn:hover {
  background: var(--color-primary-dark);
}
}

.leaflet-bar a {
  background: var(--color-surface) !important;
  color: var(--color-text) !important;
  border-color: var(--color-border) !important;
}@media (hover: hover) {


.leaflet-bar a:hover {
  background: var(--color-surface-2) !important;
}
}

/* Text buttons are still buttons: "Report an issue" and "Refresh" sat at
   19px tall. Padding rather than a fixed height, so they stay inline. */
.btn-text {
  min-height: var(--target-min);
  display: inline-flex;
  align-items: center;
}

/* Count bubble standing in for a group of stops when zoomed out. Same gold
   family as the dots it replaces, so it reads as "stops, collapsed" rather
   than as a new kind of thing. */
.stop-cluster-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-accent);
  color: var(--color-on-accent);
  border: 2px solid var(--color-on-fill);
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 700;
  box-shadow: 0 2px 6px rgba(10, 18, 32, 0.35);
  cursor: pointer;
  transition: transform var(--transition-fast);
}
html.dark-mode .stop-cluster-icon {
  border-color: rgba(9, 14, 26, 0.85);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
}

/* Leaflet's attribution ships as black on white and sat there unchanged over
   the dark theme. It has to stay legible and present — it is the OSM licence
   condition — so it is themed rather than hidden. */
.leaflet-control-attribution {
  background: var(--color-surface) !important;
  color: var(--color-text-muted) !important;
  font-size: var(--font-size-2xs);
  padding: 1px 6px;
  border-radius: var(--radius-sm) 0 0 0;
}
.leaflet-control-attribution a { color: var(--color-link) !important; }

/* Leaflet's own controls ship at 30x30. */
.leaflet-bar a,
.leaflet-bar a:hover {
  width: var(--target-min) !important;
  height: var(--target-min) !important;
  line-height: var(--target-min) !important;
}

/* Stop markers are 12px dots and must stay that size — they mark a position,
   and a 44px dot would cover the street it sits on. Instead the dot keeps a
   transparent hit area around it, which is what a finger actually needs.
   (WCAG 2.5.8 exempts targets whose presentation is essential; the enlarged
   hit area means the exemption is not being leaned on.) */
.stop-marker-icon::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--target-min);
  height: var(--target-min);
  transform: translate(-50%, -50%);
}

/* ===== TABBED PANEL ===== */
/* (tab bar styles already defined above) */

/* ===== BUS INFO TAB ===== */
.bus-info-container {
  padding: var(--space-md);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  flex: 1;
}

.bus-info-hero {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

.bus-info-icon {
  width: 52px;
  height: 52px;
  object-fit: contain;
  flex-shrink: 0;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.3));
}

.bus-info-icon-fallback {
  background: var(--color-accent);
  border-radius: 50%;
}

.bus-info-hero-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
}

.service-badge-large {
  min-width: 48px;
  height: 30px;
  font-size: var(--font-size-base);
  padding: 0 10px;
}

.bus-info-operator {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  font-weight: 500;
}

.bus-info-grid {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--color-border);
  margin: 0;
}

.bus-info-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  font-size: var(--font-size-sm);
}

.bus-info-row dt {
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-weight: 600;
}

.bus-info-row dd {
  color: var(--color-text);
  font-weight: 600;
  text-align: right;
  margin: 0;
}

.bus-info-mono {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}

.follow-bus-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  font-weight: 600;
  transition: background var(--transition), border-color var(--transition);
}@media (hover: hover) {


.follow-bus-toggle:hover {
  background: var(--color-border);
}
}

.follow-bus-toggle input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--color-accent);
  cursor: pointer;
}

.follow-bus-hint {
  display: block;
  font-size: var(--font-size-2xs);
  font-weight: 400;
  color: var(--color-text-muted);
  margin-top: 1px;
}

.bus-info-footer {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  text-align: center;
  margin-top: auto;
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
}

/* ===== UPCOMING STOPS ===== */
.upcoming-stops {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.upcoming-stops-title {
  font-family: var(--font-mono);
  font-size: var(--font-size-2xs);
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin: 0;
  padding-bottom: var(--space-2xs);
  border-bottom: 1px solid var(--color-border);
}

.upcoming-stops-loading {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  padding: var(--space-sm) 0;
}

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

.upcoming-stop {
  display: grid;
  grid-template-columns: 16px 1fr auto;
  align-items: center;
  gap: var(--space-sm);
  padding: 7px 0;
  border-bottom: 1px solid var(--color-border);
}

.upcoming-stop:last-child {
  border-bottom: none;
}

.upcoming-stop-marker {
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  text-align: center;
}

.upcoming-stop:first-child .upcoming-stop-marker {
  color: var(--color-accent);
}

.upcoming-stop-name {
  color: var(--color-text);
  font-size: var(--font-size-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.upcoming-stop-time {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
  letter-spacing: -0.01em;
}

.upcoming-stops-note {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  margin-top: var(--space-2xs);
  font-style: italic;
}

.upcoming-stop-gap {
  list-style: none;
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--font-size-2xs);
  padding: 3px 0;
  letter-spacing: 0.12em;
}

.upcoming-stop--terminus .upcoming-stop-marker {
  color: var(--color-accent);
  font-size: var(--font-size-md);
}

/* ===== TICKET INFO ===== */
.ticket-info {
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.ticket-info-title {
  font-family: var(--font-mono);
  font-size: var(--font-size-2xs);
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin: 0 0 4px;
}

.ticket-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.ticket-row {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: var(--space-2xs);
  font-size: var(--font-size-xs);
  align-items: baseline;
}

.ticket-label {
  color: var(--color-text-muted);
  font-weight: 500;
}

.ticket-value {
  color: var(--color-text);
}

.ticket-value a {
  color: var(--color-accent-text);
  text-decoration: none;
}@media (hover: hover) {


.ticket-value a:hover {
  text-decoration: underline;
}
}

.ticket-more-link {
  font-size: var(--font-size-xs);
  color: var(--color-accent-text);
  text-decoration: none;
  margin-top: var(--space-2xs);
  align-self: flex-start;
}@media (hover: hover) {


.ticket-more-link:hover {
  text-decoration: underline;
}
}

.bus-info-lost {
  background: #fff8e8;
  color: #8a5200;
  border: 1px solid #f0d898;
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-align: center;
}

html.dark-mode .bus-info-lost {
  background: rgba(var(--color-accent-rgb), 0.1);
  color: var(--color-accent);
  border-color: rgba(var(--color-accent-rgb), 0.25);
}

/* ===== BUS POPUP ===== */
.bus-popup {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-width: 160px;
}

.bus-popup-header {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--color-border);
}

.bus-popup-icon {
  width: 30px;
  height: 30px;
  object-fit: contain;
  flex-shrink: 0;
}

.bus-popup-icon-fallback {
  border-radius: 50%;
}

.bus-popup-destination {
  font-size: var(--font-size-xs);
  color: var(--color-text);
  margin: 0;
}

.bus-popup-status { margin: 0; }

.bus-popup-hint {
  font-size: 10px;
  color: var(--color-text-muted);
  text-align: center;
  font-style: italic;
  margin-top: 2px;
}

/* ===== STANDALONE CLOSE BUTTON ===== */
.btn-close {
  background: none;
  border: none;
  font-size: var(--font-size-base);
  color: var(--color-text-muted);
  padding: var(--space-2xs);
  border-radius: var(--radius-sm);
  transition: background var(--transition), color var(--transition);
  flex-shrink: 0;
  margin-left: var(--space-sm);
}@media (hover: hover) {


.btn-close:hover {
  background: var(--color-surface-2);
  color: var(--color-text);
}
}

/* ================================================================
 * PHASE 0 UPGRADES — visual polish (Civic Coastal direction)
 * ================================================================ */

/* ---- Header: enlarge + wave-shaped bottom edge ----
   NOTE (maintainability): this "PHASE 0" block intentionally re-styles several
   components already defined earlier in the file (.site-header, .header-title,
   .header-icon, .service-badge, .due-time, .map-loading, .departures-table…).
   The cascade resolves correctly, but if you edit one of those components,
   search the whole file — the winning rule may live down here, not at the
   first definition. --header-height is now declared once (in :root above);
   only the mobile override remains. */

.site-header {
  position: relative;
  overflow: visible;   /* let the wave hang below the header rectangle */
  border-bottom: none; /* hard edge replaced by the wave */
  box-shadow: none;    /* drop-shadow now follows the wave contour */
  /* Above the side panel (z-index 1001) so the section-nav dropdown, which
     hangs below the header, can overlay the panel instead of being clipped
     behind it. */
  z-index: 1100;
}
html.dark-mode .site-header {
  border-bottom: none;
  box-shadow: none;
}

.header-wave {
  position: absolute;
  top: 100%;
  left: 0;
  /* End the wave where the side panel begins so the panel butts flush
     against a clean, flat header edge instead of a misaligned wavy notch.
     Needs an explicit width (not left+right) so preserveAspectRatio="none"
     stretches the SVG across the full span; with only left/right the SVG
     collapses to its intrinsic ~18:1 ratio and cuts off early. Reset to
     full width on mobile (panel stacks below the map). */
  width: calc(100% - var(--panel-width));
  height: 22px;
  display: block;
  pointer-events: none;
  z-index: 2;
  filter: drop-shadow(0 4px 8px rgba(10, 18, 32, 0.10));
}
html.dark-mode .header-wave {
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.40));
}

/* Lift the side panel above the header's wave so the Stop/Bus tabs
   aren't obscured by the wave hanging down from the header. */
.departure-panel { z-index: 1001; }

.header-wave-back  { fill: var(--color-primary-dark); }
.header-wave-front { fill: var(--color-primary); }
html.dark-mode .header-wave-front { fill: var(--color-surface); }

/* Above the wave (z-index 2) so the section-nav dropdown menu, which lives
   inside header-inner, isn't painted behind the wave that hangs below. */
.header-inner { position: relative; z-index: 3; gap: var(--space-md); }

/* min-width:0 lets the brand text shrink + ellipsis inside the flex row
   at every width, not just under the mobile breakpoint. */
.header-brand { gap: 14px; min-width: 0; }
.header-brand > div { min-width: 0; }

/* The mark is a self-contained disc with its own white ring, so it needs no
   tile behind it. The old translucent plate existed to give a line-art glyph
   an edge; giving one to the disc as well reads as a sticker stuck onto the
   header — which is exactly how the supplied artwork's baked-in pill read. */
.header-icon {
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
/* An <img>, not an inline svg: the mark is a raster illustration and the
   header serves it at 1x/2x/3x. object-fit guards the aspect if a future
   export is not square — the build script squares it, but the CSS should not
   depend on that being remembered. */
.brand-mark {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
}

.header-title {
  /* Outfit, not Fraunces: the wordmark's own voice is a geometric sans, and
     the page already loads this family for UI. A sans lockup above serif
     headings is a deliberate pairing, where setting the brand in the heading
     face gave the header two competing voices. */
  font-family: var(--font-family);
  /* Fluid scale, capped at the same 1.9rem as before so wide desktop is
     unchanged. The slope is shallower than it was because the old one put
     the title at 26px on a 768px tablet, where it needed 431px of a 390px
     brand column and rendered 80% of itself. */
  font-size: clamp(1.05rem, 0.35rem + 1.85vw, 1.9rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.015em;
  color: var(--color-on-fill);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding-left: 2px;
}

/* Two-tone wordmark. #56edfd is the artwork's own cyan, kept because it
   measures 6.29:1 on the light header (#1a4b82) and 12.97:1 on the dark one
   (#0d1525) — AA on both, so the design's colour survives unmodified. The
   accent stays fixed across themes: a logo that changes hue with the colour
   scheme stops reading as a logo. */
.brand-word--accent { color: #56edfd; }
html.dark-mode .header-title { color: var(--color-on-fill); }

/* Tablet: tighten control spacing and drop the non-essential timestamp
   before it collides with the toggle. */
@media (max-width: 1024px) {
  .header-controls { gap: var(--space-sm); }
}
@media (max-width: 880px) {
  .last-updated { display: none; }
}

/* Below 700px the header cannot hold everything: measured at 390px, the
   controls took 302 of it and left 64px for a title needing 137. Rather than
   shrink the title into illegibility, the bus and rail *layer* toggles move
   onto the map, which is what they act on. They keep their ids and their
   bindings; only their position and their skin change, because on the map
   they can no longer borrow the header's white-on-navy. */
@media (max-width: 700px) {
  :root { --header-height: 52px; }

  .status-pill {
    position: fixed;
    top: calc(var(--header-height) + var(--space-sm));
    right: var(--space-sm);
    z-index: 850;
    padding: 0 3px;
    gap: 2px;
    background: var(--color-surface);
    /* Inset ring for the edge, drop shadow for the float — a real border
       would push this past the 44px the header row is unified on. */
    box-shadow: inset 0 0 0 1px var(--color-border), var(--shadow-md);
  }
  .status-pill .btn-icon.status-pill-toggle {
    color: var(--color-text-muted);
  }
  .status-pill .btn-icon.status-pill-toggle[aria-pressed="true"] {
    color: var(--color-accent-text);
    background: var(--color-surface-2);
  }
  /* The mark now earns its place at this width instead of being dropped. It
     is the most compact form the brand has — 32px against the wordmark's
     ~150 — so hiding it and keeping only text gets the trade backwards. */
  .header-icon     { width: 32px; height: 32px; }
  /* Panel stacks below the map on mobile, so the wave spans full width. */
  .header-wave { width: 100%; height: 16px; }
  .header-inner { gap: var(--space-sm); padding: 0 var(--space-md); }
  .header-brand { gap: 10px; flex: 1; min-width: 0; }
  .header-brand > div { min-width: 0; }
  .header-controls { flex-shrink: 0; gap: var(--space-sm); }
  .section-nav-trigger { padding: 5px 9px; font-size: var(--font-size-xs); }
  /* Two lines, because one is not available: the brand column measures 140px
     here and the wordmark needs 143px at 14px type — below the point where it
     is worth reading. So it wraps instead of truncating, and at 16px it breaks
     itself after "Brighton" (125px) without a <br> that desktop would inherit.
     Truncation was putting "Worthing Brighto…" in the header. */
  .header-title {
    font-size: clamp(1rem, 3.4vw, 1.35rem);
    white-space: normal;
    overflow: visible;
    line-height: 1.08;
  }
}
@media (max-width: 480px) {
  /* The mark shrank at 700px rather than being hidden, and stays. */
  .header-inner { padding: 0 var(--space-sm); }
}

/* ---- Header controls: icon buttons hold svgs ---- */
.btn-icon .icon { width: 18px; height: 18px; }
.btn-icon { transition: background var(--transition), border-color var(--transition), transform var(--transition); }
.btn-icon:active { transform: scale(0.94); }

/* ---- Panel tab icons ---- */
.panel-tab-icon { width: 16px; height: 16px; }

/* ---- Panel prompt + inline svg ---- */
.panel-prompt-icon {
  width: 44px;
  height: 44px;
  opacity: 0.42;
  color: var(--color-accent);
  font-size: 0;
}
.panel-inline-icon {
  width: 14px;
  height: 14px;
  vertical-align: -0.18em;
  margin-right: 4px;
}

/* ---- Close button inside panel tab bar ---- */
.panel-tabs-bar .btn-close .icon { width: 14px; height: 14px; }

/* ---- Popup & text buttons: inline-flex layout for icon + label ---- */
.popup-btn,
.btn-text {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
}
.popup-btn .icon { width: 13px; height: 13px; }
.btn-text .icon { width: 13px; height: 13px; }

/* ---- Lost-bus banner: icon + text ---- */
.bus-info-lost {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
}
.bus-info-lost .icon { width: 15px; height: 15px; flex-shrink: 0; }

/* ---- Focus ring: visible, brand-coloured ---- */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm);
}
.leaflet-bar a:focus-visible { outline-offset: 0; }

/* ---- Loading dialog: illustrated pier + bus, centered card ---- */
.map-loading {
  gap: 14px;
  font-size: var(--font-size-sm);
  color: var(--color-text);
}
.map-loading-icon {
  width: 56px;
  height: 56px;
  color: var(--color-accent);
  opacity: 0.9;
}
.map-loading-bus {
  animation: loading-drive 2.6s ease-in-out infinite;
  transform-box: fill-box;
  transform-origin: center;
}
@keyframes loading-drive {
  0%   { transform: translateX(0); }
  45%  { transform: translateX(-38px); }
  55%  { transform: translateX(-38px); }
  100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .map-loading-bus { animation: none; }
}
.map-loading p {
  margin: 0;
}

/* ---- Stop markers: ochre dot with ring, branded ---- */
.stop-marker-icon {
  background: var(--color-accent) !important;
  border: 2px solid #ffffff !important;
  border-radius: 50% !important;
  width: 12px !important;
  height: 12px !important;
  box-shadow:
    0 0 0 2px rgba(var(--color-accent-rgb), 0.25),
    0 2px 4px rgba(10, 18, 32, 0.35);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1),
              box-shadow 0.18s;
}@media (hover: hover) {

.stop-marker-icon:hover {
  transform: scale(1.6);
  box-shadow:
    0 0 0 3px rgba(var(--color-accent-rgb), 0.35),
    0 3px 6px rgba(10, 18, 32, 0.45);
}
}
/* Zoomed out a lot (≤ z12): nudge the dots a touch smaller so ~1400 markers
   don't crowd the map. Size-based (not transform) to stay clear of Leaflet's
   positioning; more specific selector + !important wins over base sizing. */
.leaflet-container.stops-far .stop-marker-icon {
  width: 10px !important;
  height: 10px !important;
  border-width: 1.5px !important;
}
html.dark-mode .stop-marker-icon {
  border-color: rgba(9, 14, 26, 0.85) !important;
  box-shadow:
    0 0 0 2px rgba(var(--color-accent-rgb), 0.28),
    0 1px 3px rgba(0, 0, 0, 0.6);
}
/* In the proposal editor's Add-stop mode, hint that markers are click targets. */
body.editor-mode-add-stop .leaflet-marker-icon.stop-marker-icon {
  cursor: crosshair;
}
/* Brief confirmation pulse when a stop is added to a draft. */
.stop-marker-icon.just-added {
  animation: stop-add-pulse 320ms ease-out;
}
@keyframes stop-add-pulse {
  0%   { transform: scale(1);   box-shadow: 0 0 0 0   rgba(var(--color-accent-rgb), 0.55); }
  50%  { transform: scale(1.7); box-shadow: 0 0 0 10px rgba(var(--color-accent-rgb), 0);   }
  100% { transform: scale(1);   box-shadow: 0 0 0 0   rgba(var(--color-accent-rgb), 0);    }
}

/* ---- Service badge: livery-coloured variants ---- */
.service-badge {
  min-width: 40px;
  height: 24px;
  font-size: var(--font-size-xs);
  letter-spacing: 0.01em;
  box-shadow: inset 0 -1px 0 rgba(0,0,0,0.15);
  border: 1px solid rgba(0,0,0,0.1);
}
.service-badge--light-text { color: var(--color-on-fill); }
.service-badge--dark-text  { color: #0f1726; }

/* ---- Departure board: editorial upgrade ---- */
.departures-table { border-collapse: separate; border-spacing: 0; }
.departures-table th {
  border-bottom: 1px solid var(--color-border);
  background: transparent;
  padding: 8px var(--space-sm);
}
.departures-table td {
  border-bottom: 1px solid rgba(10, 18, 32, 0.05);
  padding: 10px var(--space-sm);
}
html.dark-mode .departures-table td {
  border-bottom-color: rgba(200, 216, 236, 0.06);
}
.departures-table tr:last-child td { border-bottom: none; }@media (hover: hover) {


.departures-table tr.departure-row:hover td {
  background: rgba(var(--color-accent-rgb), 0.05);
}
}@media (hover: hover) {

html.dark-mode .departures-table tr.departure-row:hover td {
  background: rgba(var(--color-accent-rgb), 0.06);
}
}

.departures-table td:nth-child(3),
.departures-table th:nth-child(3) { text-align: right; }

.due-time {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: var(--font-size-base); /* focal departure time — kept prominent */
  letter-spacing: -0.02em;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  justify-content: flex-end;
}
.due-time.due-imminent { color: var(--color-accent-text); }
.due-time.due-imminent::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 0 rgba(var(--color-accent-rgb), 0.55);
  animation: pulse-dot 1.6s ease-out infinite;
}
@keyframes pulse-dot {
  0%   { box-shadow: 0 0 0 0   rgba(var(--color-accent-rgb), 0.55); }
  70%  { box-shadow: 0 0 0 8px rgba(var(--color-accent-rgb), 0);    }
  100% { box-shadow: 0 0 0 0   rgba(var(--color-accent-rgb), 0);    }
}
@media (prefers-reduced-motion: reduce) {
  .due-time.due-imminent::before { animation: none; }
  .stop-marker-icon.just-added,
  .editor-point-row.just-added { animation: none; }
}

/* ---- Editor sidebar: new-point flash ---- */
.editor-point-row.just-added {
  animation: point-row-flash 600ms ease-out;
}
@keyframes point-row-flash {
  0%   { background: rgba(var(--color-accent-rgb), 0.32); }
  100% { background: transparent;             }
}

/* ---- Improvements: Frequency toggle (Show limited services) ---- */
.filter-toggle {
  min-height: var(--target-min);
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--font-size-sm);
  color: var(--color-text);
  cursor: pointer;
  user-select: none;
}
.filter-toggle input[type="checkbox"] {
  margin: 0;
  accent-color: var(--color-accent);
  width: 14px;
  height: 14px;
}

/* ---- Small spinner polish ---- */
.spinner { border-top-color: var(--color-accent); }

/* ---- Dark-mode map: the dark basemap itself ---- */
/* There is no second tile provider any more. CARTO's free dark basemap was
   withdrawn in August 2026 (it started stamping "API KEY REQUIRED" over every
   tile while still returning HTTP 200, so dark mode broke silently), so dark
   mode is now produced here, from the same OpenStreetMap tiles the light theme
   loads.

   invert + hue-rotate is the standard pairing: inverting alone would leave
   every colour as its complement, and rotating ~180deg puts them back. The
   angle is 174deg rather than 180deg to keep the slight navy lean the coastal
   palette was designed around, and saturation is pulled well down so the
   route liveries and bus markers drawn on top stay the brightest thing on
   screen.

   Scoped to the tile pane deliberately: markers, route polylines, ticket-zone
   polygons and reach pills live in sibling panes and keep their true colours. */
html.dark-mode .leaflet-tile-pane {
  filter: invert(1) hue-rotate(174deg) brightness(0.88) contrast(0.95) saturate(0.35);
}
html.dark-mode #map::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none !important;
  background: radial-gradient(
    120% 90% at 50% 40%,
    rgba(26, 75, 130, 0.0) 0%,
    rgba(9, 21, 35, 0.18) 80%,
    rgba(9, 21, 35, 0.28) 100%
  );
  mix-blend-mode: multiply;
  z-index: 400;
}
#map { position: relative; isolation: isolate; }

/* ============================================================
   IMPROVEMENTS VIEW (Live ↔ Improvements mode swap)
   ============================================================ */

/* Each .panel-mode wraps one mode's tabs + content, fills the panel */
.panel-mode {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Segmented Day/Night toggle, sits above the About/Proposals tab strip */
.service-mode-toggle {
  display: flex;
  gap: var(--space-2xs);
  padding: 8px var(--space-md);
  background: var(--color-surface-2);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

.service-mode-btn {
  flex: 1;
  min-height: var(--target-min);
  appearance: none;
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 5px 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}

.service-mode-btn .icon { width: 14px; height: 14px; }@media (hover: hover) {


.service-mode-btn:hover {
  color: var(--color-text);
  background: var(--color-surface);
}
}

.service-mode-btn.active {
  background: var(--color-accent, var(--color-primary));
  color: var(--color-on-accent);
  border-color: var(--color-accent, var(--color-primary));
}


/* Route endpoint tag — pill marker placed at the clipped end of an
   express or night route, showing where the line continues off the
   visible map (e.g. "1X → Brighton", "Bognor ← N700"). The icon's
   bounding box is wider than the pill itself; we flex-align the pill
   to one edge so the geographical point (set via iconAnchor in JS)
   sits just outside the pill on the correct side, and the polyline
   reads as flowing into the tag rather than crossing it. */
.route-endpoint-tag {
  display: flex;
  align-items: center;
  pointer-events: none;
  overflow: visible;
}

.route-endpoint-tag--right { justify-content: flex-start; }
.route-endpoint-tag--left  { justify-content: flex-end;   }

.route-endpoint-pill {
  display: inline-block;
  white-space: nowrap;
  font-family: var(--font-mono, monospace);
  font-weight: 700;
  font-size: var(--font-size-2xs);
  letter-spacing: 0.02em;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  border: 2px solid var(--color-on-fill);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

html.dark-mode .route-endpoint-pill {
  border-color: var(--color-bg);
}

/* Multi-select Category + Operator strips below the Day/Night toggle */
.filter-strip-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: 6px var(--space-md) 8px;
  background: var(--color-surface-2);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

.filter-strip-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: 22px;
}

.filter-strip-label {
  flex: 0 0 56px;
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.filter-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  flex: 1;
}

.filter-chip {
  appearance: none;
  min-height: var(--target-min);
  min-width: var(--target-min);
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 3px 10px;
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}@media (hover: hover) {


.filter-chip:hover {
  color: var(--color-text);
  background: var(--color-surface);
}
}

.filter-chip.active {
  background: var(--color-text);
  color: var(--color-surface);
  border-color: var(--color-text);
}

html.dark-mode .filter-chip.active {
  background: var(--color-accent, var(--color-primary));
  color: var(--color-bg);
  border-color: var(--color-accent, var(--color-primary));
}

.panel-mode.hidden { display: none; }

/* Mode visibility driven by body[data-view] so the right panel-mode shows for
   each of the three sections (live / improvements=Route view / tickets). */
.panel-mode { display: none; }
body[data-view="live"]         .panel-mode[data-mode="live"],
body[data-view="improvements"] .panel-mode[data-mode="improvements"],
body[data-view="tickets"]      .panel-mode[data-mode="tickets"],
body[data-view="network"]      .panel-mode[data-mode="network"],
body[data-view="updates"]      .panel-mode[data-mode="updates"] { display: flex; }

/* In the non-live network views stops stay visible but inert (no clicks, no
   popup). Vehicle markers are removed from the map entirely by JS. */
body:not([data-view="live"]) .leaflet-marker-icon.stop-marker-icon {
  pointer-events: none !important;
  opacity: 0.55;
}

/* Ticket view is about the zone fills, not individual stops — shrink the dots
   and fade them so ~1400 markers recede into the background behind the zones. */
body[data-view="tickets"] .leaflet-marker-icon.stop-marker-icon {
  width: 7px !important;
  height: 7px !important;
  border-width: 1px !important;
  opacity: 0.3;
  box-shadow: none !important;
}

/* ── About tab ── */
.improvements-intro {
  padding: var(--space-md) var(--space-md) var(--space-sm);
  border-bottom: 1px solid var(--color-border);
}

.improvements-title {
  font-family: var(--font-brand, var(--font-family));
  font-size: var(--font-size-lg);
  font-weight: 800;
  color: var(--color-text);
  margin: 0 0 8px;
  line-height: 1.15;
}

.improvements-intro p {
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-muted);
  margin: 0 0 8px;
}

.improvements-intro p:last-child { margin-bottom: 0; }

/* Heads-up callout: subtle left-rule + tint so the "Show limited services"
   caveat reads as a note, not just another paragraph. */
.improvements-intro p.improvements-note {
  padding: 8px 10px;
  border-left: 3px solid var(--color-accent);
  background: var(--color-surface-2);
  border-radius: var(--radius-sm);
  color: var(--color-text);
}

/* ── Route filter list ── */
.route-filters {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: var(--space-md);
  gap: 10px;
  overflow: hidden;
}

.route-filters-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.route-filters-title {
  font-family: var(--font-brand, var(--font-family));
  font-size: var(--font-size-md);
  font-weight: 700;
  margin: 0;
  color: var(--color-text);
}

.route-filters-actions {
  display: inline-flex;
  gap: var(--space-xs);
}

.route-filter-chips {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  overflow-y: auto;
  padding-right: 4px;
  align-content: flex-start;
}

.route-filters-empty,
.proposals-empty {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  font-style: italic;
}

.proposals-section + .proposals-section { margin-top: var(--space-lg); }
.proposals-section-title {
  font-size: var(--font-size-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  margin: 0 0 4px 0;
}
.proposals-section-blurb {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  margin: 0 0 var(--space-sm) 0;
  line-height: 1.4;
}
.proposals-section-cards { display: flex; flex-direction: column; gap: var(--space-sm); }

/* Each route chip — shows route number on its livery colour */
.route-chip {
  min-height: var(--target-min);
  min-width: var(--target-min);
  --chip-bg: var(--color-surface-2);
  --chip-fg: var(--color-text);
  appearance: none;
  border: 1px solid var(--color-border);
  background: var(--chip-bg);
  color: var(--chip-fg);
  font-family: var(--font-mono, monospace);
  font-weight: 700;
  font-size: var(--font-size-sm);
  letter-spacing: 0.02em;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: opacity var(--transition), transform var(--transition);
  line-height: 1.1;
}

.route-chip[aria-pressed="false"] {
  opacity: 0.32;
  background: var(--color-surface-2);
  color: var(--color-text-muted);
  border-color: var(--color-border);
}@media (hover: hover) {


.route-chip:hover { transform: translateY(-1px); }
}
.route-chip:active { transform: translateY(0); }

/* ── Proposals tab ── */
.proposals-intro {
  padding: var(--space-md) var(--space-md) var(--space-sm); /* match .improvements-intro */
  border-bottom: 1px solid var(--color-border);
}

.proposals-intro p {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text-muted);
}

.proposals-list {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.proposal-card {
  appearance: none;
  text-align: left;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-primary);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  font-family: inherit;
  color: inherit;
  transition: background var(--transition), transform var(--transition);
}@media (hover: hover) {


.proposal-card:hover { background: var(--color-surface); transform: translateY(-1px); }
}

.proposal-card.selected {
  background: var(--color-surface);
  box-shadow: 0 0 0 2px var(--color-accent, var(--color-primary)) inset;
}

.proposal-card-name {
  font-weight: 700;
  font-size: var(--font-size-md);
  color: var(--color-text);
}

.proposal-card-summary {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  line-height: 1.4;
}

.proposal-detail {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--color-border);
}

/* ---- Objective cards: wrapper, not one big button ----------------------
   An objective card used to be a single <button> containing its own expanded
   detail, which put links — and now a "write to your councillor" button —
   inside a button. Those are invalid nested interactives and were awkward to
   activate. The card chrome moves out to a wrapper, the button keeps only the
   summary row it actually toggles, and the detail becomes a sibling.
   Everything else using .proposal-card (proposals, ticket zones, ideas) is
   unwrapped and keeps the original rule untouched. */
.proposal-card-wrap {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-primary);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: background var(--transition);
}
.proposal-card-wrap.selected {
  background: var(--color-surface);
  box-shadow: 0 0 0 2px var(--color-accent, var(--color-primary)) inset;
}
/* The inner button is now only the hit area, so it gives up the card's skin
   and takes the full width of it — a summary row that stopped short of the
   edges would read as a button sitting on a card rather than as the card. */
.proposal-card-wrap > .proposal-card,
.proposal-card-wrap > .proposal-card.selected {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  width: 100%;
  transform: none;
}
@media (hover: hover) {
  .proposal-card-wrap:hover { background: var(--color-surface); }
  .proposal-card-wrap > .proposal-card:hover {
    background: none;
    transform: none;
  }
}
.proposal-card-wrap > .proposal-detail {
  margin-top: 0;
  padding: 8px 12px 12px;
}

/* ---- "Email your councillor" ------------------------------------------- */
.objective-contact-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
  min-height: var(--target-min);
  padding: 8px 12px;
  background: var(--color-primary);
  color: var(--color-on-fill);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: 700;
  cursor: pointer;
  transition: filter var(--transition);
}
.objective-contact-btn .icon { width: 16px; height: 16px; flex: 0 0 auto; }
@media (hover: hover) {
  .objective-contact-btn:hover { filter: brightness(1.08); }
}
.objective-contact-note {
  margin-top: 6px;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  line-height: 1.45;
}

/* ---- Councillor dialog -------------------------------------------------
   Shares .evidence-dialog's frame; only the contents inside it are new. */
.councillor-intro,
.councillor-draft-note,
.councillor-privacy,
.councillor-checked {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  line-height: 1.5;
  margin-top: 8px;
}
.councillor-privacy,
.councillor-checked { font-size: var(--font-size-xs); }

/* Which authority to write to, when an objective needs more than one. */
.councillor-bodies {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: 12px;
}
.councillor-body-btn {
  appearance: none;
  min-height: var(--target-min);
  padding: 8px 12px;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  cursor: pointer;
}
.councillor-body-btn.selected {
  background: var(--color-surface);
  border-color: var(--color-accent, var(--color-primary));
  box-shadow: 0 0 0 1px var(--color-accent, var(--color-primary)) inset;
}

.councillor-form { margin-top: 14px; }
.councillor-label {
  display: block;
  font-size: var(--font-size-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 6px;
}
.councillor-row {
  display: flex;
  gap: var(--space-xs);
  flex-wrap: wrap;
}
.councillor-input {
  flex: 1 1 10ch;
  min-width: 0;
  min-height: var(--target-min);
  padding: 8px 12px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--font-size-md);
  /* Postcodes are read back character by character; letter-spacing helps, and
     uppercasing means the field matches what is actually sent rather than
     correcting it silently after the fact. */
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.councillor-submit { flex: 0 0 auto; min-height: var(--target-min); }

.councillor-status {
  margin-top: 8px;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  min-height: 1.2em;   /* holds its line so the dialog does not jump */
}
/* --color-danger is defined on bare :root and re-defined for dark mode, so
   this needs no theme branch of its own. */
.councillor-status.is-error { color: var(--color-danger); }

.councillor-result { margin-top: 16px; }
.councillor-members {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.councillor-member {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px var(--space-xs);
  padding: 8px 10px;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.councillor-member-name { font-weight: 700; color: var(--color-text); }
.councillor-member-party {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* The address is shown as text as well as being wired into the mailto, so it
   can be copied by anyone whose browser will not hand off a mailto: link. */
.councillor-member-email {
  flex-basis: 100%;
  font-size: var(--font-size-sm);
  color: var(--color-link);
  word-break: break-all;
}

.councillor-draft {
  width: 100%;
  margin-top: 8px;
  padding: 10px 12px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  resize: vertical;
}
.councillor-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: 12px;
}
.councillor-actions > * { min-height: var(--target-min); }
/* The mail link is an <a> wearing a button's clothes; anchors do not inherit
   the button's centring. */
.councillor-actions a.editor-action-btn { text-decoration: none; }
.councillor-warn,
.councillor-empty {
  margin-top: 10px;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  line-height: 1.5;
}

.proposal-detail-heading {
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  margin: 8px 0 4px;
}

.proposal-detail-heading:first-child {
  margin-top: 0;
}

.proposal-detail-body {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  line-height: 1.5;
  margin: 0;
  white-space: pre-line;
}

.proposal-links {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.proposal-link {
  font-size: var(--font-size-sm);
  /* --color-link, not --color-primary: the primary reads as a fill colour and
     only measures 3.4:1 as text on the dark surface. Never showed up before
     because no view rendered these links against it. */
  color: var(--color-link);
  text-decoration: none;
  word-break: break-all;
}@media (hover: hover) {


.proposal-link:hover {
  text-decoration: underline;
}
}

/* ── Network plan view: objectives + community ideas ── */
/* Objectives list reuses the proposal-card visuals; container matches
   .proposals-list (scroll + padding + gap). */
.objectives-list {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Title + status badge share the top row of an objective card. */
.objective-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.status-badge {
  flex: none;
  font-size: var(--font-size-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  border: 1px solid currentColor;
  white-space: nowrap;
}
.status-badge.status-not-considered { color: var(--color-text-muted); }
.status-badge.status-discussed      { color: var(--color-early, var(--color-primary)); }
.status-badge.status-in-progress    { color: var(--color-accent, var(--color-primary)); }
.status-badge.status-delivered      { color: var(--color-on-time); }

/* Community idea cards are read-only — drop the button affordances. */
.idea-card { cursor: default; }@media (hover: hover) {

.idea-card:hover { transform: none; background: var(--color-surface-2); }
}
.idea-card-meta {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  font-weight: 600;
}
.community-ideas {
  padding: 0 var(--space-md) var(--space-md);
}

/* The Ideas tab stacks intro + form + ideas with no single flex:1 child, so
   (unlike the Objectives/Proposals lists) let the whole tab scroll rather than
   clip against .panel-tab-content's overflow:hidden on a short panel. */
#tab-content-ideas { overflow-y: auto; }
#tab-content-ideas > * { flex: 0 0 auto; }

/* Suggestion form — reuses .editor-field for labels/inputs; adds select
   styling (the editor only styled text + textarea) and layout. The select
   rules also cover the stop-report form, which uses the same field markup. */
.suggest-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}
.suggest-form .editor-field select,
.report-stop-form .editor-field select {
  appearance: none;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 6px 10px;
  font-family: inherit;
  font-size: var(--font-size-md);
  color: var(--color-text);
  width: 100%;
}
.suggest-form .editor-field select:focus,
.report-stop-form .editor-field select:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
  border-color: var(--color-primary);
}
.suggest-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
}
.suggest-actions .editor-status.is-error { color: var(--color-late); }

/* Honeypot: keep it in the DOM + focusable order for bots, invisible to humans
   and assistive tech (the wrapper is aria-hidden). */
.suggest-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Stop fault reporting, folded out under the departure board. Reuses the
   suggestion form's field styling; the tinted panel separates it from the
   departures table above it. */
.report-stop-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  margin: var(--space-2xs) 0;
  padding: var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
}
.report-stop-form.hidden { display: none; }

/* Turnstile mount point. Collapses to nothing until the widget renders, so an
   unconfigured or blocked script leaves no empty gap in the form. */
.suggest-turnstile:empty { display: none; }
.suggest-turnstile {
  margin-top: var(--space-xs);
  min-height: 0;
}

/* Submissions are posted publicly — say so next to the form, not in a
   tooltip. */
.suggest-privacy {
  margin: var(--space-xs) 0 0;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--color-text-muted);
}
.suggest-privacy a { color: var(--color-link); }

/* ── Ticket view: boundary penalty calculator ── */
.journey-calc {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}
.journey-calc .editor-field input[type="text"] {
  appearance: none;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 6px 10px;
  font-family: inherit;
  font-size: var(--font-size-md);
  color: var(--color-text);
  width: 100%;
}
.journey-calc .editor-status.is-error { color: var(--color-late); }

.journey-result:empty { display: none; }
.journey-result {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}
.journey-endpoints { margin: 0; font-size: var(--font-size-md); }
.journey-note {
  margin: 0;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.journey-alert {
  padding: var(--space-sm);
  border-radius: var(--radius-md);
  border-left: 4px solid var(--color-border);
  background: var(--color-surface-2);
}
.journey-alert p { margin: 0 0 var(--space-xs); }
.journey-alert p:last-child { margin-bottom: 0; }

/* One ticket covers it — the good outcome. */
.journey-alert--ok { border-left-color: var(--color-on-time); }
/* Multiple tickets needed — the campaigning case. */
.journey-alert--penalty { border-left-color: var(--color-late); }
/* We can't tell, and say so rather than guessing. */
.journey-alert--unknown { border-left-color: var(--color-text-muted); }

.journey-headline {
  font-weight: 600;
  line-height: 1.45;
}
.journey-basis {
  font-size: 0.78rem;
  color: var(--color-text-muted);
}
/* Itemised zone tickets, so the total is shown as arithmetic rather than
   asserted — a reader can check it against the operator's own price list. */
.journey-costs {
  list-style: none;
  margin: 0 0 var(--space-2xs);
  padding: 0;
  font-size: var(--font-size-sm);
}
.journey-costs li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: 2px 0;
}
.journey-cost-price { font-variant-numeric: tabular-nums; white-space: nowrap; }
.journey-cost-total {
  margin-top: 2px;
  padding-top: 4px;
  border-top: 1px solid var(--color-border);
  font-weight: 600;
}

.journey-reform-list {
  list-style: none;
  margin: 0 0 var(--space-xs);
  padding: 0;
}
.journey-reform-list li {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-xs) 0;
}
.journey-reform-list li + li { border-top: 1px dotted var(--color-border); }

/* The campaign's counterfactual: same journey, asked-for price. Visually
   distinct from the factual sections above it so it can't be mistaken for a
   fare you can buy today. */
.journey-reform {
  margin-top: var(--space-2xs);
  padding: var(--space-sm);
  border-radius: var(--radius-md);
  border: 1px dashed var(--color-primary);
  background: var(--color-primary-light);
}
html.dark-mode .journey-reform { background: transparent; }
.journey-reform p { margin: 0 0 var(--space-xs); }
.journey-reform-head { font-weight: 600; line-height: 1.45; }
.journey-reform-link {
  padding: 0;
  font-weight: 600;
  color: var(--color-primary);
}
html.dark-mode .journey-reform-link { color: var(--color-text); }

.journey-alternative {
  font-size: 0.82rem;
  padding-top: var(--space-xs);
  border-top: 1px dashed var(--color-border);
}
.journey-zones, .journey-sources { font-size: 0.78rem; }
.journey-zones-title {
  margin: 0 0 var(--space-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.journey-zones ul, .journey-sources ul {
  margin: 0;
  padding-left: 1.1em;
}
.journey-zone-op { color: var(--color-text-muted); }

/* ── Ticket view: fare-zone cards (reuse .proposal-card visuals) ── */
.ticket-zones-list {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ticket-zone-restrict {
  font-size: var(--font-size-xs);
  color: var(--color-late);
  font-weight: 600;
}
.ticket-zone-link {
  font-size: var(--font-size-sm);
  color: var(--color-accent-text);
  text-decoration: none;
  font-weight: 600;
  align-self: flex-start;
}@media (hover: hover) {

.ticket-zone-link:hover { text-decoration: underline; }
}
.ticket-zone-note {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  font-style: italic;
}

/* Per-operator accordion: a brand-coloured operator card that expands to
   reveal its ticket sub-cards. Colours come from --op-bg/--op-fg inline. */
.ticket-operator-group + .ticket-operator-group { margin-top: var(--space-sm); }
.ticket-operator-card {
  display: flex;
  min-height: var(--target-min);
  align-items: center;
  gap: var(--space-sm);
  padding: 10px 12px;
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--op-bg, var(--color-primary));
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  color: var(--color-text);
  cursor: pointer;
  user-select: none;
  transition: background var(--transition), transform var(--transition);
}@media (hover: hover) {

.ticket-operator-card:hover { background: var(--color-surface); transform: translateY(-1px); }
}
.ticket-operator-card[aria-expanded="true"] { background: var(--color-surface); }
/* Small brand-colour swatch keeps the operator's identity without a loud fill. */
.ticket-operator-card::before {
  content: "";
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--op-bg, var(--color-primary));
}
.ticket-operator-name {
  flex: 1;
  font-family: var(--font-brand, var(--font-family));
  font-size: var(--font-size-md);
  font-weight: 600;
  color: var(--color-text);
}
.ticket-operator-count {
  font-size: var(--font-size-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
}
.ticket-operator-chevron {
  width: 16px;
  height: 16px;
  color: var(--color-text-muted);
  transition: transform var(--transition);
}
.ticket-operator-card[aria-expanded="true"] .ticket-operator-chevron { transform: rotate(180deg); }
/* Collapsible zone sub-cards under an operator card. */
.ticket-operator-zones {
  display: none;
  flex-direction: column;
  gap: var(--space-xs);
  margin: var(--space-xs) 0 0 var(--space-sm);
  padding-left: var(--space-sm);
  border-left: 2px solid var(--color-border);
}
.ticket-operator-zones.expanded { display: flex; }
/* Sub-category heading within an operator group (e.g. "Gold tickets"). */
.ticket-subgroup-heading {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}
/* Operator-wide footnote (e.g. the N700 supplement). */
.ticket-operator-footnote {
  margin: var(--space-xs) 0 0;
  font-size: var(--font-size-xs);
  font-style: italic;
  color: var(--color-text-muted);
}
/* "Proposed" tag on a future zone card */
.ticket-zone-proposed-tag {
  display: inline-block;
  font-size: var(--font-size-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  background: var(--color-accent);
  color: var(--color-on-accent);
  vertical-align: middle;
}
.ticket-zone--proposed { border-style: dashed; }

/* networkSAVER-style reach pills: a destination tag centred on its point.
   Reuses .route-endpoint-pill for the pill look; this just centres it. */
.ticket-reach-tag { overflow: visible; }
.ticket-reach-pill {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, -50%);
  white-space: nowrap;
  pointer-events: none;
}

/* ── Map overlay control: "Show proposals" toggle ── */
.map-overlay-controls {
  position: absolute;
  /* Below Leaflet's zoom control, not on top of it. At 44px per button plus
     its border the zoom stack is ~90px tall from a 10px offset, and this
     button was sitting squarely over the "+". */
  top: calc(var(--target-min) * 2 + 26px);
  left: var(--space-sm);
  z-index: 500;
  display: none;
  flex-direction: column;
  gap: var(--space-sm);
  pointer-events: none; /* container ignores; children re-enable */
}

/* Route-view only — the overlay button is "Show proposals". Ticket view gets
   its own controls (rendered separately), so don't blanket non-live here. */
body[data-view="improvements"] .map-overlay-controls { display: flex; }

.map-overlay-btn {
  pointer-events: auto;
  appearance: none;
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-md, 0 1px 4px rgba(0,0,0,0.15));
  border-radius: var(--radius-md);
  padding: 6px 12px;
  min-height: var(--target-min);
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  transition: background var(--transition), border-color var(--transition);
}@media (hover: hover) {


.map-overlay-btn:hover { background: var(--color-surface-2); }
}

/* Active/selected state uses accent across the app (tabs, Day/Night, selected
   proposal). Dark text in dark mode keeps contrast on the lighter gold —
   mirrors .service-mode-btn.active. */
.map-overlay-btn[aria-pressed="true"] {
  background: var(--color-accent);
  color: var(--color-on-accent);
  border-color: var(--color-accent);
}@media (hover: hover) {

.map-overlay-btn[aria-pressed="true"]:hover {
  background: var(--color-accent);
}
}

/* ============================================================
   PROPOSAL EDITOR
   ============================================================ */

/* "+ New proposal" button above the proposals list */
.new-proposal-btn {
  margin-top: 10px;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  appearance: none;
  background: var(--color-primary);
  color: var(--color-on-fill);
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  padding: 6px 12px;
  cursor: pointer;
  transition: background var(--transition), transform var(--transition);
}@media (hover: hover) {

.new-proposal-btn:hover { transform: translateY(-1px); background: var(--color-primary-dark, var(--color-primary)); }
}
.new-proposal-btn .icon { width: 14px; height: 14px; }

/* Drafts section in the Proposals tab */
.drafts-section {
  padding: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}
.drafts-section.hidden { display: none; }
.drafts-title {
  margin: 0 0 8px;
  font-size: var(--font-size-sm);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}
.drafts-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.draft-card {
  appearance: none;
  text-align: left;
  background: var(--color-surface-2);
  border: 1px dashed var(--color-border);
  border-left: 4px dashed var(--color-primary);
  border-radius: var(--radius-md);
  padding: 8px 10px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  font-family: inherit;
  color: inherit;
  transition: background var(--transition);
}@media (hover: hover) {

.draft-card:hover { background: var(--color-surface); }
}
.draft-card-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.draft-card-name {
  font-weight: 600;
  font-size: var(--font-size-md);
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.draft-card-meta {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.draft-card-delete {
  appearance: none;
  background: transparent;
  border: none;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: var(--space-2xs);
  border-radius: var(--radius-sm, 4px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}@media (hover: hover) {

.draft-card-delete:hover { color: var(--color-danger); background: var(--color-danger-tint); }
}
.draft-card-delete .icon { width: 14px; height: 14px; }

/* Editor view — same panel, different content */
.proposal-editor {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
.proposal-editor.hidden { display: none; }

.editor-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: 10px var(--space-md);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface-2);
}
.editor-header-title {
  font-size: var(--font-size-md);
  font-weight: 700;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.editor-back-btn, .editor-delete-btn {
  appearance: none;
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 4px 8px;
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--font-size-sm);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  transition: background var(--transition);
}@media (hover: hover) {

.editor-back-btn:hover { background: var(--color-surface); }
}@media (hover: hover) {

.editor-delete-btn:hover { background: var(--color-danger-tint); color: var(--color-danger); border-color: var(--color-danger); }
}
.editor-back-btn .icon, .editor-delete-btn .icon { width: 13px; height: 13px; }

.editor-scroll {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.editor-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}
.editor-field > label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}
.editor-field input[type="text"],
.editor-field textarea {
  appearance: none;
  min-height: var(--target-min);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 6px 10px;
  font-family: inherit;
  font-size: var(--font-size-md);
  color: var(--color-text);
  width: 100%;
}
.editor-field textarea { min-height: 70px; resize: vertical; line-height: 1.4; }
.editor-field input:focus,
.editor-field textarea:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
  border-color: var(--color-primary);
}

.editor-color-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.editor-color-row input[type="color"] {
  width: 36px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm, 6px);
  background: none;
  cursor: pointer;
}
.editor-color-row-caption {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* Mode buttons: Add stop / Add waypoint / Move / delete */
.editor-modes-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  margin-bottom: 2px;
}
.editor-mode-buttons {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-xs);
}
.editor-mode-btn {
  appearance: none;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 8px 4px;
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: 600;
  color: var(--color-text);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  transition: background var(--transition), border-color var(--transition);
}
.editor-mode-btn .icon { width: 16px; height: 16px; }@media (hover: hover) {

.editor-mode-btn:hover { background: var(--color-surface); }
}
.editor-mode-btn.active {
  background: var(--color-accent);
  color: var(--color-on-accent);
  border-color: var(--color-accent);
}
html.dark-mode .editor-mode-btn.active { color: var(--color-bg); }
.editor-mode-hint {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  line-height: 1.4;
  margin-top: -4px;
}

/* Ordered point list */
.editor-points-label {
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}
.editor-points-count {
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--font-size-xs);
}
.editor-point-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-2xs);
  background: var(--color-surface);
}
.editor-point-list-empty {
  margin: 0;
  padding: 14px;
  text-align: center;
  font-size: var(--font-size-sm);
  font-style: italic;
  color: var(--color-text-muted);
}
.editor-point-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 4px 6px;
  border-radius: var(--radius-sm, 4px);
  font-size: var(--font-size-sm);
  color: var(--color-text);
}@media (hover: hover) {

.editor-point-row:hover { background: var(--color-surface-2); }
}
.editor-point-row .icon { width: 13px; height: 13px; flex-shrink: 0; }
.editor-point-row-index {
  width: 22px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}
.editor-point-row-label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.editor-point-row[data-type="waypoint"] .editor-point-row-label { color: var(--color-text-muted); font-style: italic; }
.editor-point-row-remove {
  appearance: none;
  background: transparent;
  border: none;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: 2px 4px;
  line-height: 1;
  font-size: var(--font-size-base);
  border-radius: var(--radius-sm, 4px);
}@media (hover: hover) {

.editor-point-row-remove:hover { color: var(--color-danger); background: var(--color-danger-tint); }
}

.editor-drag-handle {
  cursor: grab;
  color: var(--color-text-muted);
  padding: 0 2px;
  flex-shrink: 0;
  touch-action: none;
  display: flex;
  align-items: center;
}
.editor-drag-handle:active { cursor: grabbing; }
.editor-drag-handle .icon { width: 13px; height: 13px; }
.editor-point-row.drag-over-above { border-top: 2px solid var(--color-primary); }
.editor-point-row.drag-over-below { border-bottom: 2px solid var(--color-primary); }
.editor-drag-ghost {
  position: fixed;
  z-index: 1100;
  pointer-events: none;
  opacity: 0.88;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm, 4px);
  box-shadow: 0 4px 16px rgba(0,0,0,0.18);
  padding: 4px 6px;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}
.editor-draggable-waypoint { cursor: grab; }
.leaflet-interactive.editor-route-clickable { cursor: crosshair; }

/* Footer action row */
.editor-actions {
  padding: 10px var(--space-md);
  border-top: 1px solid var(--color-border);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  background: var(--color-surface-2);
}
.editor-action-btn {
  appearance: none;
  min-height: var(--target-min);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 6px 10px;
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  transition: background var(--transition), border-color var(--transition);
}
.editor-action-btn .icon { width: 13px; height: 13px; }@media (hover: hover) {

.editor-action-btn:hover { background: var(--color-surface-2); }
}
.editor-action-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.editor-action-btn.primary {
  background: var(--color-primary);
  color: var(--color-on-fill);
  border-color: var(--color-primary);
}@media (hover: hover) {

.editor-action-btn.primary:hover { background: var(--color-primary-dark, var(--color-primary)); }
}

/* Info icon next to Contribute — opens a friendly walkthrough popover */
.editor-help-btn { padding: var(--space-xs); }
.editor-help-btn .icon { width: 16px; height: 16px; }

/* Icon-only secondary actions: drop the label, keep visual weight low so the
   primary Contribute reads as the focal action. */
.editor-action-btn.editor-icon-only { padding: var(--space-xs); }
.editor-action-btn.editor-icon-only .icon { width: 16px; height: 16px; }

/* Push the Info+Contribute group to the right edge of the action row so the
   primary Contribute lands where the eye expects (bottom-right). The Info
   button carries the margin since wrap behaviour on narrow widths keeps the
   pair adjacent. */
.editor-help-btn { margin-left: auto; }

/* The focal primary: slightly larger padding + font so it visually outranks
   the secondary actions without changing layout. */
.editor-action-btn--focal {
  padding: 8px 14px;
  font-size: var(--font-size-sm);
}

/* Footer is position-relative-ready: popover anchors above the button row.
   Caps width so it never overflows the side panel on mobile (~360-480px). */
.editor-actions { position: relative; }
.editor-help-popover {
  position: absolute;
  bottom: calc(100% + 8px);
  right: var(--space-md);
  left: var(--space-md);
  max-width: 360px;
  margin-left: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
  padding: 14px 16px 12px;
  z-index: 1200;
  max-height: min(70vh, 480px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.editor-help-popover.hidden { display: none; }

/* On mobile the panel is short and the popover needs to clear the action
   row + tabs above it. Pin to the viewport (no ancestor uses transform/
   filter, so position:fixed escapes cleanly) so the popover can be
   tall + scrollable without being clipped by the panel container. */
@media (max-width: 900px) {
  .editor-help-popover {
    position: fixed;
    /* Clear of the home indicator as well as the sheet's own bottom edge. */
    bottom: calc(72px + env(safe-area-inset-bottom, 0px));
    left: 12px;
    right: 12px;
    max-width: none;
    max-height: 60vh;
  }
}
.editor-help-close {
  position: absolute;
  top: 6px;
  right: 6px;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: var(--space-2xs);
  border-radius: var(--radius-sm);
  line-height: 0;
}@media (hover: hover) {

.editor-help-close:hover { background: var(--color-surface-2); color: var(--color-text); }
}
.editor-help-close .icon { width: 12px; height: 12px; }
.editor-help-title {
  margin: 0 24px 6px 0;
  font-size: var(--font-size-md);
  color: var(--color-text);
}
.editor-help-blurb {
  margin: 0 0 10px 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  line-height: 1.45;
}
.editor-help-steps {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.editor-help-steps li {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  color: var(--color-text);
}
.editor-help-step-icon {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  color: var(--color-accent);
}
.editor-help-steps a {
  color: var(--color-accent-text);
  text-decoration: underline;
}

.editor-status {
  margin-left: auto;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  align-self: center;
}

/* While editor is active: dim existing proposal lines, ensure stops
   are interactive again, hint with a crosshair cursor when adding stops. */
body.editor-mode .leaflet-marker-icon.stop-marker-icon {
  pointer-events: auto !important;
  opacity: 1 !important;
}
body.editor-mode.editor-mode-add-stop .leaflet-marker-icon.stop-marker-icon {
  cursor: crosshair !important;
}
body.proposals-tab-active .route-network-line {
  opacity: 0.3;
  transition: opacity 0.4s ease;
}
/* Night mode shows only the sparse night-service network rather than the
   dense day network, so the heavy dim that keeps proposals legible by day
   is overkill — ease it off so the night routes stay readable behind a
   proposal. (Mirrors the night-mode exemption for endpoint tags below.) */
body.proposals-tab-active.service-mode-night .route-network-line {
  opacity: 0.65;
}

body.proposals-tab-active .route-endpoint-tag {
  opacity: 0.3;
  transition: opacity 0.4s ease;
}
/* Night mode swaps the entire visible network for night-only services
   — the dim-the-day-network rationale doesn't apply, so keep tags
   fully readable. */
body.proposals-tab-active.service-mode-night .route-endpoint-tag {
  opacity: 1;
}

body.editor-mode .route-network-line {
  opacity: 0.25 !important;
}

/* Suppress stop popups while in "Add stop" mode — otherwise Leaflet's
   bindPopup toggle opens one on click in addition to our interception,
   and the popup contains a "Live departures" button that would re-add
   the same stop to the draft. */
body.editor-mode.editor-mode-add-stop .leaflet-popup-pane {
  display: none !important;
}

/* ============================================================
   MOBILE SCROLL OVERRIDES (must live at the end of the file so they
   beat the desktop rules in the cascade). On mobile the side panel
   has a fixed max-height (45vh), so we let the active tab's content
   scroll as a single column instead of carving up the space between
   intro / filters / list and squeezing the list to near-zero height.
   Inner overflow:auto blocks (proposals-list, route-filter-chips,
   ticket-zones-list) are flattened so we don't get nested scroll regions
   on a phone.
   ============================================================ */
@media (max-width: 900px) {
  .panel-tab-content { overflow-y: auto; }

  .proposals-view {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }

  .proposals-list,
  .route-filter-chips,
  /* This one was missed, and it was the worst of them: flex:1 plus
     overflow-y:auto inside an already-scrolling parent squeezed the operator
     list to 32px holding 200px of content, so the cards that reveal the zone
     outlines were all but unreachable. No cards clicked, no boundaries. */
  .ticket-zones-list {
    flex: 0 0 auto;
    min-height: 0;
    overflow: visible;
  }

  .route-filters {
    flex: 0 0 auto;
    overflow: visible;
  }
}


/* ============================================================
   RAIL — stations, trains, live departure board
   ============================================================ */

.rail-station-divicon,
.rail-train-divicon { background: transparent; border: none; pointer-events: auto; }

.rail-station-icon {
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.45));
  transition: transform 120ms ease;
}@media (hover: hover) {

.rail-station-icon:hover { transform: scale(1.18); }
}
.rail-station-icon svg { width: 100%; height: 100%; }

/* Wrapper houses the pulse ring behind the pill. No bearing rotation —
   the pill has no directional glyph and the label needs to stay readable. */
/* iconSize [0,0] + iconAnchor [0,0] in JS; transform keeps the pill
   centred over the geographic point regardless of text width. */
.rail-train-icon-wrap {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  pointer-events: auto;
  transform: translate(-50%, -50%);
}

.rail-train-icon {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  height: 26px;
  min-width: 40px;
  padding: 0 8px;
  border-radius: 13px;
  color: var(--color-on-fill);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  border: 2px solid rgba(var(--color-on-fill-rgb), 0.95);
  box-shadow: 0 3px 10px rgba(0,0,0,0.55), 0 0 0 1px rgba(0,0,0,0.25);
  white-space: nowrap;
  overflow: hidden;
  z-index: 2;
}
.rail-train-icon .rail-train-label {
  position: relative;
  z-index: 2;
  white-space: nowrap;
  text-shadow: 0 1px 4px rgba(0,0,0,0.9), 0 0 8px rgba(0,0,0,0.6);
}

/* Front-carriage livery image covers the pill; operator colour shows while
   loading or on 404. onload sets opacity:1; onerror leaves it hidden. */
.rail-train-livery {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: left center;
  border-radius: 11px;
  opacity: 0;
  z-index: 1;
  pointer-events: none;
}

/* Pulse ring tracks the pill itself (it's a child of .rail-train-icon).
   `inset: -2px` so the ring sits just outside the pill border at scale 1
   and grows from there. Appears for ~4 s on first show; the `pulse`
   modifier class is removed by JS so long-tracked markers don't strobe. */
.rail-train-pulse {
  position: absolute;
  inset: -2px;
  border-radius: 13px;
  background: var(--rail-train-bg, #0b66c2);
  opacity: 0;
  z-index: 1;
  pointer-events: none;
}
.rail-train-icon-wrap--pulse .rail-train-pulse {
  animation: rail-train-pulse 1.4s ease-out infinite;
}
@keyframes rail-train-pulse {
  0%   { transform: scale(1);   opacity: 0.55; }
  100% { transform: scale(1.9); opacity: 0;    }
}

/* Incoming = train approaching from out-of-bbox. Marker pins at the first
   in-bbox station with a dashed border + slower pulse so the user can tell
   it's not chord-interpolated. */
.rail-train-icon-wrap--incoming .rail-train-icon {
  border-style: dashed;
  border-color: rgba(var(--color-on-fill-rgb), 0.85);
}
.rail-train-icon-wrap--incoming .rail-train-pulse {
  animation: rail-train-pulse 2.2s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .rail-train-icon-wrap--pulse .rail-train-pulse,
  .rail-train-icon-wrap--incoming .rail-train-pulse { animation: none; opacity: 0.25; }
}

/* Stops and rail markers all become small + faint when not in Live view —
   handled for stops already; rail just inherits the same gating via Live-only
   visibility. No extra rule needed there. */

/* ── Live rail departure board (rendered inside the Stop tab) ── */
.rail-board {
  display: flex; flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-sm) var(--space-md);
}
.rail-board-header {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-sm);
}
.rail-board-eyebrow {
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.rail-board-station {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.rail-board-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}
.rail-board-table th,
.rail-board-table td {
  text-align: left;
  padding: 6px 8px;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}
.rail-board-table th {
  font-size: var(--font-size-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}
.rail-col-time { width: 22%; white-space: nowrap; font-family: var(--font-mono); }
.rail-col-plat { width: 10%; text-align: center; }
.rail-col-op   { width: 22%; }
.rail-col-act  { width: 14%; text-align: right; }

.rail-time-sched { font-weight: 600; }
.rail-time { margin-left: 4px; font-size: var(--font-size-xs); font-weight: 600; }
.rail-time--actual    { color: #1e8e3e; }
.rail-time--changed   { color: #d97706; }
.rail-time--cancelled { color: #b91c1c; text-decoration: line-through; }

.rail-row--cancelled td { opacity: 0.6; }

.rail-plat {
  display: inline-block;
  min-width: 1.6em;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 700;
}
.rail-plat--unconfirmed { opacity: 0.65; font-style: italic; }

.rail-op-badge {
  display: inline-block;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.rail-track-btn {
  appearance: none;
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-text);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-2xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 4px 8px;
  cursor: pointer;
}@media (hover: hover) {

.rail-track-btn:hover { background: var(--color-border); }
}
.rail-track-btn--on {
  background: var(--color-primary);
  color: var(--color-on-fill);
  border-color: var(--color-primary);
}

.rail-board-loading,
.rail-board-empty,
.rail-board-error {
  text-align: center;
  padding: var(--space-md);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.rail-board-error { color: #b91c1c; }

.rail-board-footer {
  font-size: var(--font-size-2xs);
  color: var(--color-text-muted);
  font-style: italic;
  margin-top: var(--space-2xs);
}

/* ── Train marker popup ── */
.rail-train-popup-wrap .leaflet-popup-content {
  margin: 8px 10px;
  font-family: var(--font-family);
}
.rail-train-popup-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: 4px;
}
.rail-train-popup-headcode {
  font-family: var(--font-mono);
  font-size: var(--font-size-md);
  font-weight: 700;
}
.rail-train-popup-op {
  font-size: var(--font-size-2xs);
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.rail-train-popup-seg {
  font-size: var(--font-size-xs);
  color: var(--color-text);
  margin-bottom: 6px;
}
.rail-train-popup-calls {
  list-style: none;
  padding: 0;
  margin: 0 0 6px 0;
  font-size: var(--font-size-xs);
}
.rail-train-popup-calls li { padding: 2px 0; }
.rail-train-popup-foot {
  font-size: var(--font-size-2xs);
  font-style: italic;
  color: var(--color-text-muted);
  margin: 4px 0 0 0;
}

/* ============================================================
   NETWORK OBJECTIVES — grouped by who is responsible
   ============================================================ */

/* The body groups reuse the Ticket view's operator accordion wholesale
   (.ticket-operator-group / -card / -name / -count / -chevron / -zones,
   .ticket-subgroup-heading, .ticket-operator-footnote, .ticket-zone-link),
   so there is deliberately no duplicate styling here — only the pieces the
   ticket list has no equivalent for. If you restyle that accordion, this
   follows automatically, which is the point. */

/* Pinned campaign asks, above the collapsed body groups. */
.objective-featured { margin-bottom: var(--space-md); }
.objective-bodies-intro {
  margin: 0 0 var(--space-2xs);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* Group heading, used by the pinned section and the ideas feed. */
.objective-group + .objective-group { margin-top: var(--space-md); }
.objective-group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  margin: 0 0 var(--space-xs);
  padding-bottom: 4px;
  border-bottom: 1px solid var(--color-border);
}
.objective-group-name {
  font-size: var(--font-size-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text);
}
.objective-group-count {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* Chips naming who is responsible. Only the lead gets a filled, brand- or
   palette-coloured chip: a body that merely has to agree shouldn't look like
   the one you write to first. */
.objective-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: var(--space-xs);
}
.objective-chip {
  padding: 1px 7px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface-2);
  color: var(--color-text-muted);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.objective-chip--lead {
  background: var(--chip-bg);
  color: var(--chip-fg);
  border-color: transparent;
}

/* The objective cards are <button>s, so they need the block layout the ticket
   sub-cards get from being <div>s. */
.body-group .proposal-card {
  display: flex;
  flex-direction: column;
  width: 100%;
  text-align: left;
}
