/* Shared styling for every visualisation on this site.
 *
 * Deliberately small and dependency-free. The pages are served from a local
 * directory and read by one person at a time; a build step and a framework
 * would cost more than they return.
 *
 * Light by default, dark when the reader's system asks. Colours live as custom
 * properties so a visualisation can lean on them without redefining the palette
 * - and so the marker states in the map read against either background.
 */

:root {
  color-scheme: light dark;

  --bg: #fbfbfa;
  --panel: #ffffff;
  --ink: #16191d;
  --ink-soft: #5b6470;
  /* Was #8b96a3, which is 3.0:1 on the panel - it carries the control labels
   * and the CRS codes, both small text, so it has to clear 4.5:1 rather than
   * the 3:1 that would do for a graphic. Measured at 4.72:1. */
  --ink-faint: #6b7480;
  --line: #dfe3e8;
  /* Focus is drawn in the marker blue rather than the browser default, which is
   * invisible against the darker controls in the dark theme. */
  --focus: #1d4ed8;

  /* Station marker states. Distinguishable by lightness as well as hue, so
   * they survive a colour-blind reader and a bad screen. */
  /* Read against OpenStreetMap's own palette, not against white: the standard
   * tiles are pale greens, buffs and blue water, so a light grey dot vanishes
   * into them entirely. `outside` and `unreachable` are darker than they would
   * need to be on a plain background for exactly that reason. */
  --origin: #c2410c;
  --within: #1d4ed8;
  --outside: #64748b;
  --unreachable: #94a3b8;

  --shadow: 0 1px 2px rgb(16 25 37 / 0.06), 0 4px 14px rgb(16 25 37 / 0.06);
  --radius: 10px;
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171b;
    --panel: #1c2026;
    --ink: #e8ecf1;
    --ink-soft: #a3aebc;
    /* Was #6f7c8b - 3.4:1 on the dark panel. Measured at 4.85:1. */
    --ink-faint: #8b98a8;
    --line: #2b323b;
    --focus: #7aa5ff;

    --origin: #fb923c;
    --within: #60a5fa;
    --outside: #46525f;
    --unreachable: #333c46;

    --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 4px 14px rgb(0 0 0 / 0.35);
  }
}

* { box-sizing: border-box; }

/* --- reaching things without a mouse -------------------------------------- */

/* One visible focus ring for everything, because the default is a hairline the
 * dark theme swallows. `:focus-visible` rather than `:focus` so a mouse click
 * does not leave a ring behind, but keyboard and screen-reader focus always
 * shows. The offset keeps it clear of the control's own border. */
:where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Available to screen readers, absent from the page. Used for headings that
 * name a region, and for the table caption the sighted reader gets from the
 * summary line above it. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* The first thing in the tab order. The map holds three thousand markers that a
 * keyboard cannot reach at all, so the link that matters goes to the list,
 * which carries the same answers as text. */
.skip-link {
  position: absolute; z-index: 1000; left: 0.5rem; top: -3rem;
  padding: 0.5rem 0.75rem; background: var(--panel); color: var(--ink);
  border: 1px solid var(--line); border-radius: 6px; box-shadow: var(--shadow);
  transition: top 120ms ease;
}
.skip-link:focus { top: 0.5rem; }

/* Honour the system setting. Nothing here is essential motion - the card lift
 * is decoration, and Leaflet's tile fade is already off for an unrelated
 * reason (it raced with reframing and left tiles at opacity 0.01). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

body {
  margin: 0;
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
}

a { color: inherit; }

h1, h2, h3 { line-height: 1.25; font-weight: 600; margin: 0; }
h1 { font-size: 1.6rem; letter-spacing: -0.01em; }
h2 { font-size: 1.05rem; }

code, .mono { font-family: var(--mono); font-size: 0.85em; }

/* --- landing page --------------------------------------------------------- */

