:root {
  --bg: #020817;
  --panel: rgba(15, 23, 42, 0.9);
  --panel-2: #111827;
  --line: rgba(148, 163, 184, 0.24);
  --text: #e5e7eb;
  --muted: #94a3b8;
  --brand: #0ea5e9;
  --brand-2: #22c55e;
  --danger: #ef4444;
  --shadow: 0 20px 40px rgba(15, 23, 42, 0.28);
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  font-family: "Inter", sans-serif;
  background: linear-gradient(180deg, #020817 0%, #0f172a 100%);
  color: var(--text);
  overflow: hidden;
}

body {
  display: flex;
  flex-direction: column;
}

.app-shell {
  display: grid;
  grid-template-columns: 340px 1fr;
  height: 100%;
  width: 100%;
  flex: 1;
}

/* Matches the apple-touch-startup-image splash PNGs' own look (same
   #111827 background, same mark, roughly the same ~34% width) -- see
   index.html's own comment on #appSplashOverlay for why this exists. */
.app-splash-overlay {
  position: fixed;
  inset: 0;
  z-index: 5000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #111827;
  opacity: 1;
  transition: opacity 0.4s ease;
}

.app-splash-overlay-hidden {
  opacity: 0;
  pointer-events: none;
}

.app-splash-icon {
  width: 34vw;
  max-width: 180px;
}

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

.panel {
  background: rgba(15, 23, 42, 0.9);
  border-right: 1px solid var(--line);
  padding: 28px 22px;
  backdrop-filter: blur(18px);
  overflow-y: auto;
  max-height: 100%;
  scrollbar-width: thin;
  scrollbar-color: rgba(148, 163, 184, 0.45) transparent;
}

.panel::-webkit-scrollbar {
  width: 8px;
}

.panel::-webkit-scrollbar-track {
  background: transparent;
}

.panel::-webkit-scrollbar-thumb {
  background-color: rgba(148, 163, 184, 0.45);
  border-radius: 999px;
}

.panel-header {
  position: relative;
}

.panel-header h1 {
  margin: 8px 0 0;
  font-size: 2rem;
}

.panel-title {
  display: flex;
  align-items: center;
  gap: 16px;
}

.app-icon {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
}

.eyebrow {
  margin: 0;
  font-size: 0.76rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--brand-2);
}

.panel-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
}

.favourites-control {
  position: relative;
}

/* Only the main panel's favourites button moved into the left-aligned
   .panel-toolbar row -- its dropdown needs to open from the button's own
   left edge there instead of .favourites-control-menu's default right:0
   (which still suits the splash's copy, whose button stays top-right --
   see .splash-favourites-control), otherwise it opens off-screen to the
   left (confirmed live). */
.panel-toolbar .favourites-control-menu {
  left: 0;
  right: auto;
}

.favourites-control-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(148, 163, 184, 0.12);
  color: var(--text);
  cursor: pointer;
}

.favourites-control-btn:hover {
  border-color: #f59e0b;
  color: #f59e0b;
}

.panel-help-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(148, 163, 184, 0.12);
  color: var(--text);
  cursor: pointer;
}

.panel-help-btn:hover {
  border-color: var(--brand);
  color: var(--brand);
}

.favourites-control-menu {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 6px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  overflow-y: auto;
  max-height: 320px;
  min-width: 240px;
  z-index: 1000;
}

.favourites-control-menu.open {
  display: block;
}

.favourite-group-label {
  padding: 8px 10px 4px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.favourite-row {
  display: flex;
  align-items: center;
  gap: 4px;
}

.favourite-row-label {
  flex: 1;
  padding: 8px 10px;
  border: none;
  background: none;
  text-align: left;
  font-family: inherit;
  font-size: 0.82rem;
  color: var(--text);
  cursor: pointer;
}

.favourite-row:hover {
  background: rgba(148, 163, 184, 0.08);
}

.favourite-row-remove {
  flex-shrink: 0;
  margin-right: 6px;
  border: none;
  background: none;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.favourite-row-remove:hover {
  color: var(--danger);
}

/* Saved-search rows' own operator/line subline (see favouritesFlyout.js's
   findRouteLabel) -- the main label is "Board stop → Arrive stop", which on
   its own doesn't say which service that actually is when the same two
   stops have more than one favourited line between them. */
.favourite-row-sub {
  display: block;
  margin-top: 2px;
  font-size: 0.72rem;
  color: var(--muted);
}

.favourites-empty {
  padding: 14px 12px;
  color: var(--muted);
  font-size: 0.82rem;
  max-width: 220px;
}

.filters {
  margin-top: 30px;
  padding: 18px 16px;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: rgba(31, 41, 55, 0.7);
}

.filters label {
  display: block;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin-bottom: 10px;
}

/* ---------- County / Bus number / Operator checkbox dropdowns ---------- */
/* #region-select is now just the empty <div> CheckboxDropdown mounts into
   (see main.js's countyDropdown) -- its own .checkbox-dropdown-toggle
   below carries the actual select-like styling, same as Bus number/
   Operator already did before County became multi-select too. */

.checkbox-dropdown {
  position: relative;
  /* Matches #find-from-input/#find-to-input's own width:100% -- harmless in
     the standalone block context (Bus number/Operator), and needed so the
     County dropdown (now inside a .find-input-row flex row alongside its
     own pick-on-map button) shrinks to fit next to it the same way those
     text inputs already do, rather than sizing to its own content. */
  width: 100%;
}

.checkbox-dropdown-row {
  display: flex;
  gap: 6px;
}

.checkbox-dropdown-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--text);
  font-family: inherit;
  font-size: 0.92rem;
  text-align: left;
  cursor: pointer;
}

.checkbox-dropdown-toggle:focus {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

.checkbox-dropdown-toggle:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

/* One-click clear, next to the toggle -- only shown once something is
   selected (see CheckboxDropdown's own _updateSummary) -- so you never
   need to open a long list and scroll to find whichever row is currently
   checked just to turn it off. */
.checkbox-dropdown-clear {
  flex-shrink: 0;
  width: 36px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--muted);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
}

.checkbox-dropdown-clear:hover {
  color: var(--danger);
  border-color: var(--danger);
}

/* Same visual recipe as .checkbox-dropdown-clear above, generalised for
   any plain text input that wants a one-click clear button next to it
   (currently the splash's own Find Routes From/To fields) -- `hidden`
   toggled by main.js in step with the input's own value, same "only shown
   once there's something to clear" convention. */
.input-clear-btn {
  flex-shrink: 0;
  width: 36px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--muted);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
}

.input-clear-btn:hover {
  color: var(--danger);
  border-color: var(--danger);
}

.checkbox-dropdown-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  background: rgba(15, 23, 42, 0.97);
  border: 1px solid rgba(148, 163, 184, 0.25);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 8px;
}