.landing { max-width: 60rem; margin: 0 auto; padding: 3rem 1.5rem 4rem; }
.landing header { margin-bottom: 2rem; }
/* The strapline under a page's title. The landing page dropped its own, so
 * today this is the map's - hence a size that suits the 22rem control panel
 * rather than a full-width page, and a `max-width` that only bites if one is
 * ever used across a wide layout again. */
.lede { color: var(--ink-soft); max-width: 42rem; margin: 0.4rem 0 0;
        font-size: 0.9rem; line-height: 1.4; }

.landing main { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); }

.card {
  display: block;
  padding: 1.25rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-decoration: none;
  box-shadow: var(--shadow);
  transition: transform 120ms ease, border-color 120ms ease;
}
.card:hover { transform: translateY(-2px); border-color: var(--ink-faint); }
.card p { color: var(--ink-soft); margin: 0.5rem 0 0.75rem; }
.card code { color: var(--ink-faint); }

/* A card that leaves the site should say so before it is clicked, not after.
   The arrow is decorative, so it hangs off the heading rather than sitting in
   the markup where a screen reader would read it as part of the title. */
.card--external h2::after {
  content: " \2197";
  color: var(--ink-faint);
  font-weight: normal;
}

/* --- attribution, which is a licence condition rather than decoration ------ */

footer { margin-top: 3rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
.disclaimer { color: var(--ink-soft); font-size: 0.85rem; max-width: 46rem; }
.credits { list-style: none; padding: 0; margin: 0.75rem 0 0;
           font-size: 0.8rem; color: var(--ink-faint); }
.credits li { margin-bottom: 0.15rem; }
.credits a { color: var(--ink-soft); }

/* --- map layout ----------------------------------------------------------- */

.map-page {
  display: grid;
  grid-template-columns: 22rem minmax(0, 1fr);
  height: 100dvh;
}

.controls {
  /* **`position` is load-bearing, not decoration.** `overflow` only clips an
   * absolutely-positioned descendant when the scrolling ancestor is *itself*
   * positioned - otherwise the descendant's containing block is further up the
   * tree and it escapes the clip entirely.
   *
   * Without this the visually-hidden `<caption>` on the matches table - which
   * is `position: absolute` - sat at its static position deep inside the
   * table's unclipped flow and stretched the document to its own bottom edge.
   * `<html>` became 1044px against a 273px viewport, so the whole page
   * scrolled and carried the map off the top of the window, while every
   * element you could see was still inside the viewport. Nothing looked wrong
   * in the layout; the page was 1px of hidden text taller than the screen. */
  position: relative;
  overflow-y: auto;
  padding: 1.25rem;
  background: var(--panel);
  border-right: 1px solid var(--line);
}
.controls > * + * { margin-top: 1.1rem; }

#map { height: 100%; background: var(--bg); }

/* **Leaflet's zoom transition is 0.25s, and the markers are hidden for exactly
 * that long** - see the `leaflet-zoom-hide` note in map.js for why they are
 * hidden at all. A quarter of a second of absence is long enough to read as a
 * flash rather than as a redraw, so the transition is halved.
 *
 * This is a mitigation, not a fix, and there is no fix: a canvas whose markers
 * are sized in fixed pixels per zoom band cannot be transform-scaled truthfully
 * for the duration of a zoom. Something has to give - the dots scale wrongly
 * and snap, or the tiles vanish, or the dots blink. Shortening the window is
 * the only move that improves all three at once.
 *
 * Must come after leaflet.css, which it does: the page links Leaflet first. */
.leaflet-zoom-anim .leaflet-zoom-animated {
  transition: transform 0.12s cubic-bezier(0, 0, 0.25, 1);
}

/* The zoom readout - see `zoomReadout` in map.js for why it exists and why it
 * sits bottom-left. Quiet enough to ignore and legible enough to quote in a
 * bug report, which is the whole job. */
.zoom-readout {
  padding: 0.1rem 0.4rem;
  font-family: var(--mono); font-size: 0.7rem;
  color: var(--ink-faint);
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  border: 1px solid var(--line); border-radius: 4px;
}

/* Narrow screens: controls above, map below.
 *
 * **This block has to come after the rules it overrides.** `#map` and the
 * mobile `#map` are the same specificity, so with the media query written
 * first the base `height: 100%` won - and 100% of a grid row with no definite
 * height is nought, which is a map that loads, sizes itself to zero, and shows
 * a white rectangle. Leaflet dutifully reported a viewport of 375x0 and a zoom
 * of 18. */
@media (max-width: 50rem) {
  .map-page {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0, 1fr);
  }
  .controls { max-height: 45dvh; border-right: 0; border-bottom: 1px solid var(--line); }
  #map { height: auto; min-height: 55dvh; }
}