.checkbox-dropdown-filter {
  width: 100%;
  padding: 8px 10px;
  margin-bottom: 6px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: rgba(2, 8, 23, 0.6);
  color: var(--text);
  font-family: inherit;
  font-size: 0.85rem;
}

.checkbox-dropdown-list {
  max-height: 220px;
  overflow-y: auto;
}

.checkbox-dropdown-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: 6px;
  background: none;
  font-family: inherit;
  font-size: 0.85rem;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.checkbox-dropdown-item:hover {
  background: rgba(148, 163, 184, 0.1);
}

.checkbox-dropdown-item.selected {
  background: rgba(14, 165, 233, 0.18);
  color: var(--brand);
  font-weight: 600;
}

.checkbox-dropdown-item.selected::after {
  content: "✓";
  margin-left: 8px;
}

.checkbox-dropdown-item-label {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

/* Small coloured circle before a row's label (Operator dropdown only, see main.js) -- same "gig listing" dot pattern as the45_webapp, tying each row to the colour its routes are actually drawn with on the map. */
.checkbox-dropdown-swatch {
  flex-shrink: 0;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}

.checkbox-dropdown-empty {
  padding: 8px 4px;
  font-size: 0.8rem;
  color: var(--muted);
}

/* Non-interactive heading row above a run of grouped options (ComboSelect
   only -- see its own setOptions doc comment; Journey Planner's From-stop
   dropdown when an operator+line has more than one real distinct route). */
.checkbox-dropdown-group-heading {
  padding: 8px 10px 4px;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.second-label {
  margin-top: 16px;
}

/* .filters (not just .route-toggle) in the selector so this reliably
   outweighs .filters label's own specificity (a class + an element type
   beats a single class) -- .route-toggle alone was silently losing
   margin-bottom, text-transform and letter-spacing to that rule despite
   declaring its own values for all three (confirmed live: "Show route"
   was rendering as "SHOW ROUTE" in caps, and margin-bottom stayed 10px
   no matter what value was set here). */
.filters .route-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  color: var(--text);
  font-size: 0.9rem;
  font-weight: normal;
  text-transform: none;
  letter-spacing: normal;
  /* The real fix for the star looking low: this margin, being on the
     bottom of this label but not of its sibling .favourite-star-btn,
     inflated this label's own box taller than the star's within
     .route-toggle-row -- align-items: center then centered both against
     that taller combined height, leaving the checkbox+text sitting near
     the top of its own box while the star sat at the row's true centre,
     reading as the star being low. */
  margin-bottom: 0px;
}

.route-toggle input[type="checkbox"] {
  accent-color: var(--brand);
  cursor: pointer;
}

.route-toggle input[type="checkbox"]:disabled {
  cursor: not-allowed;
}

.route-toggle-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.favourite-star-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  flex-shrink: 0;
  background: none;
  border: none;
  padding: 0;
  line-height: 0;
  color: var(--muted);
  cursor: pointer;
}

/* Inline <svg> defaults to baseline alignment, leaving a small gap below
   it meant for text descenders -- harmless in flowing text, but it threw
   off this icon-only button's own box height next to the "Show route"
   label. display:block removes that gap (the real fix for the label
   being visually low is .route-toggle's own margin-bottom override). */
.favourite-star-btn svg {
  display: block;
}

.favourite-star-btn .star-path {
  transition: fill 0.15s ease;
}

.favourite-star-btn:hover:not(:disabled) {
  color: #f59e0b;
}

.favourite-star-btn.active {
  color: #f59e0b;
}

.favourite-star-btn.active .star-path {
  fill: currentColor;
}

.favourite-star-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.route-toggle:has(input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}

.mode-switch {
  display: flex;
  margin-top: 14px;
  background: rgba(15, 23, 42, 0.8);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}

.mode-btn {
  flex: 1;
  background: transparent;
  border: none;
  color: var(--muted);
  padding: 8px 10px;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.85rem;
  font-family: inherit;
}

.mode-btn.active {
  background: var(--brand);
  color: #04202e;
}

.mode-btn:disabled {
  cursor: not-allowed;
  opacity: 0.4;
}

.status-text {
  margin-top: 14px;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.4;
}

/* Moved out of Find All Nearby Routes' own card into the main filters
   panel -- it resets Bus number/Operator and the splash's own postcode
   search marker too (see main.js's handleClearSearch), not just Find
   Routes/Nearby Routes, so it needed to be reachable without first
   opening that collapsible panel. */
.clear-searches-btn {
  width: 100%;
  margin-top: 14px;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--muted);
  font-family: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.clear-searches-btn:hover {
  border-color: var(--brand);
  color: var(--text);
}

/* ---------- Collapsible sections (disruptions) ---------- */

.leaderboard {
  margin-top: 26px;
}

.leaderboard h2 {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}

.section-count {
  background: var(--danger);
  color: white;
  font-size: 0.68rem;
  font-weight: 700;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  text-transform: none;
  letter-spacing: normal;
}

.collapsible-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: transparent;
  border: 0;
  padding: 0;
  font-family: inherit;
  cursor: pointer;
}

.collapsible-chevron {
  color: var(--muted);
  font-size: 0.75rem;
  transition: transform 0.2s ease;
  flex-shrink: 0;
}

.collapsible-section.collapsed .collapsible-chevron {
  transform: rotate(-90deg);
}

.collapsible-body {
  margin-top: 12px;
}

.collapsible-section.collapsed .collapsible-body {
  display: none;
}

#disruptions-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.disruption-card {
  background: rgba(31, 41, 55, 0.6);
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand-2);
  border-radius: 10px;
  padding: 10px 12px;
  font-size: 0.83rem;
}

.disruption-card.severe {
  border-left-color: var(--danger);
}

.disruption-card summary {
  cursor: pointer;
  font-weight: 600;
}

.disruption-card p {
  margin: 8px 0 0;
  color: var(--muted);
  line-height: 1.4;
}

.disruption-card .meta {
  margin-top: 8px;
  font-size: 0.74rem;
  color: var(--muted);
}

.disruption-card a {
  color: var(--brand);
}

.disruption-empty {
  color: var(--muted);
  font-size: 0.85rem;
  padding: 10px 2px;
}

/* ---------- Find Routes ---------- */

.find-routes {
  margin-top: 26px;
  padding: 18px 16px;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: rgba(31, 41, 55, 0.7);
}

/* .filters h2 alongside .find-routes h2 -- View Live Services' own heading
   lives inside .filters (its card styling comes from .filters itself, see
   that class' own comment), not .find-routes, now that the earlier
   double-box bug's fix removed the .find-routes class from
   #viewLiveServicesSection. Without this, that h2 fell back to the
   browser's own default h2 styling -- bold, dark, oversized, with a big
   default top/bottom margin that (since margins don't collapse inside a
   flex item, see .collapsible-header) visibly inflated the whole header
   row's height. */
.find-routes h2,
.filters h2,
#journeyPlannerSection h2 {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin: 0;
}

/* Matches .find-routes' own card chrome exactly (margin, padding, border,
   radius, fill) -- this section sits at the same level as .find-routes/
   .filters (a direct .panel child, not nested inside either), so it
   needs that same styling applied directly rather than inherited.
   Confirmed live as a real bug otherwise: View Live Services and Find
   Routes From/To both rendered as filled rounded-corner cards, but
   Journey Planner sat flush against the bare panel background with no
   card around it at all -- "no consistency in style." */
#journeyPlannerSection {
  margin-top: 26px;
  padding: 18px 16px;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: rgba(31, 41, 55, 0.7);
}

.find-input-group {
  margin-bottom: 12px;
}

.find-input-group label {
  display: block;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin-bottom: 8px;
}

.find-input-row {
  display: flex;
  gap: 8px;
}

#find-from-input,
#find-to-input {
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--text);
  font-family: inherit;
  font-size: 0.92rem;
}

#find-from-input:focus,
#find-to-input:focus {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

.find-locate-btn {
  flex-shrink: 0;
  width: 40px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--brand);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.find-locate-btn:hover {
  border-color: var(--brand);
}

.find-locate-btn.active {
  background: var(--brand);
  color: #04202e;
  border-color: var(--brand);
}

/* Address autocomplete (see ui/addressAutocomplete.js) -- the anchor class
   is added to whichever element the autocomplete input already lives in
   (typically .find-input-row, the flex row shared with this field's own
   locate/pick buttons), so the dropdown below positions against that whole
   row rather than just the bare input. Styling matches
   .checkbox-dropdown-menu/-item, the closest existing "dark dropdown menu"
   in this app. */
.address-autocomplete-anchor {
  position: relative;
}

.address-autocomplete-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  background: rgba(15, 23, 42, 0.97);
  border: 1px solid rgba(148, 163, 184, 0.25);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 6px;
  max-height: 240px;
  overflow-y: auto;
}

.address-autocomplete-item {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: 6px;
  background: none;
  font-family: inherit;
  font-size: 0.85rem;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.address-autocomplete-item:hover,
.address-autocomplete-item.active {
  background: rgba(148, 163, 184, 0.1);
}

/* Non-selectable status line (searching/no matches/error) -- see
   addressAutocomplete.js's showMessage(). */
.address-autocomplete-message {
  padding: 8px 10px;
  font-size: 0.82rem;
  color: var(--muted);
}

.find-distance-group {
  margin-bottom: 16px;
}

.find-distance-group label {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.find-distance-value {
  color: var(--brand);
  font-weight: 700;
  letter-spacing: normal;
  text-transform: none;
  font-size: 0.85rem;
}

.find-distance-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 6px;
  border-radius: 999px;
  /* --fill (0-100%, set on "input" in main.js) draws the filled portion of
     the track left of the thumb in Chrome/Edge/Safari -- appearance:none
     makes the input's own background double as the track there. Firefox
     ignores this and uses ::-moz-range-progress below instead. */
  background: linear-gradient(
    to right,
    var(--brand) 0%,
    var(--brand) var(--fill, 100%),
    rgba(148, 163, 184, 0.24) var(--fill, 100%),
    rgba(148, 163, 184, 0.24) 100%
  );
  cursor: pointer;
  outline-offset: 4px;
}

.find-distance-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--bg);
  box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.25);
  cursor: pointer;
}

.find-distance-slider::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: rgba(148, 163, 184, 0.24);
}

.find-distance-slider::-moz-range-progress {
  height: 6px;
  border-radius: 999px;
  background: var(--brand);
}

.find-distance-slider::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--bg);
  box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.25);
  cursor: pointer;
}

.find-distance-slider:focus-visible {
  outline: 2px solid var(--brand);
}

.find-route-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

.find-route-btn {
  flex: 1;
  padding: 10px 12px;
  border-radius: 10px;
  border: none;
  background: var(--brand);
  color: #04202e;
  font-family: inherit;
  font-weight: 700;
  font-size: 0.9rem;
  text-align: center;
  cursor: pointer;
}

.find-route-btn:hover {
  filter: brightness(1.08);
}

.find-route-btn:disabled {
  cursor: not-allowed;
  opacity: 0.5;
  filter: none;
}

.find-route-result {
  margin-top: 14px;
}

/* ---------- Journey Planner ---------- */

.journey-leg {
  padding: 12px;
  margin-bottom: 12px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: rgba(15, 23, 42, 0.5);
}

.journey-leg-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 4px;
}

.journey-leg-title {
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.journey-leg-remove-btn {
  border: none;
  background: none;
  color: var(--muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}

.journey-leg-remove-btn:hover {
  color: var(--danger);
}

/* ComboSelect.js adds the real .checkbox-dropdown class to this same
   element -- already fully self-contained (width, colours -- see its own
   CSS further up) in a standalone block context exactly like this one,
   just a bit tight here with five of these stacked per leg. */
.journey-leg-combo.checkbox-dropdown {
  padding: 4px;
}

.journey-leg-warning {
  margin-top: 6px;
  font-size: 0.8rem;
  color: var(--danger);
}

.journey-leaving-row {
  display: flex;
  gap: 8px;
  padding: 6px;
}

.journey-leaving-row input[type="time"] {
  flex: 1;
  padding: 9px 10px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--text);
  font-family: inherit;
  font-size: 0.85rem;
}

.journey-leaving-row input:disabled {
  opacity: 0.4;
}

.journey-leaving-now-btn {
  flex-shrink: 0;
  padding: 9px 14px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--muted);
  font-family: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.journey-leaving-now-btn.active {
  background: var(--brand);
  border-color: var(--brand);
  color: #04202e;
  font-weight: 700;
}

.journey-add-leg-btn {
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px dashed var(--line);
  background: transparent;
  color: var(--brand);
  font-family: inherit;
  font-size: 0.88rem;
  cursor: pointer;
}

.journey-add-leg-btn:hover {
  border-color: var(--brand);
}

.journey-actions {
  margin-top: 4px;
  display: flex;
}

.journey-result-meta,
.find-route-result-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.find-route-refresh-btn {
  flex-shrink: 0;
  margin-left: 10px;
}

.journey-result-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.journey-refresh-btn {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(148, 163, 184, 0.12);
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.journey-refresh-btn:hover {
  border-color: var(--brand);
  color: var(--text);
}

.journey-favourite-btn {
  flex-shrink: 0;
}

.journey-result-row {
  cursor: default;
}

.journey-result-row-clickable {
  cursor: pointer;
}

/* .route-candidate-row.journey-result-row-clickable (two classes), not
   .journey-result-row-clickable alone -- needs to outweigh
   .route-candidate-row:hover's own border-color further down this file
   (equal specificity otherwise would just lose to source order). */
.route-candidate-row.journey-result-row-clickable:hover {
  border-color: var(--candidate-color, var(--brand));
  background: rgba(var(--candidate-color-rgb, 14, 165, 233), 0.08);
}

/* Fixed overlay, same convention as .app-splash-overlay -- a small modal
   for naming a journey before it can be favourited (see
   handleToggleJourneyFavourite in main.js). No existing favourite type in
   this app needed a name prompt before this, so this is genuinely new UI,
   not a reused pattern. */
.journey-name-dialog {
  position: fixed;
  inset: 0;
  z-index: 4000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(2, 8, 23, 0.72);
}

/* The class rule above's own `display: flex` otherwise outweighs the
   browser's default `[hidden] { display: none }` (equal specificity, but
   that default lives in the lowest-priority user-agent stylesheet layer,
   which loses to any plain author-stylesheet class rule) -- confirmed
   live as a real bug: the dialog showed on every page load regardless of
   the `hidden` attribute main.js sets/clears on it. This selector's own
   higher specificity (a class + an attribute) restores `hidden` actually
   hiding it. */
.journey-name-dialog[hidden] {
  display: none;
}

.journey-name-dialog-card {
  width: min(320px, calc(100vw - 48px));
  padding: 20px;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--panel-2);
  box-shadow: var(--shadow);
}

.journey-name-dialog-card label {
  display: block;
  margin-bottom: 8px;
  font-size: 0.82rem;
  color: var(--muted);
}

.journey-name-dialog-card input {
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--text);
  font-family: inherit;
  font-size: 0.92rem;
}