.field label { display: block; font-size: 0.78rem; text-transform: uppercase;
               letter-spacing: 0.04em; color: var(--ink-faint); margin-bottom: 0.3rem; }
/* Input plus an affordance beside it, sharing the row. `min-width: 0` because
 * a grid item will not shrink below its content and a search input's default
 * intrinsic width would push the button off the end. */
.with-button { display: grid; grid-template-columns: 1fr auto; gap: 0.4rem; }
.with-button input { min-width: 0; }
.with-button button {
  display: grid; place-items: center;
  padding: 0 0.6rem; font: inherit; line-height: 1; cursor: pointer;
  color: var(--ink-soft); background: var(--bg);
  border: 1px solid var(--line); border-radius: 6px;
}
.with-button button:hover:not(:disabled) { color: var(--ink); border-color: var(--ink-faint); }
.with-button button:disabled { opacity: 0.5; cursor: progress; }
#locate-status:empty { display: none; }
#locate-status { margin: 0.35rem 0 0; }

.field select, .field input[type="text"], .field input[type="search"] {
  width: 100%; padding: 0.45rem 0.55rem; font: inherit; color: inherit;
  background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
}
/* **Room for the clear button, which the browser draws inside the field.**
 * WebKit lays `::-webkit-search-cancel-button` out after the text and inside
 * the padding box, so with 0.55rem to play with it sat flush against the right
 * border and came out visibly clipped by it - half an `x`. The extra padding
 * is on the right only, and only on search inputs, so nothing else moves. */
.field input[type="search"] { padding-right: 1.35rem; }
/* **The two sliders are the main controls on the page, so they are drawn
 * rather than left to the browser.** `accent-color` alone recoloured the
 * native widget and left a pale, small thumb that the panel swallows - and a
 * grab handle you have to look for is the wrong thing to be subtle about.
 *
 * Styling a thumb at all means `appearance: none`, and that takes the native
 * track *and its filled portion* with it. The fill is worth keeping - it is
 * what makes the position readable at a glance - so `--fill` is written from
 * the same function that writes the readouts, and the track is a two-stop
 * gradient with both stops at the same place, which is how you get a hard
 * edge out of `linear-gradient`.
 *
 * Each rule is written twice because WebKit and Gecko name every one of these
 * pseudo-elements differently, and **neither tolerates the other's selector in
 * a shared rule** - one unrecognised selector drops the whole declaration
 * block, so grouping them silently styles nothing anywhere. Firefox has a real
 * `::-moz-range-progress`, so it does not need the gradient. */
.field input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 1.5rem; background: none; cursor: pointer;
}

.field input[type="range"]::-webkit-slider-runnable-track {
  box-sizing: border-box; height: 8px; border-radius: 999px;
  border: 1px solid var(--line);
  background: linear-gradient(to right,
    var(--within) var(--fill, 100%), var(--bg) var(--fill, 100%));
}
.field input[type="range"]::-moz-range-track {
  box-sizing: border-box; height: 8px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg);
}
.field input[type="range"]::-moz-range-progress {
  height: 8px; border-radius: 999px; background: var(--within);
}

/* The ring is the panel colour rather than white so the thumb reads as a disc
 * lifted off the track in both themes. `margin-top` centres it on the track,
 * which WebKit does not do for you: (track - thumb) / 2. */