.journey-name-dialog-actions {
  margin-top: 14px;
  display: flex;
  gap: 8px;
}

.journey-name-dialog-actions .clear-searches-btn,
.journey-name-dialog-actions .find-route-btn {
  margin-top: 0;
}

.route-result-card {
  background: rgba(31, 41, 55, 0.6);
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: 10px;
  padding: 14px 16px;
}

.route-candidates-list {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* A plain button per row -- clicking anywhere on it toggles that candidate
   on/off (see main.js's handleToggleRouteCandidate). No separate select
   control any more: confirmed live as wanted, back to one click doing the
   whole job rather than a preview-then-commit checkbox. */
.route-candidate-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: rgba(15, 23, 42, 0.5);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.route-candidate-row:hover {
  border-color: var(--muted);
}

/* Line/meta on the left, the favourite star on the right -- needed once the
   star (a real <button>) had to live inside the row (a <div role="button">
   now, not a <button> itself -- see findRoutePanel.js's own comment on why
   nested buttons aren't valid HTML). */
.route-candidate-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  width: 100%;
  gap: 8px;
}

.route-candidate-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.route-candidate-favourite-btn {
  margin-top: 2px;
}

/* Small warning-triangle badge (format.js's disruptionBadge()) -- findRoutePanel.js's
   own candidate rows and journeyPlannerPanel.js's own leg result rows both
   use it, next to the favourite star/journey-leg header respectively. */
.disruption-badge-btn {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--danger);
  cursor: pointer;
}

.disruption-badge-btn:hover {
  opacity: 0.8;
}

.journey-leg-disruption {
  flex-shrink: 0;
  margin-top: 2px;
}

.route-candidate-line {
  font-size: 1.1rem;
  font-weight: 800;
  color: var(--muted);
  line-height: 1.2;
}

.route-candidate-meta {
  margin-top: 2px;
  font-size: 0.75rem;
  color: var(--muted);
}

/* The one active (toggled-on) candidate -- --candidate-color/
   --candidate-color-rgb are set per row (see findRoutePanel.js) to that
   route's own real operator colour (colorForOperator, same palette/key
   RouteCandidatesLayer draws its map line with), not a fixed hue -- so the
   card and the map line always agree on which option is "on" AND on what
   colour it actually is. */