.field input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  box-sizing: border-box; width: 18px; height: 18px; border-radius: 50%;
  background: var(--within); border: 2px solid var(--panel);
  box-shadow: 0 1px 3px rgb(16 25 37 / 0.4);
  margin-top: -5px;
}
.field input[type="range"]::-moz-range-thumb {
  box-sizing: border-box; width: 18px; height: 18px; border-radius: 50%;
  background: var(--within); border: 2px solid var(--panel);
  box-shadow: 0 1px 3px rgb(16 25 37 / 0.4);
}

/* **The minimum's slider fills the other way.** `.lower` is the lower *bound*,
 * not the lower row - the maximum is drawn first, so this one sits underneath,
 * and the class would be a lie if it meant position.
 *
 * On a maximum the coloured part of the track is what is included, running
 * from the left up to the thumb. On a minimum the same drawing would colour
 * everything the filter *excludes* - the exact opposite claim, made in the
 * most legible way available. So it runs from the thumb rightwards, and the
 * two coloured bars overlap on precisely what is on the map.
 *
 * Firefox draws `::-moz-range-progress` over the track from the left, so it is
 * made transparent here to let the reversed gradient show through. */
.field input[type="range"].lower::-webkit-slider-runnable-track {
  background: linear-gradient(to right,
    var(--bg) var(--fill, 0%), var(--within) var(--fill, 0%));
}
.field input[type="range"].lower::-moz-range-track {
  background: linear-gradient(to right,
    var(--bg) var(--fill, 0%), var(--within) var(--fill, 0%));
}
.field input[type="range"].lower::-moz-range-progress { background: transparent; }

.field input[type="range"]:hover::-webkit-slider-thumb,
.field input[type="range"]:active::-webkit-slider-thumb {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--within) 22%, transparent);
}
.field input[type="range"]:hover::-moz-range-thumb,
.field input[type="range"]:active::-moz-range-thumb {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--within) 22%, transparent);
}
.row { display: grid; gap: 0.6rem; grid-template-columns: 1fr 1fr; }

/* A pair of sliders under one heading, laid out label-then-track so the two
 * rows line up and the pair reads as one control rather than two. The label
 * column sizes to the longest of them; `1fr` gives the rest to the tracks.
 *
 * The legend carries the combined phrase - "£10.00 to £46.00" - which is also
 * what the summary sentence says, so the two cannot describe the same filter
 * two ways. It spans both columns. It is a `<p>` rather than a `<label>` only
 * because a `<label for>` can name one control and this names a group. */
.range-pair { display: grid; grid-template-columns: auto 1fr;
              align-items: center; column-gap: 0.6rem; row-gap: 0.1rem; }
.range-legend, .range-pair > .range-legend { grid-column: 1 / -1;
                display: block; font-size: 0.78rem; text-transform: uppercase;
                letter-spacing: 0.04em; color: var(--ink-faint);
                margin: 0 0 0.2rem; }
/* Inherits the small uppercase faint treatment from `.field label`; only the
 * bottom margin goes, since the grid is doing the spacing now. */
.range-pair > label { margin-bottom: 0; white-space: nowrap; }

/* The label wraps the box, so the words are part of the hit target rather than
 * a 13px square being the whole of it. `accent-color` is enough here - unlike
 * the sliders there is no thumb to hunt for, and the native tick is well drawn
 * in both themes.
 *
 * Everything else about the label is inherited from `.field label` above:
 * small, uppercase, faint. It reads as one of the controls rather than as a
 * stray sentence, which is what a bolder treatment would have made it. Only
 * the bottom margin is dropped, because the hint sits directly underneath. */
.field.check label { display: flex; align-items: center; gap: 0.45rem;
                     cursor: pointer; margin-bottom: 0; }
.field.check input { accent-color: var(--within); width: 1rem; height: 1rem;
                     margin: 0; cursor: pointer; }
.hint { margin: 0.3rem 0 0; font-size: 0.78rem; line-height: 1.4;
        color: var(--ink-faint); }