.route-candidate-row.active {
  border-color: var(--candidate-color, #f59e0b);
  background: rgba(var(--candidate-color-rgb, 245, 158, 11), 0.1);
}

.route-candidate-row.active .route-candidate-line {
  color: var(--candidate-color, #f59e0b);
}

/* Boarding/alighting stop names + times, appended inside each row. */
.route-stop-times {
  margin-top: 6px;
  padding-top: 6px;
  width: 100%;
  border-top: 1px solid var(--line);
  font-size: 0.75rem;
  color: var(--text);
}

.route-stop-times-row + .route-stop-times-row {
  margin-top: 2px;
}

.route-stop-times-time {
  font-weight: 700;
  color: var(--candidate-color, var(--brand));
  margin-right: 4px;
}

/* "(X min late)" next to the board time, "(estimated)" next to a delay-
   adjusted arrival time -- same amber as .tt-stop-row-estimate's own
   "Est. HH:MM" badge in the stop popup, for the same reason (a delayed
   time with nothing marking it as such reads as perfectly on time). */
.route-stop-times-delay {
  margin-right: 4px;
  font-size: 0.7rem;
  font-weight: 600;
  color: #f59e0b;
}

.route-stop-times-loading,
.route-stop-times-empty {
  color: var(--muted);
  font-style: italic;
}

.route-stop-departures-btn {
  display: block;
  margin-top: 8px;
  padding: 0;
  border: none;
  background: none;
  color: var(--candidate-color, var(--brand));
  font-family: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.route-stop-departures-btn:hover {
  text-decoration: underline;
}

.route-result-meta {
  margin-top: 6px;
  font-size: 0.8rem;
  color: var(--muted);
  line-height: 1.4;
}

.route-live-status {
  margin-top: 8px;
  padding: 6px 10px;
  border-radius: 8px;
  font-size: 0.8rem;
  font-weight: 600;
  background: rgba(34, 197, 94, 0.12);
  color: var(--brand-2);
}

.route-live-status.route-live-status-none {
  background: rgba(148, 163, 184, 0.12);
  color: var(--muted);
  font-weight: 400;
}

.route-result-empty {
  color: var(--muted);
  font-size: 0.85rem;
  padding: 10px 2px;
}

/* Shared "the app is working on this" indicator -- findRoutePanel.js's
   renderRouteSearching and nearbyRoutesPanel.js's renderNearbyRoutesSearching
   both use it. Confirmed live as wanted: plain searching text alone wasn't
   obviously "loading," especially since a cold search can genuinely take
   10-20+ seconds (checking several candidate operator/line pairs' real
   route geometry) -- a visibly spinning indicator reads as "still working,"
   not "stuck." */
.route-result-loading {
  display: flex;
  align-items: center;
  gap: 10px;
}

.loading-spinner {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border: 2.5px solid rgba(148, 163, 184, 0.25);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: loading-spinner-spin 0.8s linear infinite;
}

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

.route-result-searched {
  margin-top: 8px;
  font-size: 0.75rem;
  color: var(--muted);
  opacity: 0.75;
}

.route-result-error {
  color: var(--danger);
  font-size: 0.85rem;
  padding: 10px 2px;
}

/* ---------- Nearby Routes (within the Find Routes card) ---------- */

.nearby-routes {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

.nearby-routes h3 {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin: 0 0 12px;
}

.nearby-radius-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 0.82rem;
  color: var(--muted);
  margin-bottom: 8px;
}

#nearby-radius-input {
  width: 64px;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--text);
  font-family: inherit;
  font-size: 0.85rem;
}

/* Which point(s) "Nearby routes" searches around -- see main.js's
   nearbyRoutesLocation. Smaller variant of .mode-switch/.mode-btn (same
   segmented-control pattern already used for the Bus/Rail toggle), sized
   to sit inline under the radius row rather than as its own full-width
   block. */
.nearby-location-switch {
  display: flex;
  margin-bottom: 12px;
  background: rgba(15, 23, 42, 0.8);
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
}

.nearby-location-btn {
  flex: 1;
  background: transparent;
  border: none;
  color: var(--muted);
  padding: 6px 8px;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.78rem;
  font-family: inherit;
}

.nearby-location-btn.active {
  background: var(--brand);
  color: #04202e;
}

.nearby-location-btn:disabled {
  color: var(--muted);
  opacity: 0.35;
  cursor: not-allowed;
}

.nearby-routes-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.nearby-routes-btn {
  flex: 1;
  padding: 10px 12px;
  border-radius: 10px;
  border: none;
  background: var(--brand);
  color: #04202e;
  font-family: inherit;
  font-weight: 700;
  font-size: 0.85rem;
  cursor: pointer;
}

.nearby-routes-btn:hover {
  filter: brightness(1.08);
}

.nearby-routes-btn:disabled {
  cursor: not-allowed;
  opacity: 0.5;
  filter: none;
}

.nearby-routes-filter-btn {
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--muted);
  font-family: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.nearby-routes-filter-btn:hover {
  border-color: var(--brand);
  color: var(--text);
}

.nearby-routes-filter-btn:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

/* Full-width, on its own row below the other two actions -- the single
   remaining clear control for the whole Find Routes panel (direct-route
   search and nearby-routes search both reset via the same shared
   main.js's handleClearSearch), now that Find All Nearby Routes is nested
   inside the same collapsible panel rather than living in its own card. */
/* Armed and waiting for you to click points on the map -- same active recipe as .find-locate-btn.active. */
.nearby-routes-filter-btn.active {
  background: var(--brand);
  border-color: var(--brand);
  color: #04202e;
}

.nearby-routes-result {
  margin-top: 14px;
}

.route-legend {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.route-legend-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.route-legend-operator {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 4px 6px;
  border: none;
  border-radius: 6px;
  background: none;
  font-family: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.route-legend-operator:hover {
  background: rgba(148, 163, 184, 0.1);
}

/* Every line under this operator has been toggled off (see ui/nearbyRoutesPanel.js's syncOperatorHeaderState) -- dimmed so it's clear at a glance which operators are fully hidden. */
.route-legend-operator-off {
  opacity: 0.45;
  text-decoration: line-through;
}

.route-legend-swatch {
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  border-radius: 3px;
}

.route-legend-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding-left: 20px;
}

/* Individually toggleable per-line chip -- each one is its own on/off (see NearbyRoutesLayer.setGroupVisible), independent of its sibling lines under the same operator. */
.route-legend-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px 4px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(15, 23, 42, 0.6);
  font-family: inherit;
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
}

.route-legend-chip:hover {
  border-color: var(--muted);
}

.route-legend-chip.active {
  background: rgba(148, 163, 184, 0.18);
  border-color: var(--text);
  color: var(--text);
}

/* Live-bus-count badge, updated in place (see ui/nearbyRoutesPanel.js's setChipLiveCount) as pollNearbyRoutesVehicles ticks -- "…" before the first poll resolves. */
.route-legend-chip-live {
  min-width: 14px;
  padding: 1px 5px;
  border-radius: 999px;
  font-size: 0.68rem;
  font-weight: 700;
  text-align: center;
  background: rgba(148, 163, 184, 0.18);
  color: var(--muted);
}

.route-legend-chip-live.has-buses {
  background: rgba(34, 197, 94, 0.2);
  color: var(--brand-2);
}

.route-legend-chip-live.no-buses {
  background: rgba(148, 163, 184, 0.12);
  color: var(--muted);
}

/* Hovering a line's chip in the Nearby Routes legend (see
   ui/nearbyRoutesPanel.js's onHoverRoute -> NearbyRoutesLayer.flashGroup)
   blinks that exact route's polyline on the map so it's easy to pick out
   from several overlapping/nearby lines at once -- CSS on the SVG <path>
   Leaflet renders each polyline as, not a Leaflet style option, since a
   plain class selector already beats Leaflet's own presentation
   attributes (stroke-width/opacity) without needing !important. */
@keyframes route-flash-blink {
  0%,
  100% {
    stroke-opacity: 1;
  }
  50% {
    stroke-opacity: 0.25;
  }
}

.route-flash {
  stroke-width: 6;
  animation: route-flash-blink 1.6s ease-in-out infinite;
}

.nearby-route-popup {
  font-size: 0.82rem;
  max-width: 240px;
}

.nearby-route-popup-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}

.nearby-route-popup-swatch {
  flex-shrink: 0;
  width: 11px;
  height: 11px;
  border-radius: 3px;
}

.nearby-route-popup-line {
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--text);
  line-height: 1.1;
}

.nearby-route-popup-operator {
  color: var(--muted);
  font-size: 0.76rem;
  margin-bottom: 6px;
}

.nearby-route-popup-journey {
  color: var(--text);
  font-size: 0.78rem;
  line-height: 1.35;
}

.nearby-route-popup-meta {
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid rgba(148, 163, 184, 0.12);
  color: var(--muted);
  font-size: 0.72rem;
}

/* One stop dot's own hover/tap popup (routeStopDotsLayer.js) -- shares the
   same dark-card chrome every other popup in this app already gets from
   .leaflet-popup-content-wrapper, styled to match .nearby-route-popup's
   own convention (swatch + line/operator, small secondary meta line). */
.route-stop-popup {
  font-size: 0.82rem;
  max-width: 220px;
}

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

.route-stop-popup-line {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: 0.78rem;
}

.route-stop-popup-swatch {
  flex-shrink: 0;
  width: 10px;
  height: 10px;
  border-radius: 3px;
}

.route-stop-popup-atco {
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid rgba(148, 163, 184, 0.12);
  color: var(--muted);
  font-size: 0.68rem;
  font-family: ui-monospace, "SF Mono", Consolas, monospace;
  letter-spacing: 0.02em;
}

.attribution {
  margin-top: 20px;
  color: var(--muted);
  font-size: 0.68rem;
  line-height: 1.4;
  opacity: 0.7;
}

/* ---------- Map ---------- */

.map-area {
  position: relative;
  height: 100%;
  width: 100%;
}

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

.locate-control-btn {
  display: flex;
  align-items: center;
  justify-content: center;
}

.basemap-control {
  position: relative;
}

.basemap-control-btn {
  display: flex;
  align-items: center;
  justify-content: center;
}

.basemap-control-menu {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 4px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
  overflow: hidden;
  min-width: 180px;
  z-index: 1000;
}

.basemap-control-menu.open {
  display: block;
}

.basemap-control-item {
  display: block;
  width: 100%;
  padding: 8px 12px;
  border: none;
  background: none;
  text-align: left;
  font-family: inherit;
  font-size: 0.82rem;
  color: #1f2937;
  cursor: pointer;
}

.basemap-control-item:hover {
  background: #f1f5f9;
}

.basemap-control-item.active {
  font-weight: 600;
  color: var(--brand);
  background: #eff6ff;
}

.leaflet-container {
  background: #dfeaf5;
  font-family: "Inter", sans-serif;
}

.panel-toggle {
  display: none;
}

/* Rotated arrow marker — see static/js/map/busMarkers.js. The wrapping div
   carries the rotation transform; Leaflet's own marker transform (marker
   positioning) is applied to the outer icon element, so rotating only this
   inner element avoids fighting Leaflet's own translate3d. */
.bus-marker-wrapper {
  background: transparent;
  border: none;
}

.bus-marker {
  width: 26px;
  height: 26px;
  transition: transform 0.3s ease;
}

/* ---------- Vehicle tooltip / popup ---------- */

.leaflet-popup-content-wrapper {
  background: rgba(15, 23, 42, 0.97);
  border: 1px solid rgba(148, 163, 184, 0.25);
  border-radius: 12px;
  color: var(--text);
  box-shadow: var(--shadow);
}

.leaflet-popup-tip {
  background: rgba(15, 23, 42, 0.97);
}

.vehicle-popup h3 {
  margin: 0 0 6px;
  font-size: 0.95rem;
  color: var(--brand);
}

.vehicle-popup dl {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 3px 10px;
  font-size: 0.82rem;
}

.vehicle-popup dt {
  color: var(--muted);
}

.vehicle-popup dd {
  margin: 0;
}

.search-location-popup {
  font-size: 0.82rem;
  max-width: 220px;
}

.route-stop-marker-popup {
  font-size: 0.82rem;
  max-width: 220px;
}

.vehicle-popup-note {
  margin: 8px 0 0;
  padding-top: 8px;
  border-top: 1px solid rgba(148, 163, 184, 0.12);
  color: var(--muted);
  font-size: 0.74rem;
  line-height: 1.4;
}

.show-route-btn {
  display: block;
  width: 100%;
  margin: 8px 0 0;
  padding: 7px 10px;
  border-radius: 8px;
  border: 1px solid var(--brand);
  background: transparent;
  color: var(--brand);
  font-family: inherit;
  font-weight: 600;
  font-size: 0.78rem;
  cursor: pointer;
}

.show-route-btn:hover {
  background: rgba(14, 165, 233, 0.12);
}

/* ---------- "You are here" marker / popup ---------- */

.user-location-popup {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.82rem;
  color: var(--text);
  white-space: nowrap;
}

.remove-location-btn {
  padding: 4px 9px;
  border-radius: 7px;
  border: 1px solid rgba(148, 163, 184, 0.35);
  background: transparent;
  color: var(--muted);
  font-family: inherit;
  font-weight: 600;
  font-size: 0.7rem;
  cursor: pointer;
}

.remove-location-btn:hover {
  border-color: var(--danger);
  color: var(--danger);
}

/* ---------- Bus stop markers / popup ---------- */

.stop-marker-wrapper {
  background: transparent;
  border: none;
}

.stop-popup {
  min-width: 220px;
  width: 100%;
  display: flex;
  flex-direction: column;
  /* This popup no longer auto-pans the map to fit itself on screen (see
     stopMarkers.js's own comment on why) -- capped here instead, with only
     .stop-popup-scroll (not the sticky header) actually scrolling, so tall
     content (a long departures list, an expanded "stops down the route")
     is always reachable rather than potentially rendering above the
     viewport's own top edge with nothing able to bring it into view.
     min() keeps this sane on both a small phone screen and a large
     desktop one. */
  max-height: min(60vh, 420px);
}

.stop-popup-sticky {
  flex-shrink: 0;
}

.stop-popup-scroll {
  overflow-y: auto;
  /* flex:1 alone isn't enough -- a flex child's default min-height is
     "auto" (its content's natural height), which would just grow the
     whole popup past max-height instead of actually scrolling. */
  flex: 1 1 auto;
  min-height: 0;
  /* Leaves room so a scrollbar doesn't sit flush against row content. */
  padding-right: 2px;
}

.stop-popup-header {
  display: flex;
  align-items: baseline;
  gap: 7px;
}

.stop-popup h3 {
  margin: 0;
  font-size: 0.92rem;
  color: var(--text);
}

.stop-popup-indicator {
  flex-shrink: 0;
  padding: 1px 8px;
  border-radius: 999px;
  background: rgba(148, 163, 184, 0.15);
  border: 1px solid var(--line);
  color: #cbd5e1;
  font-size: 0.62rem;
  font-weight: 700;
}

.stop-popup-meta {
  font-size: 0.78rem;
  color: var(--muted);
  margin-bottom: 8px;
}

.stop-popup-favourite-btn {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(148, 163, 184, 0.12);
  color: var(--muted);
  cursor: pointer;
}

.stop-popup-favourite-btn svg {
  display: block;
}

.stop-popup-favourite-btn .star-path {
  transition: fill 0.15s ease;
}

.stop-popup-favourite-btn:hover {
  border-color: #f59e0b;
  color: #f59e0b;
}

.stop-popup-favourite-btn.active {
  border-color: #f59e0b;
  color: #f59e0b;
}

.stop-popup-favourite-btn.active .star-path {
  fill: currentColor;
}

.stop-popup-refreshed {
  font-size: 0.7rem;
  color: var(--muted);
  margin: -4px 0 8px;
}