/* Quiet by default: the address bar already holds the link, and this is a
 * convenience rather than the main way to do anything. It gains its border on
 * hover so it still reads as a button when reached for. */
/* A column, so "See a random journey" is reliably *under* the copy button
 * rather than beside it whenever the panel happens to be wide enough. Two
 * actions that wrap differently at different widths read as one row that has
 * broken rather than as a list. */
.share { margin: 0.5rem 0 0; display: flex; flex-direction: column;
         align-items: flex-start; gap: 0.4rem; }
/* Empty most of the time, and an empty line still takes a line. */
.share .readout:empty { display: none; }
.share button {
  font: inherit; font-size: 0.78rem; color: var(--ink-soft);
  background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
  padding: 0.25rem 0.6rem; cursor: pointer;
}
.share button:hover { color: var(--ink); border-color: var(--ink-faint); }
.share .readout { font-size: 0.78rem; }

.readout { font-variant-numeric: tabular-nums; color: var(--ink-soft);
           font-size: 0.85rem; }
.readout b { color: var(--ink); font-weight: 600; }

/* **The summary sentence keeps a fixed depth**, because it sits above every
 * control and its length is not fixed: setting both limits takes it from two
 * lines to four, and each line it gained pushed the sliders down the panel
 * *while they were being dragged* - the one moment a control must stay still.
 *
 * Four lines, measured rather than guessed. At the 22rem panel the realistic
 * sentences span two lines to four; the worst case is the longest origin name
 * on the site - "Birkenhead Hamilton Square", 26 characters - with the day,
 * the departure time, both ranges, a change limit and rail only all set, and
 * that still lands on four. `line-height` here is 1.5, so four lines is 6em of
 * the element's own size and survives a change of font-size.
 *
 * **Re-measure this whenever the sentence gains a clause.** It has taken the
 * departure time and the change limit since it was set, and both were checked
 * against that worst case rather than assumed to fit - a fifth line does not
 * overflow, it grows the box, which puts the jitter straight back.
 *
 * The group note is deliberately outside this box. It is constant for a given
 * origin, so it never moves anything while a slider is being dragged, and
 * reserving for it as well would leave a hole under every ordinary station.
 *
 * One reservation for both layouts, deliberately. The narrow-screen layout
 * puts the controls across the full width, but a phone is about 375px and the
 * sentence has the same ~335px to wrap in - so four lines is right there too.
 * The only place it over-reserves is a stacked layout several hundred pixels
 * wide, where it costs a line of white space above the controls and buys the
 * same stillness. Not worth a breakpoint to chase. */
#summary-line { display: block; min-height: 6em; }
#summary-group { display: block; }

/* --- autocomplete --------------------------------------------------------- */

.combo { position: relative; }
.combo ul {
  position: absolute; z-index: 900; inset-inline: 0; top: 100%;
  margin: 0.2rem 0 0; padding: 0.25rem; list-style: none;
  max-height: 16rem; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 8px; box-shadow: var(--shadow);
}
.combo ul:empty, .combo ul[hidden] { display: none; }
.combo li { padding: 0.35rem 0.5rem; border-radius: 5px; cursor: pointer;
            display: flex; justify-content: space-between; gap: 0.5rem; }
.combo li[aria-selected="true"], .combo li:hover { background: var(--bg); }
.combo li .crs { color: var(--ink-faint); font-family: var(--mono); font-size: 0.8em; }

/* --- notes the reader has to see, not a footnote -------------------------- */

/* **The notes are `details.aside`, the same as the station list and the
 * SailRail panel** - the same control styled the same way, rather than a
 * second thing that opens and shuts and looks unlike the first.
 *
 * They used to be a grey box with a coloured edge. That was right when they
 * were paragraphs: a block of caveat needs to be told apart from the answer.
 * Once they became collapsible the box was competing with the disclosure
 * triangle to say the same thing, and two signals for one affordance is one
 * too many. Their prose carries `.readout` for the same reason - it is what
 * the SailRail panel's own explanation uses.
 *
 * The disclosure triangle is left to the browser: it is the only thing saying
 * the box opens, and each engine draws its own where its users expect it. */
details.aside > p:first-of-type { margin-top: 0.5rem; }

.legend { font-size: 0.8rem; color: var(--ink-soft); }
.legend li { list-style: none; display: flex; align-items: center; gap: 0.5rem; }
.legend ul { padding: 0; margin: 0.4rem 0 0; display: grid; gap: 0.25rem; }
.swatch { width: 0.7rem; height: 0.7rem; border-radius: 50%; flex: none; }

/* --- the panel for answers the map cannot draw ---------------------------- */

details.aside summary { cursor: pointer; font-size: 0.85rem; color: var(--ink-soft); }
details.aside table { width: 100%; border-collapse: collapse; margin-top: 0.5rem;
                      font-size: 0.8rem; }
/* **The gap above the table belongs outside the scroll box.** Inside it, the
 * header's natural position is 8px down the scrollport and its stuck position
 * is 0, so the first scroll of the list jerked it up by 8. Moving the margin to
 * the scroller makes the two the same and the header never moves at all. The
 * SailRail table has no scroller and keeps the rule above. */
.scroller { margin-top: 0.5rem; }
/* `details.aside` on the front, or the rule above it wins on specificity and
 * the table keeps its margin - which was exactly the 8px jump this removes. */
details.aside .scroller table { margin-top: 0; }
details.aside td { padding: 0.2rem 0; border-bottom: 1px solid var(--line); }
/* A gutter before every column after the first. Without it the journey time
 * and the fare butt together - "8h 08£290.80" on a long name, since both are
 * right-aligned and neither carried any horizontal padding. Set on `td` and
 * `th` alike so the headers stay over their own columns. */
details.aside td:not(:first-child),
details.aside th:not(:first-child) { padding-left: 0.9rem; }
details.aside td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
/* **A sliver of the row underneath used to show above the column names**, and
 * `border-collapse: collapse` is why: a collapsed border belongs to the table
 * rather than to the cell, so it paints outside the sticky cell's own
 * background, and fractional row heights on a 2x display land it in the same
 * device pixel as the header's top edge.
 *
 * A negative `top` covered it and **moved the header**, which is worse: pinning
 * at -2px from a natural 0 jerked the column names on the first scroll. The
 * shadow covers the same 2px without touching layout, so the header is at one
 * position from the first paint to the last. */
details.aside th {
  position: sticky; top: 0; z-index: 1; background: var(--panel);
  box-shadow: 0 -2px 0 var(--panel);
  text-align: left; font-weight: 600; font-size: 0.75rem; color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: 0.04em;
  padding: 0.2rem 0; border-bottom: 1px solid var(--line);
}
details.aside th:not(:first-child) { text-align: right; }

/* The station name is a button so the detail is reachable without a mouse -
 * see `.row-button`. Styled as text so the list still reads as a list. */
.row-button {
  font: inherit; color: inherit; background: none; border: 0; padding: 0;
  text-align: left; cursor: pointer; width: 100%;
}
.row-button:hover { text-decoration: underline; }
tr[aria-current="true"] td { background: color-mix(in srgb, var(--within) 10%, transparent); }

/* The detail a mouse user gets from a map popup, for everyone else. */
#station-detail:not(:empty) {
  margin-top: 0.6rem; padding: 0.6rem 0.7rem; font-size: 0.8rem;
  background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
}
#station-detail dl { display: grid; grid-template-columns: auto 1fr;
                     gap: 0.1rem 0.6rem; margin: 0.3rem 0 0; }
#station-detail dt { color: var(--ink-faint); }
#station-detail dd { margin: 0; font-variant-numeric: tabular-nums; }