.stop-popup-refresh-btn {
  flex-shrink: 0;
  margin-left: auto;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(148, 163, 184, 0.12);
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.stop-popup-refresh-btn:hover {
  border-color: var(--brand);
  color: var(--text);
}

.stop-popup-refresh-btn:disabled {
  cursor: not-allowed;
  opacity: 0.7;
}

/* Spins the refresh icon in place while a forced re-fetch is in flight --
   deliberately NOT a content/size change to the popup itself (see
   main.js's handleStopPopupOpen doc comment on why that closes the popup
   out from under the cursor). */
@keyframes stop-popup-refresh-spin {
  to {
    transform: rotate(360deg);
  }
}

.stop-popup-refresh-btn-spinning {
  animation: stop-popup-refresh-spin 0.8s linear infinite;
}

.stop-popup-status {
  font-size: 0.82rem;
  color: var(--muted);
  padding: 6px 0 2px;
}

.stop-popup-status-error {
  color: var(--danger);
}

.timetable-rows {
  display: flex;
  flex-direction: column;
}

.tt-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid rgba(148, 163, 184, 0.12);
}

.tt-row:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.tt-line-badge {
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  background: rgba(14, 165, 233, 0.16);
  color: var(--brand);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 0.78rem;
}

.tt-info {
  flex-grow: 1;
  min-width: 0;
}

.tt-dest {
  color: var(--text);
  font-weight: 600;
  font-size: 0.82rem;
  /* Wraps up to 2 lines instead of cutting off mid-word -- confirmed live
     as unreadable at a single-line ellipsis truncation, even at this
     popup's wider size (see stopMarkers.js's own maxWidth). */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.tt-operator-row {
  display: flex;
  align-items: center;
  gap: 4px;
  min-height: 18px;
}

.tt-operator {
  flex-grow: 1;
  min-width: 0;
  color: var(--muted);
  font-size: 0.68rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tt-time-block {
  flex-shrink: 0;
  text-align: right;
}

.tt-time {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: 0.82rem;
  color: var(--text);
}

.tt-countdown-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  margin-top: 1px;
}

.tt-countdown {
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--muted);
}

.tt-countdown-live {
  color: var(--brand-2);
  font-weight: 700;
}

.live-badge {
  display: inline-block;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--brand-2);
  color: #04240f;
  font-size: 0.55rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  vertical-align: middle;
}

/* A small icon, not a repeated "Show stops down the route" sentence on
   every row (confirmed live as far too busy at 8 rows) -- rotates in place
   to indicate open/closed rather than needing room for a text label. */
.tt-show-stops-btn {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: rgba(14, 165, 233, 0.15);
  color: var(--brand);
  font-size: 0.62rem;
  line-height: 16px;
  cursor: pointer;
  transition: transform 0.15s ease;
}

.tt-show-stops-btn:hover {
  background: rgba(14, 165, 233, 0.3);
}

.tt-show-stops-btn:disabled {
  cursor: default;
}

.tt-show-stops-btn-open {
  transform: rotate(180deg);
}

.tt-show-stops-btn-spinning {
  animation: stop-popup-refresh-spin 0.8s linear infinite;
}

/* Hidden until you're actually looking at this row (or it's already
   expanded) -- on a hover-capable device only; touch has no hover state to
   reveal it with, so it stays visible there. Confirmed live as wanted:
   with one per row, having all of them visible at once was a big part of
   what made the popup feel busy. */
@media (hover: hover) {
  .tt-show-stops-btn {
    opacity: 0;
  }

  .tt-row:hover .tt-show-stops-btn,
  .tt-show-stops-btn:focus-visible,
  .tt-show-stops-btn-open,
  .tt-show-stops-btn-spinning {
    opacity: 1;
  }
}

/* One departure's own further stops -- a simple vertical timeline (a line
   down the left, a dot per stop) rather than another table, since it's a
   single-column "place, then time" list read top to bottom, the same
   pattern transit apps use for "next stops on this journey". */
.tt-stops-timeline {
  /* No max-height/overflow of its own -- nested scroll areas (this one
     inside .stop-popup-scroll, which now handles the whole popup's own
     height cap) make for an awkward, easy-to-get-stuck-in-the-wrong-one
     scroll experience, especially on a touchscreen. This one just adds to
     the outer popup's own scrollable height instead. */
  position: relative;
  margin: 8px 0 2px 4px;
  padding-left: 14px;
  border-left: 2px solid rgba(148, 163, 184, 0.2);
}

.tt-stop-row {
  position: relative;
  display: flex;
  /* flex-start, not baseline -- a wrapped 2-line name (see
     .tt-stop-row-name) would otherwise align to its LAST line's baseline,
     leaving the time looking oddly low against a name's first line. */
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  padding: 3px 0;
  font-size: 0.72rem;
}

.tt-stop-row::before {
  content: "";
  position: absolute;
  left: -18px;
  top: 50%;
  transform: translateY(-50%);
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--muted);
}

.tt-stop-row-current {
  font-weight: 700;
}

.tt-stop-row-current::before {
  background: var(--brand);
  box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.25);
}