/* Leaflet's own furniture, toned down to match. */
.leaflet-container { font: inherit; background: var(--bg); }
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
  background: var(--panel); color: var(--ink); box-shadow: var(--shadow);
}
.leaflet-popup-content { margin: 0.7rem 0.85rem; font-size: 0.85rem; }
.leaflet-popup-content h3 { font-size: 0.95rem; margin-bottom: 0.3rem; }
.leaflet-popup-content dl { display: grid; grid-template-columns: auto 1fr;
                            gap: 0.1rem 0.6rem; margin: 0; }
.leaflet-popup-content dt { color: var(--ink-faint); }
.leaflet-popup-content dd { margin: 0; font-variant-numeric: tabular-nums; }
.leaflet-control-attribution { background: var(--panel) !important; color: var(--ink-faint); }
.leaflet-control-attribution a { color: var(--ink-soft); }

/* The matches list: what is currently within both limits, in words.
 * A dot on a map is not readable by everyone, nor searchable, nor copyable. */
details.aside + details.aside { margin-top: 0.9rem; }
details.aside table td:first-child { width: 100%; }
details.aside td .crs { color: var(--ink-faint); margin-left: 0.3rem; }
/* No `padding-left` here: the gutter is set once above for every column after
 * the first. This rule sits later in the file at equal specificity, so a
 * padding of its own would quietly win and leave the two columns unevenly
 * spaced. */
details.aside td:nth-child(2) { text-align: right; white-space: nowrap;
                                color: var(--ink-soft); }
details.aside td:last-child { white-space: nowrap; }
/* **The scrolling lives on a wrapper, not on the table.** `display: block` on a
 * `<table>` is the usual way to make it scroll and it strips the element of its
 * table semantics entirely: rows and cells stop being rows and cells, the
 * column headers below stop being associated with anything, and a screen reader
 * reads "Penzance PNZ 8h 08 £290.80" as four loose strings. Checked in the
 * accessibility tree, where the cells came through as anonymous `generic`
 * nodes. */
/* The list scrolls inside its own box so the panel's other sections stay
 * reachable - 250 rows would otherwise make the panel 6,800px tall.
 *
 * **The height decides how tiny the scrollbar thumb is**, and 18rem made it a
 * 12px sliver: a thumb is the box squared over the content, and 288px of box
 * against 6,826px of rows leaves almost nothing to grab. `clamp` gives it the
 * viewport where there is one - 55vh is 28px of thumb and 16 visible rows on
 * an 800px window against 11 - while never dropping below the old 18rem on a
 * short one, or growing past 34rem where the list would crowd out the notes
 * below it.
 *
 * **Both axes are declared**, which changes no behaviour and states one.
 * Setting `overflow-y` alone does not leave the other axis alone: CSS computes
 * `visible` to `auto` when its companion is not visible, so this box has always
 * scrolled sideways too, and does below about 293px of panel - the width at
 * which the two nowrap numeric columns stop fitting. That is the right
 * behaviour, since the alternative is clipping the fare, but it was arrived at
 * by a spec rule rather than by anyone choosing it. */
.scroller { max-height: clamp(18rem, 55vh, 34rem); overflow: auto; }

/* **The map is in a mode, and the cursor is the only thing that says so while
 * the pointer is over it** - the status line lives up in the panel, out of
 * view of someone looking at Cornwall. Set on `body` rather than on `#map`
 * because Leaflet writes `cursor` onto `.leaflet-container` and its panes, so a
 * rule on the container alone loses to them; `!important` is how a mode beats a
 * library's own grab cursor without unpicking which pane owns it. */
body.picking-origin .leaflet-container,
body.picking-origin .leaflet-interactive { cursor: crosshair !important; }

/* **A hover preview must not offer to clear a choice nobody has made.** The
 * browser draws the `x` on a `type="search"` input the moment it has a value,
 * so previewing a station's name under the pointer put one there. It comes
 * back the instant an origin is actually committed - there the button means
 * what it says, and is how the pick-on-map mode is entered.
 *
 * WebKit and Blink are the engines that draw it; Firefox renders no such
 * button, so there is nothing to hide and the rule is simply inert. */
input.previewing::-webkit-search-cancel-button { display: none; }