.tt-stop-row-name {
  /* Same fix as .tt-dest above, same reason: single-line ellipsis was
     confirmed live as unreadable (stop names here are frequently longer
     than the main departure board's destination names, and this list sits
     in a narrower, further-indented column), not just cosmetically
     truncated. Wraps up to 2 lines instead of cutting mid-word. */
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.tt-stop-row-times {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.tt-stop-row-time {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.tt-stop-row-time-live {
  color: var(--brand-2);
  font-weight: 700;
}

/* The timetabled time at a stop further down the route, adjusted by
   whatever delay was observed at the originally-clicked stop (see
   stopInfo.js's own renderTripStopsList doc comment) -- distinct styling
   (amber, smaller) so it reads as an ESTIMATE, never confused with the
   plain timetabled time directly above it or a real live prediction
   (.tt-stop-row-time-live). */
.tt-stop-row-estimate {
  font-variant-numeric: tabular-nums;
  font-size: 0.65rem;
  color: #f59e0b;
}

.tt-stops-status {
  margin-top: 6px;
  font-size: 0.7rem;
  color: var(--muted);
}

/* ---------- First-load splash ---------- */

.splash-overlay {
  position: fixed;
  inset: 0;
  z-index: 1600;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(2, 8, 23, 0.72);
  backdrop-filter: blur(4px);
  padding: 20px;
}

.splash-overlay[hidden] {
  display: none;
}

.splash-card {
  position: relative;
  width: 100%;
  max-width: 420px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 28px 24px 24px;
  box-shadow: var(--shadow);
}

.splash-close {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

/* Sits just left of .splash-close -- reuses .favourites-control-btn/
   -menu's own sizing/dropdown look (see the main panel header's identical
   favourites control) for the button and menu themselves, just its own
   position since .favourites-control's own top:0;right:0 would otherwise
   land in the exact same corner as the close button. */
.splash-favourites-control {
  position: absolute;
  top: 12px;
  right: 50px;
}

.splash-close:hover {
  color: var(--text);
  border-color: var(--brand);
}

.splash-card h2 {
  margin: 6px 0 4px;
  font-size: 1.35rem;
}

.splash-intro {
  margin: 0 0 18px;
  color: var(--muted);
  font-size: 0.88rem;
}

/* Marks which splash option(s) belong to which tool (View Live Services'
   four buttons vs. the mini Find Routes form below them) -- confirmed
   live as wanted: green (the same accent .eyebrow already uses) to stand
   out clearly against the rest of the splash's muted grey text, not just
   another grey label easy to skim past. */
.splash-section-label {
  margin: 0 0 10px;
  color: var(--brand-2);
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* #splashAddressForm now sits in the DOM between .splash-options and this
   label (moved there so revealing it pushes "Find Routes" down instead of
   leaving a gap where it used to sit, further down the card) -- this
   selector matches the label by its real DOM-adjacent sibling (the form
   itself), not .splash-options directly, so the gap is correct whether
   the form is hidden or visible: a hidden element still counts as the
   DOM-adjacent sibling for CSS combinator purposes, even though it
   contributes no box of its own. */
.splash-address-form + .splash-section-label {
  margin-top: 24px;
}

.splash-options {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.splash-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 16px 10px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(148, 163, 184, 0.08);
  color: var(--text);
  font-family: inherit;
  font-size: 0.82rem;
  text-align: center;
  cursor: pointer;
}

.splash-option:hover {
  border-color: var(--brand);
  color: var(--brand);
}

.splash-address-form {
  margin-top: 14px;
}

/* No separate "Go" button -- Enter submits the form (matches this app's
   own convention, see #find-from-input's identical Enter-to-pin
   behaviour), so the input alone can span the full row width and its
   edges line up with .splash-options' grid edges either side, rather than
   losing width to an adjacent button. */
#splashAddressInput {
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
}

#splashAddressInput:focus {
  outline: 2px solid var(--brand);
}

/* Reuses .find-input-row/.find-locate-btn's own layout (both generic, not
   scoped to the main panel's specific input ids) so this row matches the
   real Find Routes form's own From/To rows exactly -- just the two text
   inputs below need their own styling, matching #splashAddressInput's. */
.splash-find-route-form {
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

#splashFindFromInput,
#splashFindToInput {
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(15, 23, 42, 0.8);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
}

#splashFindFromInput:focus,
#splashFindToInput:focus {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

.splash-dont-show-again {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
  color: var(--muted);
  font-size: 0.82rem;
  cursor: pointer;
}

.splash-dont-show-again input[type="checkbox"] {
  accent-color: var(--brand);
  cursor: pointer;
}

/* ---------- Responsive (mobile) ---------- */

@media (max-width: 768px) {
  .app-shell {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    position: relative;
    height: 100%;
  }

  .panel-toggle {
    display: flex;
    position: fixed;
    /* env() falls back to 0 on non-notched devices, so this is just 20px
       there -- only pulls in further on devices with a home indicator/
       safe-area inset now that viewport-fit=cover (see index.html's own
       viewport meta) lets this fixed element sit at the TRUE screen edge
       instead of iOS's old implicit safe-area-only viewport. */
    bottom: calc(20px + env(safe-area-inset-bottom));
    right: calc(20px + env(safe-area-inset-right));
    width: 56px;
    height: 56px;
    background: linear-gradient(135deg, var(--brand), #4f46e5);
    border: none;
    border-radius: 50%;
    color: white;
    font-size: 24px;
    cursor: pointer;
    z-index: 1500;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 20px rgba(14, 165, 233, 0.4);
  }

  /* Leaflet's own zoom/locate (top-left) and basemap/layers (top-right)
     controls default to Leaflet's own `top: 0`, which now sits under the
     iOS status bar (clock/battery) now that viewport-fit=cover (see
     index.html's own viewport meta) lets the map extend into that area --
     same root cause as .panel-toggle/.panel's own env() rules above, just
     for Leaflet's control container rather than this app's own elements.
     env() falls back to 0 on non-notched devices, so this is a no-op
     there. */
  .leaflet-top {
    top: env(safe-area-inset-top);
  }

  .panel {
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    /* 100vh on mobile Safari is the LARGEST possible viewport, as if its
       own address bar/bottom toolbar were already hidden -- not the
       actually-visible area, which is shorter whenever that chrome is on
       screen (confirmed live as the real cause of the panel's own bottom
       content, e.g. "Clear searches" with nothing selected, being covered
       by the browser's own UI and needing a scroll to reach, despite
       .panel's overflow-y:auto already making the rest of it scrollable).
       100dvh is the actual dynamic/current visible viewport -- declared
       SECOND (not replacing 100vh) so it only overrides in browsers that
       understand the unit; a browser that doesn't recognise `dvh` just
       ignores that whole declaration and keeps the 100vh fallback above,
       same progressive-enhancement shape as this app's own env()-based
       safe-area rules. */
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    /* Same env() reasoning as .panel-toggle above -- keeps the header/
       content clear of the status bar/notch and home indicator now that
       this fixed panel can extend the full physical screen. */
    padding: calc(20px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right)) calc(20px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
    border-right: none;
    z-index: 1400;
    transform: translateX(-100%);
    transition: transform 0.3s ease;
    background: rgba(15, 23, 42, 0.97);
  }

  .panel.active {
    transform: translateX(0);
  }

  .panel-header h1 {
    font-size: 1.5rem;
  }

  .app-icon {
    width: 48px;
    height: 48px;
  }

  .map-area,
  #map {
    height: 100%;
    width: 100%;
  }

  /* Mobile Safari/Chrome auto-zoom the whole page in on focusing any
     input/select/textarea whose own font-size is under 16px (to make the
     text legible), and nothing resets that zoom on blur -- confirmed live
     as the cause of "the panel toggle button/zoom controls are cut off
     after tapping a county": those are position:fixed, so once the browser
     auto-zooms, they end up partly outside the now-shrunk visual viewport.
     Every real text/number/date/time input on the page needs to be listed
     here (missing #splashFindFromInput/#splashFindToInput here, added
     since this comment was first written, was confirmed live as a real
     recurrence of the exact same bug) -- bumped to 16px here, mobile-only,
     so desktop's own denser look is unaffected. */
  .checkbox-dropdown-filter,
  #find-from-input,
  #find-to-input,
  #nearby-radius-input,
  #splashAddressInput,
  #splashFindFromInput,
  #splashFindToInput,
  #journeyNameInput,
  .journey-leaving-row input {
    font-size: 16px;
  }

  .splash-card {
    max-width: 92vw;
    padding: 22px 18px 18px;
  }
}
