        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        /* Stop iOS Safari inflating text on this page (content.css already does this for
           the other pages). Without it the key renders noticeably larger on iPhone. */
        html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
            background: #e8e8e8;
            height: 100vh;
            overflow: hidden;
        }

        /* Pinching over the UI panels used to fall through to the BROWSER and zoom the whole
           page — the map only absorbs gestures over its own canvas. `pan-y` lets the legend
           body still scroll while refusing pinch-zoom on the chrome.
           Deliberately NOT using user-scalable=no / maximum-scale on the viewport: that
           would block people who zoom on purpose, which is a real part of this audience. */
        .search-panel,
        .nearest-stations,
        .legend-panel,
        .maplibregl-ctrl-group,
        .maplibregl-ctrl-attrib { touch-action: pan-y; }
        /* Bigger zoom / locate buttons — easier to hit, especially on a phone */
        .maplibregl-ctrl-group button { width: 36px; height: 36px; }
        .maplibregl-ctrl-group button .maplibregl-ctrl-icon { background-size: 22px 22px; }

        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }

        /* Search Panel */
        .search-panel {
            position: absolute;
            top: 20px;
            left: 20px;
            background: rgba(255, 255, 255, 0.98);
            padding: 10px;
            border-radius: 6px;
            box-shadow: 0 2px 12px rgba(11, 45, 39, 0.2);
            border: 3px solid #0b2d27;
            z-index: 1000;
            width: 360px;
        }

        .search-container {
            display: flex;
            gap: 8px;
            position: relative;   /* anchor for the type-ahead dropdown */
        }
        /* Type-ahead dropdown: line/service matches while you type. */
        .search-suggest {
            position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 1100;
            background: #fff; border-radius: 8px; overflow: hidden;
            box-shadow: 0 6px 20px rgba(11, 45, 39, 0.22); border: 1px solid #e2e6e4;
        }
        .search-suggest[hidden] { display: none; }
        .suggest-row {
            display: flex; align-items: center; gap: 10px; width: 100%;
            padding: 9px 12px; border: none; background: none; cursor: pointer;
            font-size: 14px; text-align: left; color: #0b2d27;
        }
        .suggest-row:hover, .suggest-row.active { background: #eef3f1; }
        .suggest-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .suggest-tag {
            flex: none; font-size: 10px; font-weight: 700; letter-spacing: 0.03em;
            text-transform: uppercase; color: #fff; border-radius: 4px; padding: 3px 7px;
        }
        .suggest-tag.is-service { background: #6950A1; }   /* colour overridden per service */
        .suggest-tag.is-track { background: #6b7a75; }
        .suggest-tag.is-station { background: #2f6fb0; }
        .suggest-tag.is-underground { background: #10069F; }   /* London Underground navy */
        .suggest-qual { flex: none; margin-left: auto; padding-left: 8px; font-size: 12px; color: #6b7a75; white-space: nowrap; }

        /* Empty message / info take no space — keeps the box compact until there's something to say */
        .message:empty,
        #infoPanel:empty {
            display: none;
        }

        #postcodeInput {
            flex: 1;
            /* A flex item will not shrink below its INTRINSIC width, and an input's intrinsic
               width is its `size` attribute: 20 characters, about 200px at 16px text. So the
               input refused to shrink and pushed the search button clean off the right edge
               (measured 53px past it) on a 240px viewport, which is what a 360px phone at
               150% page zoom gives you. Nothing else in the row can give, so this must. */
            min-width: 0;
            padding: 12px;
            border: 2px solid #0b2d27;
            border-radius: 4px;
            background: #ffffff;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            /* MUST stay >= 16px: iOS Safari auto-zooms the whole page when you focus an
               input smaller than that, which is the other half of the accidental-zoom
               problem. This was 14px. Don't lower it. */
            font-size: 16px;
            color: #0b2d27;
            text-transform: uppercase;
            font-weight: 500;
        }

        #postcodeInput::placeholder {
            color: #666;
            text-transform: none;
            font-weight: 400;
        }

        #postcodeInput:focus {
            outline: none;
            border-color: #0b2d27;
            box-shadow: 0 0 0 3px rgba(11, 45, 39, 0.1);
        }

        #searchBtn {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0 14px;
            background: #0b2d27;
            color: #ffffff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            flex-shrink: 0;
            transition: all 0.2s;
        }

        #searchBtn:hover {
            background: #164038;
        }

        #searchBtn:disabled {
            background: #999;
            cursor: not-allowed;
        }

        .message {
            padding: 10px;
            border-radius: 4px;
            font-size: 14px;
            margin-top: 10px;
            text-align: center;
        }

        .message.success {
            background: rgba(76, 175, 80, 0.1);
            color: #2e7d32;
            border: 1px solid rgba(76, 175, 80, 0.3);
        }

        .message.error {
            background: rgba(196, 30, 58, 0.1);
            color: #c41e3a;
            border: 1px solid rgba(196, 30, 58, 0.3);
        }

        /* Disambiguation list shown when a search matches several stations. */
        #infoPanel:not(:empty) { display: block; }
        .pick-head {
            font-size: 12.5px; font-weight: 600; color: #0b2d27;
            margin-bottom: 4px; padding-bottom: 6px; border-bottom: 1px solid #ddd;
        }
        .pick-item .pick-code {
            font-size: 11px; font-weight: 600; color: #666;
            background: #e8ecea; border-radius: 3px; padding: 1px 5px; margin-left: 6px;
        }
        .pick-item .station-distance { color: #0b2d27; font-weight: 600; }

        #infoPanel {
            display: none;
            margin-top: 15px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 4px;
            border: 1px solid #ddd;
        }

        .info-item {
            margin-bottom: 8px;
            font-size: 14px;
            color: #333;
        }

        .info-item:last-child {
            margin-bottom: 0;
        }


        .nearest-stations {
            display: none;
            position: absolute;
            top: 20px;
            right: 20px;
            min-width: 220px;
            padding: 15px;
            background: rgba(255, 255, 255, 0.98);
            border-radius: 6px;
            box-shadow: 0 2px 12px rgba(11, 45, 39, 0.2);
            border: 3px solid #0b2d27;
            z-index: 1000;
        }
        .nearest-stations h3 {
            font-size: 13px;
            color: #0b2d27;
            font-weight: 600;
        }
        .ns-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
        }
        .ns-toggle-btn {
            background: transparent;
            border: none;
            cursor: pointer;
            color: #0b2d27;
            padding: 2px;
            border-radius: 4px;
            display: flex;
            line-height: 0;
        }
        .ns-toggle-btn:hover { background: rgba(11, 45, 39, 0.1); }
        .ns-toggle-btn .icon-expand { display: none; }
        .ns-body { margin-top: 8px; }
        /* Minimising now HIDES the panel outright and moves its re-open control into the
           search row (#nearestRestoreBtn). Previously "collapsed" still left a full-width
           "Nearest Stations" bar under the search box, which on a phone cost a whole row
           of map for no information. */
        .nearest-stations.minimized { display: none !important; }

        .ns-restore-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0 12px;
            background: #ffffff;
            color: #0b2d27;
            border: 2px solid #0b2d27;
            border-radius: 4px;
            cursor: pointer;
            flex-shrink: 0;
            transition: background 0.2s;
        }
        .ns-restore-btn:hover { background: #eef4f2; }
        .ns-restore-btn[hidden] { display: none; }   /* beat display:flex above */

        .station-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 6px 0;
            border-bottom: 1px solid #eee;
            cursor: pointer;
            font-size: 13px;
        }

        .station-item:last-child {
            border-bottom: none;
        }

        .station-item:hover {
            color: #0b2d27;
            background: rgba(11, 45, 39, 0.05);
            margin: 0 -10px;
            padding: 6px 10px;
        }

        .station-name {
            color: #333;
        }

        .station-distance {
            color: #666;
            font-size: 12px;
        }

        @media (max-width: 600px) {
            .search-panel {
                left: 10px;
                right: 10px;
                width: auto;
            }
            /* Compact search box sits at the top-left; nearest-stations below it. The offset
               tracks the search panel's real height (--search-bottom, set by app.js) so a
               disambiguation list or error message pushes this down instead of being covered.
               The 84px is the fallback for the first paint / no ResizeObserver. */
            .nearest-stations {
                top: calc(var(--search-bottom, 71px) + 13px);
                left: 10px;
                right: 10px;
                min-width: auto;
            }
        }

        /* Legend Panel */
        .legend-panel {
            position: absolute;
            bottom: 20px;
            left: 20px;
            background: rgba(255, 255, 255, 0.98);
            padding: 15px;
            border-radius: 6px;
            box-shadow: 0 2px 12px rgba(11, 45, 39, 0.2);
            border: 3px solid #0b2d27;
            z-index: 1000;
            min-width: 180px;
            /* The key is anchored to the BOTTOM and grows upward, so anything that shrinks
               the CSS viewport — Safari page zoom, iOS Display Zoom, landscape — used to
               push its header (and the minimise button) off the top of the screen, leaving
               no way to collapse it. Cap the height and scroll the BODY instead, so the
               header with that button is always reachable. dvh tracks the iOS URL bar;
               the vh line above it is the fallback for browsers without dvh. */
            display: flex;
            flex-direction: column;
            max-height: calc(100vh - 120px);
            max-height: calc(100dvh - 120px);
        }

        .legend-panel .legend-body {
            overflow-y: auto;
            overscroll-behavior: contain;   /* don't chain the scroll to the map behind it */
            min-height: 0;                  /* let it actually shrink inside the flex column */
        }
        .legend-panel .legend-header { flex: 0 0 auto; }

        .legend-panel h2 {
            font-size: 14px;
            color: #0b2d27;
            margin-bottom: 10px;
            font-weight: 600;
            letter-spacing: 0.3px;
        }

        .legend-item {
            display: flex;
            align-items: center;
            margin-top: 8px;
            padding-left: 11px;   /* align swatch with the toggle pills' inset content */
            font-size: 12px;
        }

        .legend-color {
            width: 20px;
            height: 12px;
            border-radius: 2px;
            margin-right: 10px;
            border: 1px solid rgba(0, 0, 0, 0.2);
        }

        .legend-label {
            color: #333;
            font-weight: 500;
        }

        .legend-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            margin-bottom: 10px;
        }
        .legend-header h2 { margin-bottom: 0; }

        .legend-toggle-btn {
            background: transparent;
            border: none;
            cursor: pointer;
            color: #0b2d27;
            padding: 2px;
            border-radius: 4px;
            display: flex;
            line-height: 0;
        }
        .legend-toggle-btn:hover { background: rgba(11, 45, 39, 0.1); }
        /* Share button sits left of the minimise button; pushed right of the title.
           Shows the share glyph normally, a tick briefly after copying. */
        .legend-share-btn { margin-left: auto; }
        .legend-share-btn .icon-shared { display: none; }
        .legend-share-btn.shared .icon-share { display: none; }
        .legend-share-btn.shared .icon-shared { display: block; }
        .legend-panel.minimized .legend-share-btn { display: none; }
        .legend-toggle-btn .icon-expand { display: none; }

        /* Toggles styled as button-like pills: filled when on, dimmed when off */
        .legend-toggle {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-top: 6px;
            padding: 5px 10px;
            border: 1.5px solid #d5d5d5;
            border-radius: 14px;
            font-size: 12px;
            color: #333;
            cursor: pointer;
            user-select: none;
            transition: border-color .15s, background .15s, opacity .15s;
        }
        .legend-toggle input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
        .legend-toggle:has(input:focus-visible) { outline: 2px solid #0b2d27; outline-offset: 1px; }
        .legend-toggle:hover { border-color: #0b2d27; }
        .legend-toggle:has(input:checked) {
            border-color: #0b2d27;
            background: rgba(11, 45, 39, 0.07);
            font-weight: 600;
        }
        .legend-toggle:not(:has(input:checked)) { opacity: 0.5; }

        /* "Colour-code" stays a plain checkbox (a mode toggle, not a layer pill) */
        .legend-check {
            display: flex;
            align-items: center;
            gap: 6px;
            margin-top: 8px;
            font-size: 12px;
            color: #333;
            cursor: pointer;
        }
        .legend-check input { cursor: pointer; margin: 0; }
        .legend-hr { border: none; border-top: 1px solid #ddd; margin: 12px 0 2px; }

        /* HS2 sub-key: the build-stage colours, shown only while the HS2 layer is on */
        .legend-subkey {
            margin: 2px 0 4px 22px;
            padding-left: 8px;
            border-left: 2px solid #eee;
        }
        .legend-subitem {
            display: flex;
            align-items: center;
            gap: 8px;
            font-size: 12px;
            color: #555;
            padding: 2px 0;
        }
        /* An interactive sub-option (e.g. Underground "Stations") nested under its layer */
        .legend-subkey .legend-toggle { margin-top: 4px; }
        /* Compact legend rows: statuses flow inline; paired swatches/toggles/checks share a row */
        .legend-inline { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 14px; margin-top: 4px; }
        .legend-inline .legend-subitem { padding: 1px 0; }
        /* A pair per row where there is room, STACKED where there is not. `min-width: 0` let
           each pill shrink below its own label, and a word cannot shrink, so at a 240px
           viewport "Passenger" ran straight out through its pill's border and under the
           Freight one beside it. `max-content` is the floor that makes them wrap instead, and
           it needs no breakpoint or guessed flex-basis: they stack exactly when the two no
           longer fit, whatever the label or the text size. */
        .legend-cols { display: flex; flex-wrap: wrap; gap: 8px; }
        .legend-cols > * { flex: 1 1 auto; min-width: max-content; }
        .legend-checks { display: flex; flex-wrap: wrap; gap: 4px 16px; }
        .legend-checks .legend-check { margin-top: 8px; }

        /* Line-speed legend: one compact row — a segmented slow->fast bar with end labels */
        .legend-speedkey { display: none; align-items: center; gap: 6px; margin-top: 5px; }
        .legend-panel.speed .legend-speedkey { display: flex; }
        .speed-end { font-size: 11px; color: #555; white-space: nowrap; }
        .speed-bar { flex: 1; display: flex; height: 9px; min-width: 60px; border-radius: 3px; overflow: hidden; }
        .speed-bar i { flex: 1; }

        /* When colour-coding is off, dim the bridge/tunnel rows */
        .legend-panel.plain .legend-item.toggleable { opacity: 0.35; }

        /* Minimised key: collapse to just the header button */
        /* The whole collapsed panel is clickable (see app.js), so it needs to look it. */
        .legend-panel.minimized { min-width: auto; cursor: pointer; }
        .legend-panel.minimized:hover { background: #f4f8f6; }
        .legend-panel.minimized .legend-body { display: none; }
        .legend-panel.minimized .legend-header { margin-bottom: 0; }
        .legend-panel.minimized .legend-title-long { display: none; }
        .legend-panel.minimized .icon-collapse { display: none; }
        .legend-panel.minimized .icon-expand { display: block; }

        @media (max-width: 600px) {
            .legend-panel {
                left: 10px;
                min-width: auto;
                bottom: 44px;   /* lift clear of the collapsed attribution bar below it */
                /* Tighter than the desktop cap: the search box sits above (top 20 + ~71
                   tall) and the attribution below, so leave room for both. */
                max-height: calc(100vh - 155px);
                max-height: calc(100dvh - 155px);
            }
            /* When the attribution is expanded it grows to 2–3 lines — lift the key further
               so it doesn't sit on top. `maplibregl-compact-show` only exists in compact
               (mobile) mode, so this never affects desktop. */
            body:has(.maplibregl-ctrl-attrib.maplibregl-compact-show) .legend-panel {
                bottom: 74px;
            }
        }

        /* Home, "What's new" + maintainer links sit inside the attribution bar (bottom-right) */
        .maplibregl-ctrl-attrib a[href="/"],
        .maplibregl-ctrl-attrib a[href="/changelog.html"],
        .maplibregl-ctrl-attrib a[href="https://optional.org.uk"] {
            color: #0b2d27;
            font-weight: 600;
        }

        /* Tiny zoom-level readout tucked under the key (bottom-left) */
        .zoom-indicator {
            position: absolute;
            bottom: 3px;
            left: 6px;
            z-index: 900;
            font-size: 11px;
            color: rgba(11, 45, 39, 0.55);
            background: rgba(255, 255, 255, 0.55);
            padding: 0 4px;
            border-radius: 3px;
            pointer-events: none;
            font-variant-numeric: tabular-nums;
        }

        /* Donate button at the bottom of the key + its pop-up message card */
        .support-panel {
            position: relative;
            margin-top: 12px;
        }
        .support-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 7px;
            width: 100%;
            background: #0b2d27;
            color: #fff;
            border: none;
            border-radius: 6px;
            padding: 8px 12px;
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
        }
        .support-btn:hover { background: #164038; }
        .support-btn svg { display: block; flex-shrink: 0; }
        /* The card pops up from the button, overlaying the key while open (solid
           white so the key's content doesn't show through) */
        .support-card {
            position: absolute;
            bottom: calc(100% + 10px);
            left: 0;
            width: 270px;
            max-width: calc(100vw - 40px);
            background: #fff;
            border-radius: 8px;
            padding: 14px 16px;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
        }
        .support-card h3 { font-size: 14px; color: #0b2d27; margin-bottom: 6px; }
        .support-card p { font-size: 13px; line-height: 1.5; color: #333; margin-bottom: 10px; }
        .support-cta {
            display: block;
            text-align: center;
            background: #0b2d27;
            color: #fff;
            border-radius: 6px;
            padding: 9px;
            font-size: 13px;
            font-weight: 600;
            text-decoration: none;
        }
        .support-cta:hover { background: #164038; }

        /* On a PHONE the station popup stops being a balloon and becomes a card: pinned
           under the search box, horizontally centred, no tip. MapLibre positions a popup on
           its marker and never clamps it to the screen, so a station anywhere near the right
           edge put half the card off it, and at these widths the card is most of the screen,
           so there is no anchoring worth preserving. Covering the marker costs nothing: you
           just tapped it, so you know where it is.
           Vertically it sits below the search panel rather than in the middle, tracking
           --search-bottom like .nearest-stations does. That is not decoration: a popup lives
           inside #map, which has no z-index, while the search panel is a SIBLING at z-index
           1000, so a popup can never be lifted above it (see clearPopupOfPanels) and one that
           landed underneath would take the taps meant for its buttons. z-index 1150 does work
           against the dock tab strip, which IS inside the map, at 1100.
           `transform` needs !important because MapLibre writes the marker position there as
           an inline style on every camera move. */
        @media (max-width: 600px) {
            .maplibregl-popup.station-popup {
                left: 50% !important; right: auto !important; bottom: auto !important;
                top: calc(var(--search-bottom, 71px) + 13px) !important;
                transform: translateX(-50%) !important;
                z-index: 1150;
            }
            .station-popup .maplibregl-popup-tip { display: none; }
        }

        /* Station popup: name with info (Wikipedia) + navigation (Google Maps) links */
        /* The width CEILING, and it has to live here rather than on the constructor. MapLibre
           applies its `maxWidth` option as an inline style on the container and never clamps a
           popup to the viewport, so a fixed 310px drew a 247px-wide popup inside a 240px
           screen, cut off at both ends. `!important` is what beats the inline style. */
        .maplibregl-popup.station-popup { max-width: min(310px, calc(100vw - 20px)) !important; }
        .station-popup .maplibregl-popup-content {
            display: flex;
            align-items: center;
            /* Wraps so the action buttons drop onto their own full-width rows beneath the name.
               As a single non-wrapping row (one button, pre-Aug 2026) a second button pushed the
               popup absurdly wide and wrapped its label mid-phrase. */
            flex-wrap: wrap;
            gap: 8px;
            /* The close button is absolutely positioned top-right, so only the NAME needs to
               clear it. Reserving 28px on the whole content stopped the full-width buttons
               short of the right edge and read as stray padding. */
            padding: 10px 12px 11px;
            /* A floor as well as a ceiling. The popup was sizing to its content at about
               196px, comfortably under MapLibre's 240px default cap, which left the three
               chips fighting for room and the buttons looking cramped. This is the floor;
               the ceiling is the rule above. Both are capped against the viewport, so a
               narrow phone cannot be overflowed from either direction. */
            min-width: min(268px, calc(100vw - 44px));
        }
        .station-popup .popup-name {
          flex: 0 0 100%; font-size: 16.5px; font-weight: 700; line-height: 1.25;
          color: #0b2d27; padding-right: 34px;   /* clears the close button, this row only */
        }
        /* Icon plus word. The icons alone were ambiguous, and an info circle beside a bare
           arrow looked like one bordered button and one unbordered. */
                /* The three share the row EQUALLY rather than sizing to their text. The popup's
           width is set by the Arrivals & departures button below, which is narrower than
           three natural-width chips, so any label long enough to matter wrapped one onto
           its own row — first "Directions", and it would have happened again with the next
           wording. Equal thirds cannot wrap, and they line up with the full-width buttons
           underneath. */
        .station-popup .popup-links {
          /* min-width: 0 or the row cannot shrink to the popup. `flex: 0 0 100%` sets the
             basis, but a flex item's min-width is `auto`, i.e. its MIN-CONTENT: the three
             chips added up to 223px inside a 196px card at a 240px viewport, so the row hung
             27px out of its own popup and Share was cut off by the edge. With the floor
             released the chips compress and their labels ellipsis, which is what the
             `.popup-link span` rule below is for. Same trap as #postcodeInput. */
          flex: 0 0 100%; min-width: 0; display: flex; gap: 6px; margin: -1px 0 2px;
        }
        /* Small outlined chips. They are secondary to the two action buttons, so the border
           is a hairline and the fill stays white: enough to read as pressable without
           competing with the solid Arrivals button below. */
        .station-popup .popup-link {
          /* Equal thirds, and CENTRED: the icons carry their own whitespace inside a 24px
             viewBox, so symmetric side padding looked left-heavy. Centring makes that
             invisible instead of needing a fudged asymmetric padding. */
          flex: 1 1 0; min-width: 0; justify-content: center;
          gap: 5px; padding: 6px 4px; font-size: 12.5px; font-weight: 600;
          border: 1px solid #dde2df; border-radius: 7px; background: #fff;
          opacity: 1; color: #3d5b53; text-decoration: none;
        }
        .station-popup .popup-link:hover {
          color: #0b2d27; border-color: #0b2d27; background: #f4f8f6; text-decoration: none;
        }
        /* Belt and braces on the focus ring: keep it for keyboard users, drop it when
           focus arrived any other way. */
        .station-popup .popup-link:focus:not(:focus-visible) { outline: none; }

        /* Dated note for one station (core/notices.js). Full width like the buttons below it,
           amber so it does not read as a third action competing with them. */
        .station-popup .popup-notice {
          flex: 0 0 100%; display: block; margin: 2px 0 0; padding: 8px 10px;
          background: #fdf6e8; border: 1px solid #eeddba; border-left: 4px solid #d9a441;
          border-radius: 7px; font-size: 12.5px; font-weight: 700; line-height: 1.35;
          color: #7a5a1e; text-decoration: none;
        }
        .station-popup .popup-notice:hover { background: #fbeed3; color: #5f4616; }
        /* Close button: MapLibre's default sits hard against the corner and is a small tap
           target. Inset, boxed and 26px square, which is close to the 24px minimum worth
           aiming at on a phone. */
        .station-popup .maplibregl-popup-close-button {
          top: 7px; right: 7px; width: 26px; height: 26px; padding: 0;
          display: flex; align-items: center; justify-content: center;
          border: 1px solid #d8ded9; border-radius: 7px; background: #fff;
          color: #5b6670; font-size: 17px; line-height: 1;
        }
        .station-popup .maplibregl-popup-close-button:hover {
          background: #eef4f2; color: #0b2d27; border-color: #0b2d27;
        }
        /* Three sections: who it is, what you can do, where to read more. Hairlines rather
           than more whitespace, because the popup is already tall on a phone. */
        .station-popup .popup-actions {
          flex: 0 0 100%; display: flex; flex-direction: column; gap: 8px;
          border-top: 1px solid #edf0ee; padding-top: 10px; margin-top: 1px;
        }
        /* flex: none is load-bearing. These are flex children of a chip that is itself
           flex: 1 1 0 with min-width: 0, so with the default flex-shrink the icon is
           squeezed to nothing as soon as the label needs the room — which is exactly what
           happened to Directions. A width alone does not hold a flex item's size. */
        .station-popup .popup-link svg { flex: none; width: 15px; height: 15px; }
        /* Same reason: let the LABEL take the squeeze, and ellipsis rather than collapse. */
        .station-popup .popup-link span {
          min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        }
        /* Share is a <button> among <a>s, so it needs the anchor's box reset. The tick
           replaces the share glyph for a moment after a copy, the same feedback the train
           panel's share button gives. */
        /* A <button> among <a>s: it takes the same chip styling above, and only needs the
           bits a button does not inherit. */
        .station-popup .popup-share-btn {
          cursor: pointer; font-family: inherit; display: inline-flex; align-items: center;
        }
        .station-popup .popup-share-btn .ico-tick,
        .station-popup .popup-share-btn svg:nth-of-type(2) { display: none; }
        .station-popup .popup-share-btn.shared svg:nth-of-type(1) { display: none; }
        .station-popup .popup-share-btn.shared svg:nth-of-type(2) { display: block; }
        .station-popup .popup-share-btn.shared { color: #2e9e3f; }
        .popup-closed { font-size: 12px; color: #777; }
        .popup-links { display: inline-flex; gap: 4px; }
        .popup-link {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            color: #0b2d27;
            opacity: 0.7;
            transition: opacity 0.15s;
        }
        .popup-link:hover { opacity: 1; }
        .popup-link svg { width: 18px; height: 18px; fill: currentColor; display: block; }

        /* Live-train popup: stacked lines (name, route, next stop, lateness, disclaimer) */
        .train-popup .maplibregl-popup-content { padding: 9px 28px 10px 12px; font-size: 12px; line-height: 1.45; }
        .train-popup .popup-name { display: block; font-size: 14px; font-weight: 700; color: #0b2d27; }
        .train-popup .popup-train-route { color: #0b2d27; margin-top: 2px; }
        .train-popup .popup-train-next { color: #555; }
        .train-popup .popup-sched { color: #999; text-decoration: line-through; }
        /* The superseded half of a freight category ("Coal biomass", coal struck). Left
           UNSCOPED on purpose: the same label is drawn in the train panel and in the
           freight list, which share no ancestor. <s> supplies the line, this the colour. */
        .cat-was { color: #999; }
        .train-popup .popup-exp { color: #c0392b; font-weight: 600; }
        .train-popup .popup-train-delay { margin-top: 2px; }
        /* "Not reporting": the dot is the last confirmed position, not a live one. Amber
           rather than red, and a tinted block rather than a line of text, because it is a
           caveat about the DATA and has to survive being read at a glance next to a green
           "On time" that came from the same feed. */
        .train-popup .popup-train-unc {
            margin-top: 4px; padding: 5px 8px; border-radius: 6px;
            background: #fdf3e3; border: 1px solid #f0d9ae; color: #7a5a1e; font-size: 12px;
        }
        .train-popup .popup-train-unc strong { display: block; color: #8a5a00; }
        .train-popup .popup-train-rtt { display: block; font-size: 12px; margin-top: 3px; color: #0b2d27; }
        /* Where a FOLLOWED train still calls. A plain two-column list: station left, booked
           time right, one row per stop. No dots-and-line "journey spine" graphic, because the
           card is 124px wide at the sizes the last few fixes were about and a spine would eat
           a third of that for decoration. */
        .popup-calls { margin-top: 6px; border-top: 1px solid #e6ebe9; padding-top: 5px; }
        .popup-calls-head {
            font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
            color: #6b7a75; margin-bottom: 3px;
        }
        .popup-call { display: flex; gap: 8px; align-items: baseline; padding: 2px 0; color: #333; }
        /* The name gives way, not the time: "Ashford International" may wrap or ellipsis,
           while a time that wrapped to its own line would break the column the eye reads
           down. min-width: 0 is what lets the name shrink at all inside a flex row. */
        .popup-call-name { flex: 1 1 auto; min-width: 0; }
        .popup-call-time { flex: 0 0 auto; margin-left: auto; white-space: nowrap; color: #555; }
        .popup-call.is-last .popup-call-name { font-weight: 700; color: #0b2d27; }
        .popup-diagram-map {
            display: block; width: 100%; margin: 6px 0 2px; padding: 8px 12px;
            border: 1px solid #cddbd7; border-radius: 6px; background: #fff;
            color: #0b2d27; font-family: inherit; font-size: 12.5px; font-weight: 600;
            cursor: pointer; text-align: center;
        }
        .popup-diagram-map:hover { background: #eef4f2; }
        .popup-journey {
            display: block; width: 100%; margin: 8px 0 0; padding: 9px 12px;
            border: 1px solid #0b2d27; border-radius: 6px; background: #fff;
            color: #0b2d27; font-family: inherit; font-size: 13px; font-weight: 600;
            cursor: pointer; text-align: center;
        }
        .popup-journey:hover { background: #0b2d27; color: #fff; }
        .popup-calls-more {
            display: block; width: 100%; margin-top: 4px; padding: 4px 6px;
            border: 1px solid #d5dcd9; border-radius: 6px; background: #fff; color: #0b2d27;
            font-family: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; text-align: center;
        }
        .popup-calls-more:hover { background: #eef4f2; }
        /* Speed "how is this measured?" disclosure inside the train popup */
        .popup-sv-info { display: inline-block; margin-left: 5px; vertical-align: middle; }
        .popup-sv-info summary { list-style: none; cursor: pointer; display: inline-flex; }
        .popup-sv-info summary::-webkit-details-marker { display: none; }
        .popup-sv-info summary svg { width: 12px; height: 12px; fill: #0b2d27; opacity: 0.55; }
        .popup-sv-info summary:hover svg { opacity: 1; }
        .popup-sv-note { font-size: 11px; line-height: 1.4; color: #666; max-width: 210px; margin-top: 3px; }
        .train-popup .live-on { color: #2e7d32; font-weight: 600; }
        .train-popup .live-late { color: #c0392b; font-weight: 600; }
        .train-popup .popup-train-est { color: #999; font-size: 11px; font-style: italic; margin-top: 3px; }

        /* NOTE: this block lives at the END of the file ON PURPOSE. The offsets below
           target .search-panel / .nearest-stations, whose base rules appear earlier
           with the same specificity — from higher up the file they lost on source
           order, so the header silently overlapped the search box. */
        /* Site header — DESKTOP ONLY.
           Without it the map is a dead end: visitors landing here from search have no route
           to the rest of the site. On phones the map IS the product and a 58px bar costs ~9%
           of the viewport, so below 601px the header is hidden and the way back lives in the
           attribution bar instead (the "Train Map UK" link added in app.js). Everything here
           sits inside the media query, so the mobile layout is untouched. */
        .site-header { display: none; }

        @media (min-width: 601px) {
            .site-header {
                display: block;
                position: absolute; top: 0; left: 0; right: 0; height: 58px;
                z-index: 1100;   /* above the panels (1000) */
                background: #fff; border-bottom: 1px solid #e3e6e4;
            }
            .site-header .bar {
                max-width: 980px; margin: 0 auto; padding: 0 20px; height: 58px;
                display: flex; align-items: center; justify-content: space-between;
            }
            .site-header .nav { display: flex; align-items: center; flex-wrap: nowrap; }
            .site-header .brand {
                font-weight: 800; font-size: 17px; color: #0b2d27;
                text-decoration: none; letter-spacing: -0.01em;
            }
            .site-header .nav a {
                color: #5b6670; text-decoration: none; font-size: 14px;
                font-weight: 600; margin-left: 16px;
            }
            .site-header .nav a:hover { color: #0b2d27; }
            .site-header .nav a.btn {
                background: #0b2d27; color: #fff; padding: 8px 15px;
                border-radius: 8px; display: inline-block;
            }
            .site-header .nav a.btn:hover { background: #164038; color: #fff; }

            /* Nav dropdowns. map.html shows the header on desktop only, so the mobile sheet
               never applies here — but the hamburger must stay hidden, or it appears beside
               the five links. */
            .site-header .menu { display: none; }
            .site-header .nav-item { position: relative; margin-left: 16px; }
            .site-header .nav-item > summary {
                list-style: none; cursor: pointer; display: inline-flex; align-items: center;
                gap: 5px; color: #5b6670; font-size: 14px; font-weight: 600; white-space: nowrap;
            }
            .site-header .nav-item > summary::-webkit-details-marker { display: none; }
            .site-header .nav-item > summary::marker { content: ""; }
            .site-header .nav-item > summary::after {
                content: ""; width: 6px; height: 6px; margin-top: -3px; flex: none;
                border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
                transform: rotate(45deg);
            }
            .site-header .nav-item[open] > summary::after { transform: rotate(-135deg); margin-top: 2px; }
            .site-header .nav-item > summary:hover,
            .site-header .nav-item[open] > summary { color: #0b2d27; }
            .site-header .nav-item .drop {
                position: absolute; right: 0; top: calc(100% + 10px); z-index: 1200;
                background: #fff; border: 1px solid #e3e6e4; border-radius: 10px;
                box-shadow: 0 12px 34px rgba(11, 45, 39, .18); padding: 8px; min-width: 250px;
            }
            .site-header .nav-item .drop a {
                display: block; margin: 0; padding: 9px 12px; border-radius: 7px;
                color: #1f2933; font-size: 14.5px; font-weight: 600; white-space: nowrap;
            }
            .site-header .nav-item .drop a:hover { background: #eef4f2; color: #0b2d27; }


            /* Make room for it: the map and the top-anchored panels drop by 58px. */
            #map { top: 58px; }
            .search-panel { top: 78px; }
            .nearest-stations { top: 78px; }
            .board-panel { top: 78px; }
        }

        /* --- Routes view -------------------------------------------------------------
           A station's "Routes" button opens every station reachable without changing.
           The bar sits bottom-centre so it clears the key (bottom-left) and the zoom
           controls (bottom-right) on both phone and desktop. */
        /* Two stacked actions in a station popup. The board is the primary (solid) one —
           "what's coming" is what most people open a station for — and Routes steps back to
           an outline button rather than competing as a second solid block. */
        /* flex-basis 100% (not width) — these are flex items in the wrapping popup row above,
           so each claims a line of its own. Spacing comes from the container's gap. */
        .popup-board-btn, .popup-routes-btn {
            flex: 0 0 100%; padding: 9px 12px; white-space: nowrap;
            border-radius: 6px; font-family: inherit; font-size: 14px; font-weight: 700;
            cursor: pointer;
        }
        .popup-board-btn {
            background: #0b2d27; color: #fff; border: 2px solid #0b2d27;
        }
        .popup-board-btn:hover { background: #164038; border-color: #164038; }
        .popup-routes-btn {
            background: #fff; color: #0b2d27; border: 2px solid #0b2d27;
        }
        .popup-routes-btn:hover { background: #eef4f2; }
        .popup-routes-btn[disabled] { opacity: .7; cursor: progress; }

        .routes-bar {
            position: absolute; left: 50%; transform: translateX(-50%); bottom: 20px;
            z-index: 1050; display: flex; flex-direction: column; gap: 8px;
            background: rgba(255, 255, 255, 0.98); border: 3px solid #0b2d27;
            border-radius: 8px; padding: 12px 14px; max-width: min(420px, calc(100vw - 40px));
            box-shadow: 0 4px 18px rgba(11, 45, 39, 0.28); touch-action: pan-y;
        }
        .routes-bar[hidden] { display: none; }
        /* "Showing: <line>" bar when a line-name search isolates one line (mirrors the routes bar). */
        .line-isolate-bar {
            /* Centred with auto margins against left:0/right:0, NOT left:50% + translateX.
               An absolutely positioned box with only `left` set shrink-to-fits into the space
               from that edge to the containing block's right edge, so at left:50% the bar
               could never grow past HALF the viewport however high its max-width was. On a
               390px phone that capped it at 195px and squeezed the line name, which read as
               the name being too long rather than the bar being unable to widen. */
            position: absolute; left: 0; right: 0; margin-inline: auto; width: max-content;
            bottom: 20px;
            z-index: 1050; display: flex; align-items: center; gap: 10px;
            background: rgba(255, 255, 255, 0.98); border: 3px solid #0b2d27;
            border-radius: 8px; padding: 8px 8px 8px 14px;
            /* 460 was sized for a bar holding a name and one button. With the detail line and
               the share button it needs ~520 on one row, and the shortfall came off the NAME
               (the detail is `flex: none`), so a desktop bar read "Brig... | 33 stations".
               Narrow screens are unaffected: the 100vw term still governs there. */
            max-width: min(620px, calc(100vw - 40px));
            box-shadow: 0 4px 18px rgba(11, 45, 39, 0.28);
        }
        .line-isolate-bar[hidden] { display: none; }
        .line-isolate-name {
            font-weight: 600; color: #0b2d27; font-size: 14px;
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
            /* Bounded by the wrapper, or the name keeps its max-content width, overflows,
               and gets cut by the wrapper's clip with no ellipsis to show for it. */
            max-width: 100%; min-width: 0;
        }
        /* TWO ROWS AT EVERY WIDTH: name and controls, then the detail beneath them.
           An inline detail was tried first and does not survive five facts. At the 620px cap
           "78.9 miles · 20 stations · 12 trains now · up to 75 mph · 7.9m passengers a year"
           took the whole row, squeezed the line's NAME out completely (it is the only
           shrinkable item) and pushed the buttons off the edge. One layout for all widths
           also means one thing to reason about, rather than a breakpoint that swaps them. */
        .line-isolate-bar { flex-wrap: wrap; row-gap: 3px; }
        .line-isolate-name  { order: 1; flex: 1 1 auto; }
        .line-isolate-info  { order: 2; }
        .line-isolate-more  { order: 3; }
        .line-isolate-share { order: 4; }
        .line-isolate-clear { order: 5; }
        .line-isolate-count { order: 9; flex: 1 1 100%; }
        .line-isolate-note  { order: 10; flex: 1 1 100%; }
        /* WRAPS rather than truncating. It is a list of short facts, and clipping it mid-item
           produced "33 stations · 69 trains now · up to" on a 390px phone, where a cut-off
           "up to" reads as the map being broken rather than as there being more to see. */
        .line-isolate-count {
            font-size: 12px; color: #4a6a63; line-height: 1.35;
        }
        .line-isolate-count[hidden] { display: none; }
        /* Narrow: the detail drops to its own full-width row UNDER the buttons, so it has the
           whole bar to wrap into instead of the sliver left beside them. */
        /* At the narrowest widths the "About this line" label is what goes: the exit button
           and the share are actions, while that link is also reachable from the detail the
           reader can already see. */
        /* Matches the Routes card's share button: an icon-only chip that sits between the
           text and the exit button. `ICON_SHARE` carries no width/height of its own (the
           inline copies it replaced set them as attributes), so it is sized here. */
        .line-isolate-info {
            flex: none; display: inline-flex; align-items: center; justify-content: center;
            width: 34px; height: 34px; padding: 0; cursor: pointer;
            border: 1px solid #cddbd7; border-radius: 6px; background: #fff; color: #0b2d27;
        }
        .line-isolate-info svg { width: 18px; height: 18px; display: block; fill: currentColor; }
        .line-isolate-info:hover { background: #eef4f2; }
        .line-isolate-info.on { background: #0b2d27; color: #fff; border-color: #0b2d27; }
        /* The note is the only part of the bar allowed to scroll: on a short phone the full
           text is taller than the space above the map controls, and a bar that grew to fit
           would push its own buttons off the top. max-height in vh so it shrinks with the
           viewport rather than assuming one. */
        .line-isolate-note {
            margin-top: 4px; padding-top: 8px; border-top: 1px solid #e2eae8;
            font-size: 12px; line-height: 1.5; color: #33463f; text-align: left;
            max-height: min(42vh, 320px); overflow-y: auto; overscroll-behavior: contain;
        }
        .line-isolate-note[hidden] { display: none; }
        .line-isolate-note p { margin: 0 0 8px; }
        .line-isolate-note ul { margin: 0 0 4px; padding-left: 16px; }
        .line-isolate-note li { margin: 0 0 6px; }
        .line-isolate-note strong { color: #0b2d27; }
        .line-isolate-note a { color: #0b2d27; font-weight: 600; }
        /* Through to the line's own page, which has room for the caveats the bar has not.
           Hidden rather than absent when a line has no page: only 312 of 2,622 named lines
           are page-worthy, and a link to a 404 is worse than no link. */
        .line-isolate-more {
            flex: none; font-size: 12px; font-weight: 600; text-decoration: none;
            color: #0b2d27; border: 1px solid #cddbd7; border-radius: 6px;
            padding: 8px 10px; white-space: nowrap; background: #fff;
        }
        .line-isolate-more:hover { background: #eef4f2; }
        .line-isolate-more[hidden] { display: none; }
        /* On a phone the link's LABEL is shortened rather than the bar gaining a third row:
           "About this line" cost 48px more than the row had, which pushed "Show all" onto a
           line of its own. The `::after` swap keeps one element and one href. */
        @media (max-width: 480px) {
            .line-isolate-more { font-size: 0; padding: 8px 10px; }
            .line-isolate-more::after { content: 'About'; font-size: 12px; }
        }
        .line-isolate-share {
            flex: none; display: inline-flex; align-items: center; justify-content: center;
            width: 34px; height: 34px; padding: 0; cursor: pointer;
            border: 1px solid #cddbd7; border-radius: 6px; background: #fff; color: #0b2d27;
        }
        .line-isolate-share svg { width: 17px; height: 17px; display: block; }
        .line-isolate-share:hover { background: #eef4f2; }
        .line-isolate-share.shared { background: #0b2d27; color: #fff; border-color: #0b2d27; }
        .line-isolate-clear {
            flex: none; border: none; background: #0b2d27; color: #fff;
            border-radius: 6px; padding: 7px 12px; font-size: 13px; font-weight: 600; cursor: pointer;
        }
        .line-isolate-clear:hover { background: #14453b; }
        body:has(.maplibregl-ctrl-attrib.maplibregl-compact-show) .line-isolate-bar { bottom: 74px; }
        /* Routes takes over the bottom-left corner while it's open. */
        body.routes-active .legend-panel,
        body.routes-active .nearest-stations,
        body.routes-active .ns-restore-btn { display: none !important; }
        .routes-info { font-size: 13px; color: #333; line-height: 1.4; }
        /* The legs a unit's day could not draw, named under the counts. A gap in the drawing
           is the first thing a reader asks about, and a bare "2 not drawable" does not say
           which two. */
        /* `.routes-why`, NOT `.routes-info`: that class is the card's text wrapper. */
        .routes-why {
            flex: none; display: inline-flex; align-items: center; justify-content: center;
            width: 30px; height: 30px; padding: 0; cursor: pointer; margin-right: 2px;
            border: 1px solid #cddbd7; border-radius: 6px; background: #fff; color: #0b2d27;
        }
        .routes-why svg { width: 17px; height: 17px; display: block; fill: currentColor; }
        .routes-why.on { background: #0b2d27; color: #fff; border-color: #0b2d27; }
        /* The caveats, behind the i. Scrolls rather than growing the card off the screen. */
        .routes-note {
            margin-top: 6px; padding-top: 8px; border-top: 1px solid #e2eae8;
            font-size: 12px; line-height: 1.5; color: #33463f;
            max-height: min(38vh, 260px); overflow-y: auto; overscroll-behavior: contain;
        }
        .routes-note[hidden] { display: none; }
        .routes-note p { margin: 0 0 8px; }
        .routes-missed {
            display: block; margin-top: 3px; font-size: 11.5px; color: #6b7671;
            line-height: 1.45;
        }
        .routes-info strong { display: block; color: #0b2d27; font-size: 15px; }
        .routes-btn {
            padding: 10px 14px; border-radius: 6px; border: none; cursor: pointer;
            font-family: inherit; font-size: 14px; font-weight: 700; text-align: left;
        }
        .routes-btn span { display: block; font-weight: 400; font-size: 12px; opacity: .85; margin-top: 2px; }
        /* Slot for the zoom-out button, filled after the trial fit (see app.js). The bar is a
           column flex with a gap, so an empty slot would still cost 8px and inflate the
           measured height the fit padding is derived from. */
        .routes-more { display: contents; }
        .routes-more:empty { display: none; }
        /* The A-to-B departure list. Its own scroller with a bounded height: the bar is
           positioned, not docked, and a route with 200 departures would otherwise push the
           Done button off the bottom of a phone. Bounded in vh rather than px so it keeps
           its share of a short screen. */
        .pair-times:empty { display: none; }
        .pair-times { max-height: 28vh; overflow-y: auto; overscroll-behavior: contain; }
        .pair-times-head {
            font-size: 11px; font-weight: 700; text-transform: uppercase;
            letter-spacing: .04em; color: #6b7d78; margin: 2px 0 4px;
        }
        /* Three columns, not a flowing line: departure times are read by scanning DOWN one
           column, so they have to line up whether the clock is 09:13 or 14:07. */
        .pair-dep {
            display: grid; grid-template-columns: auto auto 1fr; gap: 8px;
            font-size: 13px; color: #0b2d27; padding: 3px 0;
            border-bottom: 1px solid #eef2f1; font-variant-numeric: tabular-nums;
        }
        .pair-dep:last-of-type { border-bottom: none; }
        .pair-arr { color: #4a6b62; }
        .pair-arr::before { content: '→ '; color: #9bb0aa; }
        .pair-len { color: #6b7d78; text-align: right; }
        .pair-more {
            margin-top: 4px; padding: 5px 8px; font-family: inherit; font-size: 12px;
            background: #fff; color: #0b2d27; border: 1px solid #cfdcd8;
            border-radius: 5px; cursor: pointer; width: 100%;
        }
        .pair-more:hover { background: #eef4f2; }
        .routes-zoomout { background: #fff; color: #0b2d27; border: 2px solid #0b2d27; }
        .routes-zoomout:hover { background: #eef4f2; }
        /* Share sits as an icon on the title row, matching the key's own share control,
           which leaves Done as the single full-width action. */
        .routes-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
        /* Share and minimise sit together at the right. A flex row rather than two loose
           children, so `justify-content: space-between` keeps the TITLE against the left
           edge: as siblings the three items would spread evenly and the title would drift. */
        .routes-head-btns { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
        .routes-min {
            background: transparent; border: none; cursor: pointer; color: #0b2d27;
            padding: 4px; border-radius: 5px; display: flex; line-height: 0; flex-shrink: 0;
        }
        .routes-min svg { width: 18px; height: 18px; }
        .routes-min:hover { background: rgba(11, 45, 39, 0.1); }
        .routes-share {
            background: transparent; border: none; cursor: pointer; color: #0b2d27;
            padding: 4px; border-radius: 5px; display: flex; line-height: 0; flex-shrink: 0;
        }
        .routes-share svg { width: 17px; height: 17px; }
        .routes-share:hover { background: rgba(11, 45, 39, 0.1); }
        .routes-share.shared { color: #2e7d32; }
        .routes-share.shared svg { display: none; }
        .routes-share.shared::after { content: '✓'; font-size: 15px; font-weight: 700; line-height: 1; }
        /* Secondary action, so the outline style: Done stays the one solid button in the bar. */
        .routes-board { background: #fff; color: #0b2d27; border: 2px solid #0b2d27; text-align: center; }
        .routes-board:hover { background: #eef4f2; }
        .routes-exit { background: #0b2d27; color: #fff; text-align: center; }
        .routes-exit:hover { background: #164038; }

        @media (max-width: 600px) {
            /* Stop short of the right edge: the zoom controls live there and a full-width
               bar sat straight on top of them. And sit near the bottom rather than floating
               84px up — the lift is only needed when the attribution bar is expanded. */
            .routes-bar { bottom: 12px; left: 10px; right: 76px; transform: none; max-width: none; }
            body:has(.maplibregl-ctrl-attrib.maplibregl-compact-show) .routes-bar { bottom: 74px; }
        }

        /* --- Station board: arrivals & departures of the trains running right now ----------
           Its resting place is the top-right corner, which the Routes view has already emptied
           (it hides the nearest-stations panel), well clear of the bottom-centre Routes bar and
           train panel. In practice the dock is on at every width and re-anchors whichever panel
           is open to the bottom-left, exactly as it does for the other four. */
        .board-panel {
            position: absolute; top: 20px; right: 20px; z-index: 1050;
            width: 320px; max-height: calc(100dvh - 190px); overflow-y: auto;
            background: rgba(255, 255, 255, 0.98); border: 3px solid #0b2d27;
            border-radius: 8px; box-shadow: 0 4px 18px rgba(11, 45, 39, 0.28);
            font-size: 13px; touch-action: pan-y;
        }
        .board-panel[hidden] { display: none; }
        /* Sticky so the station name stays put while a long board scrolls under it. */
        .board-head {
            position: sticky; top: 0; z-index: 1; background: #0b2d27; color: #fff;
            padding: 9px 32px 9px 12px;
        }
        .board-head strong { display: block; font-size: 14px; }
        .board-head span { font-size: 11px; opacity: .82; }
        .board-close {
            position: absolute; top: 5px; right: 7px; width: 24px; height: 24px;
            border: none; background: transparent; cursor: pointer; color: #fff;
            font-size: 22px; line-height: 22px; padding: 0; border-radius: 5px;
        }
        .board-close:hover { background: rgba(255, 255, 255, 0.18); }
        .board-group {
            font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
            color: #0b2d27; background: #eef4f2; padding: 5px 12px;
            border-bottom: 1px solid #d5e2de;
        }
        .board-row {
            display: block; width: 100%; text-align: left; font-family: inherit; font-size: 13px;
            border: none; border-bottom: 1px solid #e6ecea; background: #fff; color: #0b2d27;
            padding: 8px 12px; cursor: pointer;
        }
        .board-row:hover { background: #f2f7f5; }
        .board-row.selected { background: #e2eeea; box-shadow: inset 3px 0 0 #0b2d27; }
        .board-when { display: flex; align-items: baseline; gap: 7px; }
        .board-time { font-weight: 700; font-variant-numeric: tabular-nums; }
        .board-where { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .board-meta { font-size: 11.5px; color: #55665f; margin-top: 2px; }
        .board-plat { font-weight: 700; color: #0b2d27; }
        .board-empty { padding: 12px; font-size: 12.5px; line-height: 1.5; color: #55665f; }
        /* The disruption link on the empty board: its own line, so it reads as the next
           thing to do rather than as part of the sentence above it. */
        .board-empty a { display: block; margin-top: 7px; font-weight: 700; color: #0b2d27; }
        /* Per-group "Show more". Inset and outlined so it reads as a BUTTON: full-bleed with a
           row divider (its first form) made it look like just another board entry. Same outline
           style as the Routes bar's own buttons, and louder than the Realtime Trains link below
           — our next four are the better answer, with no page load. */
        .board-more {
            display: block; width: calc(100% - 24px); margin: 9px 12px 11px;
            box-sizing: border-box; text-align: center; padding: 8px 10px;
            border: 2px solid #0b2d27; border-radius: 6px;
            background: #fff; color: #0b2d27;
            font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
        }
        .board-more:hover { background: #eef4f2; }
        .board-more span {
            display: block; font-size: 11px; font-weight: 400; color: #55665f; margin-top: 1px;
        }
        /* Foot of the board: the full timetable, for everything the live view can't show —
           trains not yet running, and cancellations. */
        .board-rtt {
            display: block; padding: 10px 12px; font-size: 12.5px; font-weight: 700;
            color: #0b2d27; background: #f4f8f6; border-top: 1px solid #d5e2de;
        }
        .board-rtt:hover { background: #eef4f2; }

        /* Live-train tracking panel: same bottom card as the Routes bar. Reuses the .train-popup
           text styles (the body is wrapped in a .train-popup element). */
        .train-panel {
            position: absolute; left: 50%; transform: translateX(-50%); bottom: 20px;
            z-index: 1050; background: rgba(255, 255, 255, 0.98); border: 3px solid #0b2d27;
            border-radius: 8px; padding: 10px 30px 11px 14px;
            max-width: min(360px, calc(100vw - 40px));
            box-shadow: 0 4px 18px rgba(11, 45, 39, 0.28); touch-action: pan-y;
        }
        .train-panel[hidden] { display: none; }
        .train-panel .train-popup .popup-name { font-size: 15px; }
        .train-close {
            position: absolute; top: 4px; right: 6px; width: 24px; height: 24px;
            border: none; background: transparent; cursor: pointer; color: #0b2d27;
            font-size: 22px; line-height: 22px; padding: 0; border-radius: 5px;
        }
        .train-close:hover { background: rgba(11, 45, 39, 0.1); }
        /* Minimise / expand toggle — sits just left of the close ×; keeps tracking either way. */
        .train-toggle-btn {
            position: absolute; top: 4px; right: 32px; width: 24px; height: 24px;
            border: none; background: transparent; cursor: pointer; color: #0b2d27;
            padding: 0; border-radius: 5px; display: flex; align-items: center; justify-content: center;
        }
        .train-toggle-btn svg { width: 18px; height: 18px; }
        .train-toggle-btn:hover { background: rgba(11, 45, 39, 0.1); }
        /* Share-this-train button — sits left of the minimise button; flashes a tick on copy */
        .train-share {
            position: absolute; top: 4px; right: 58px; width: 24px; height: 24px;
            border: none; background: transparent; cursor: pointer; color: #0b2d27;
            padding: 0; border-radius: 5px; display: flex; align-items: center; justify-content: center;
        }
        .train-share span { display: flex; }
        .train-share svg { width: 16px; height: 16px; }
        .train-share:hover { background: rgba(11, 45, 39, 0.1); }
        /* While following there is no × (see showTrainPanel), so the remaining two buttons
           take over its slots rather than leaving a gap in the corner. */
        .train-panel.no-close .train-toggle-btn { right: 6px; }
        .train-panel.no-close .train-share { right: 32px; }
        .train-share .ico-tick { display: none; }
        .train-share.shared .ico-share { display: none; }
        .train-share.shared .ico-tick { display: flex; color: #1e7a44; }
        /* The two follow-mode buttons: zoom, and the explicit way out of the follow. Both are
           outline, so neither shouts over the card's own delay colours. */
        .train-zoom, .train-unfollow {
            display: block; width: 100%; margin-top: 8px; padding: 7px 10px;
            border: 2px solid #0b2d27; border-radius: 6px; background: #fff; color: #0b2d27;
            font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer; text-align: center;
        }
        .train-zoom:hover, .train-unfollow:hover { background: #eef4f2; }
        /* Primary CTA in the info panel: start following this train (opt-in, not automatic). */
        .train-follow {
            display: block; width: 100%; margin-top: 8px; padding: 8px 10px;
            border: 2px solid #0b2d27; border-radius: 6px; background: #0b2d27; color: #fff;
            font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer; text-align: center;
        }
        .train-follow:hover { background: #14453b; }
        /* "Tracking" tag: a small pill with a gently pulsing dot, so it's clear the panel is
           following a live train rather than showing a static popup. */
        .train-tag {
            display: inline-flex; align-items: center; gap: 5px; margin-bottom: 7px;
            font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
            color: #0b6b4f; background: #e7f0ed; border-radius: 10px; padding: 2px 8px 2px 7px;
        }
        .train-tag-dot { width: 7px; height: 7px; border-radius: 50%; background: #2e9e3f; flex: 0 0 auto; }
        /* Still marker (dwelling / held): waiting on the next position rather than actively moving. */
        .train-tag-upd { color: #8a6a00; background: #f6edd2; }
        .train-tag-upd .train-tag-dot { background: #e0a200; }
        /* Sitting at a station: a solid green, distinct from the lighter "Tracking" pill. */
        .train-tag-at { color: #fff; background: #1e7a44; }
        .train-tag-at .train-tag-dot { background: #fff; }
        @media (prefers-reduced-motion: no-preference) {
            .train-tag-dot { animation: trainTagPulse 1.6s ease-in-out infinite; }
        }
        @keyframes trainTagPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

        /* Freight detail on the train card: tags for WHAT it is, a labelled row for the
           numbers. Sentence case, not the uppercase of .train-tag above — a commodity can
           run to "Nuclear and power-station fuel", which shouts at that treatment. */
        /* The identity line: headcode, then the operator as a label beside it. Wraps, because
           "Great Western Railway" is 21 characters and the card is 244px wide on a phone. */
        .popup-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 7px; }
        /* In the PANEL (not the map popup) the header buttons are absolutely positioned over
           this line, and the card's 30px of right padding does not clear them: the share icon
           reaches 82px in from the right edge, so it landed on top of the headcode. Reserve
           the difference on this one line, the same trick .popup-name uses for the station
           popup's close button. Following drops the ×, so the reach is 26px shorter and the
           tag gets that room back. */
        .train-panel .popup-head { padding-right: 52px; }
        .train-panel.no-close .popup-head { padding-right: 26px; }
        /* Below ~300px of viewport no reserve can work, because there is no longer room for
           the headcode AND three 24px buttons on one line, and a word cannot shrink: the text
           runs under them whatever the padding. So give the buttons the line to themselves and
           start the body below it. Floating them was tried first and collides for the same
           reason (74px of content width, 72px of buttons); this needs no space at all, and the
           buttons keep the exact positions and order they have everywhere else.
           Only reachable by ZOOMING: the narrowest phone is 320px at 100%, and this is a 360px
           one at 150%, where the panel is left with 124px between the tab strip and the map
           controls. */
        @media (max-width: 300px) {
            .train-panel { padding: 34px 14px 11px; }
            .train-panel .popup-head, .train-panel.no-close .popup-head { padding-right: 0; }
        }
        .fr-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 7px 0 1px; }
        /* One pill shape, grouped so the operator and the freight type cannot drift apart. */
        .fr-tag, .toc-tag {
            display: inline-block; padding: 2px 9px; border-radius: 10px;
            font-size: 11.5px; font-weight: 700; line-height: 1.4;
            color: #fff; background: #0b2d27;
        }
        /* The quieter fill, for the two labels that are context rather than the point: the
           operator (the headcode names the train) and the headcode's class (we always know
           it, whereas the commodity beside it is the thing worth looking at). */
        .fr-tag-alt, .toc-tag { color: #0b6b4f; background: #e7f0ed; font-weight: 600; }
        /* #999 vanishes on the dark pill — see .cat-was for what this is striking out. */
        .fr-tag .cat-was { color: rgba(255, 255, 255, 0.6); }
        /* The labelled-value row. Named for the train popup, not for freight, because both
           kinds of train use it — the passenger card carries coaches, class and speed here. */
        .tp-stats { display: flex; flex-wrap: wrap; gap: 4px 15px; margin-top: 8px; position: relative; }
        .tp-stat { display: flex; flex-direction: column; line-height: 1.25; }
        .tp-stat-v {
            display: flex; align-items: center; gap: 4px;
            font-size: 13px; font-weight: 700; color: #0b2d27; font-variant-numeric: tabular-nums;
        }
        .tp-stat-v a { color: #0b2d27; }
        .tp-stat-l {
            font-size: 9.5px; font-weight: 600; letter-spacing: .06em;
            text-transform: uppercase; color: #7d8c87;
        }
        /* The speed's "how is this measured?" note. In a flex row an inline note would stretch
           its own column and shove the neighbouring stats sideways on open, so it is lifted out
           of the flow and floated under the value it explains. */
        .tp-stat-v .popup-sv-info { margin-left: 0; }
        /* Anchored to the ROW, not to the speed stat. Anchoring it to the stat put the note
           wherever that stat happened to sit, and the stat's position is not fixed: coaches
           and class come from formation data that covers under half the fleet, so speed is
           sometimes the third value and sometimes the only one. Left-anchored it hung off the
           right of the card, right-anchored it hung off the left. The row is always the full
           width of the card, so anchoring there is correct in every combination. */
        .tp-stats .popup-sv-note {
            position: absolute; top: 100%; left: 0; z-index: 5;
            width: max-content; max-width: min(210px, 100%);
            background: #fff; border: 1px solid #d5ded9; border-radius: 6px;
            padding: 6px 8px; margin-top: 4px; box-shadow: 0 3px 10px rgba(11, 45, 39, 0.18);
        }

        /* The same two ideas one size down, for the freight list's rows. */
        /* Freight filters. They sit under the panel's own heading and above the list, and
           they are the reason renderFreight only redraws .fr-list: a full innerHTML rewrite
           every 15s would take the caret out of this input mid-word. */
        .fr-filters { padding: 8px 10px 6px; border-bottom: 1px solid #e6ebe9; }
        .fr-filters .fr-q {
            width: 100%; box-sizing: border-box; padding: 6px 9px; font-family: inherit;
            font-size: 12.5px; color: #0b2d27; background: #fff;
            border: 1px solid #d5dcd9; border-radius: 7px;
        }
        .fr-filters .fr-q:focus { outline: none; border-color: #0b2d27; }
        .fr-filters-row { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
        /* The select takes the room and the checkbox takes what it needs: a commodity name
           is up to 34 characters ("Nuclear and power-station fuel") and "On the map" is not. */
        .fr-filters .fr-cat {
            flex: 1 1 auto; min-width: 0; padding: 5px 6px; font-family: inherit;
            font-size: 12px; color: #0b2d27; background: #fff;
            border: 1px solid #d5dcd9; border-radius: 7px;
        }
        .fr-onmap {
            flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px;
            font-size: 12px; color: #45504c; cursor: pointer; white-space: nowrap;
        }
        .fr-onmap input { margin: 0; }
        /* Settings panel. Borrows the Key's chrome (.legend-panel) so the two feel like one
           family and the header-pinned scrolling column comes for free; .settings-panel is
           only the differences. Grouped, because a flat list of unrelated switches is what
           the Key had become. */
        .settings-panel { min-width: 230px; }
        .settings-panel .legend-header h2 { font-size: 14px; }
        .set-group { padding: 12px 0; border-bottom: 1px solid #e6ebe9; }
        .set-group:first-child { padding-top: 2px; }
        .set-group:last-child { border-bottom: none; padding-bottom: 2px; }
        .set-group h3 {
            font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
            color: #6b7a75; margin: 0 0 7px;
        }
        .set-note { font-size: 11.5px; line-height: 1.45; color: #6b7a75; margin: 6px 0 0; }
        /* Two columns, because as a single stack these five links were the TALLEST group in
           the panel (188px against 157px for the controls people actually came for). The
           bleed margin matches the links' own padding, so the grid widens by exactly as much
           as the text is inset and the labels still line up with the rest of the panel.
           auto-fill rather than a fixed 2, so a wider panel uses the room; 120px is the
           widest label ("How it was built", 100px) plus its padding, so nothing truncates. */
        .set-links {
            display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
            gap: 1px 4px; margin: 0 -8px;
        }
        .set-links a {
            display: block; padding: 6px 8px; border-radius: 6px;
            font-size: 13px; font-weight: 600; color: #0b2d27; text-decoration: none;
        }
        .set-links a:hover { background: #eef4f2; }
        .set-links a::after { content: " \2192"; opacity: .5; }
        .set-reset {
            width: 100%; padding: 7px 10px; font-family: inherit; font-size: 12.5px;
            font-weight: 600; color: #0b2d27; background: #fff; cursor: pointer;
            border: 1.5px solid #d5d5d5; border-radius: 8px;
        }
        .set-reset:hover { background: #eef4f2; border-color: #0b2d27; }
        /* Train size: a slider, with the value read out beside its label. The track is
           tinted so the control reads as adjustable at a glance in a panel that is otherwise
           all checkboxes and pills. */
        .legend-size { margin-top: 8px; }
        .legend-size-label {
            display: flex; justify-content: space-between; align-items: baseline;
            font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
            color: #6b7a75; margin-bottom: 3px;
        }
        .legend-size-label output {
            font-variant-numeric: tabular-nums; letter-spacing: 0; text-transform: none;
            font-size: 12px; color: #0b2d27;
        }
        #trainSize {
            width: 100%; margin: 0; height: 22px; background: transparent;
            -webkit-appearance: none; appearance: none; cursor: pointer;
        }
        #trainSize::-webkit-slider-runnable-track {
            height: 5px; border-radius: 3px; background: #dbe3e0;
        }
        #trainSize::-moz-range-track { height: 5px; border-radius: 3px; background: #dbe3e0; }
        /* 18px, which is a real tap target on a phone. margin-top centres the thumb on a
           5px track (-(18-5)/2). */
        #trainSize::-webkit-slider-thumb {
            -webkit-appearance: none; appearance: none; width: 18px; height: 18px;
            margin-top: -6.5px; border-radius: 50%; background: #0b2d27;
            border: 2px solid #fff; box-shadow: 0 1px 3px rgba(11, 45, 39, .35);
        }
        #trainSize::-moz-range-thumb {
            width: 18px; height: 18px; border-radius: 50%; background: #0b2d27;
            border: 2px solid #fff; box-shadow: 0 1px 3px rgba(11, 45, 39, .35);
        }
        #trainSize:focus-visible { outline: 2px solid #0b2d27; outline-offset: 3px; }
        .fr-chip {
            display: inline-block; padding: 1px 7px; border-radius: 9px;
            font-size: 10.5px; font-weight: 700; color: #0b2d27; background: #dfeae6;
        }
        .fr-chip-alt { color: #5a6b65; background: #edf0ef; font-weight: 600; }
        .fr-chip .cat-was { color: #8fa39c; }
        .fr-t { margin-left: 6px; font-size: 11.5px; color: #55665f; font-variant-numeric: tabular-nums; }
        /* On a phone the longest label ("Nuclear and power-station fuel") overran the row and
           pushed the tonnage out through the ellipsis. Let the CHIP be the thing that
           truncates and pin the number, which is short and always worth keeping. Scoped to
           the freight panel: the station board shares .board-where with plain text. */
        .freight-panel .board-where { display: flex; align-items: center; min-width: 0; }
        .freight-panel .fr-chip { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .freight-panel .fr-t { flex: 0 0 auto; }

        @media (max-width: 600px) {
            /* Clear the zoom controls on the right, like the Routes bar. */
            .train-panel { bottom: 12px; left: 10px; right: 76px; transform: none; max-width: none; }
            body:has(.maplibregl-ctrl-attrib.maplibregl-compact-show) .train-panel { bottom: 74px; }
            /* On a phone the bottom card would sit over the key — hide it while tracking a train. */
            body.train-active .legend-panel,
            body.train-active .nearest-stations,
            body.train-active .ns-restore-btn { display: none !important; }
        }

        /* ---- Panel dock: on a phone the four panels (Key, Nearest, Routes, Train) collapse to
           folder-style tabs down the LEFT edge — thin, sideways label, the selected one sticking
           out further. One open at a time; the open panel docks at the bottom, clear of the tabs
           and the attribution. Mobile-only (body.dock set by the controller below 600px); desktop
           keeps the panels in their corners. Managed via body.dock + body.dock-<name> classes. */
        .panel-dock {
            position: absolute; left: 0; top: 50%; transform: translateY(-50%); z-index: 1100;
            display: none; flex-direction: column; gap: 8px;
        }
        .panel-dock[hidden] { display: none; }
        .panel-tab {
            width: 30px; min-height: 84px; display: flex; flex-direction: column; align-items: center;
            justify-content: center; gap: 7px; padding: 9px 0;
            border: 2px solid #0b2d27; border-left: none; border-radius: 0 10px 10px 0;
            background: rgba(255, 255, 255, 0.98); color: #0b2d27; cursor: pointer;
            font-family: inherit; font-size: 11px; font-weight: 700;
            box-shadow: 2px 2px 10px rgba(11, 45, 39, 0.18);
            transition: width .12s ease;
        }
        .panel-tab svg { width: 18px; height: 18px; flex: 0 0 auto; }
        .panel-tab span { writing-mode: vertical-rl; text-orientation: mixed; letter-spacing: .02em; }
        /* Selected tab pulls out further and inverts, like the front folder in a drawer. */
        .panel-tab.active { width: 40px; background: #0b2d27; color: #fff; }

        /* Dock is on at every width. Hide all dock-managed panels; show only the open one, docked
           bottom-left clear of the tab column. Higher specificity (+ !important) beats the panels'
           own display/position rules. On desktop the panel keeps its natural width; on a phone it
           stretches to fill (below). */
        body.dock .panel-dock { display: flex; }
        body.dock #legendPanel, body.dock #nearestStations, body.dock #routesBar,
        body.dock #trainPanel, body.dock #boardPanel,
        body.dock #freightPanel, body.dock #settingsPanel { display: none !important; }
        /* flex, NOT the block the other panels get: .legend-panel is a flex COLUMN, and that
           is what keeps its header pinned and lets .legend-body shrink and scroll. Forcing
           block here silently dismantled that, which is the real cause of the un-swipeable
           Key below. Routes is the same case and already says flex. */
        body.dock.dock-key #legendPanel { display: flex !important; }
        body.dock.dock-nearest #nearestStations { display: block !important; }
        body.dock.dock-routes #routesBar { display: flex !important; }
        body.dock.dock-train #trainPanel { display: block !important; }
        body.dock.dock-board #boardPanel { display: block !important; }
        body.dock.dock-freight #freightPanel { display: block !important; }
        /* flex, like the Key: same header-pinned, body-scrolls column. */
        body.dock.dock-settings #settingsPanel { display: flex !important; }
        body.dock.dock-key #legendPanel, body.dock.dock-nearest #nearestStations,
        body.dock.dock-routes #routesBar, body.dock.dock-train #trainPanel,
        body.dock.dock-board #boardPanel, body.dock.dock-freight #freightPanel,
        body.dock.dock-settings #settingsPanel {
            box-sizing: border-box !important;
            left: 50px !important; right: auto !important;
            bottom: var(--dock-pb, 12px) !important; top: auto !important;
            transform: none !important; margin: 0 !important;
            max-height: calc(100dvh - 160px) !important; overflow-y: auto; overflow-x: hidden;
        }
        /* The Key is the one docked panel with its OWN inner scroller, and two nested
           scrollers is what broke swiping it (13 Aug 2026). The rule above made the panel a
           scroll container, so .legend-body had no overflow of its own left to scroll, and
           its `overscroll-behavior: contain` then refused to CHAIN the gesture out to the
           panel, which is exactly what contain is for. The result: dragging the scrollbar
           worked, a wheel or a swipe anywhere inside the body did nothing at all. Verified by
           removing contain in the page, at which point the same wheel scrolled it 200px.
           So the Key keeps ONE scroller, the same one at every width: the body. That is also
           what the flex column above exists for, keeping the header and its minimise button
           pinned while the layer list moves under it. Docking it made the whole card scroll,
           header and all, which that comment says must not happen.
           `hidden` here is load-bearing, not tidiness: Chrome counts a scrollable child's
           CONTENT size in its parent's scrollHeight, so on `auto` the panel reports 85px it
           cannot really show and scrolls the pinned header away into it.

           THE SETTINGS PANEL NEEDS THE SAME RULE AND HAS TO SIT HERE TO GET IT. It borrows
           the Key's chrome, so it has the same .legend-body scroller inside it and the same
           two-scroller problem. It carried its own `overflow-y: hidden` up beside its
           `display: flex` above, which never applied: the grouped rule below that line sets
           `overflow-y: auto` on the identical selector, so equal specificity went to
           whichever came last and the panel scrolled its own header out of view. Reported
           on 23 Aug 2026 as "sometimes the settings panel doesn't come up", which is what
           that looks like: you tap Settings, the panel is still scrolled from last time and
           all that is left in view is the header bar. Keep both selectors AFTER the grouped
           rule; moving either back up silently reintroduces it. */
        body.dock.dock-key #legendPanel,
        body.dock.dock-settings #settingsPanel { overflow-y: hidden; }
        @media (max-width: 600px) {
            /* Phone: fill the width between the tabs (left) and the zoom/locate controls
               (bottom-right) — the panels are bottom-anchored, so their lower corner would
               otherwise sit on top of the controls. */
            body.dock.dock-key #legendPanel, body.dock.dock-nearest #nearestStations,
            body.dock.dock-routes #routesBar, body.dock.dock-train #trainPanel,
            body.dock.dock-board #boardPanel, body.dock.dock-freight #freightPanel,
        body.dock.dock-settings #settingsPanel {
                right: 66px !important; width: auto !important; max-width: none !important;
            }
            /* Below ~300px that 66px gutter is no longer a courtesy, it is most of the card:
               at a 240px viewport (a 360px phone at 150% page zoom) the panel is left with
               124px, half the screen, and a train card at that width wraps "Manchester
               Piccadilly" over three lines. So the panel takes the controls' corner and sits
               OVER them, 186px instead of 124px, a 50% gain for nothing but a covered zoom
               button while a panel happens to be open. It already paints above them: the
               panels are z-index 1000+ against MapLibre's z-index 2.
               The TAB STRIP is deliberately still clear on the left. It is how you switch
               panels and how you put one away, so covering it would leave a card with no way
               back, and the pinch gesture already covers what the zoom buttons do. */
            @media (max-width: 300px) {
                body.dock.dock-key #legendPanel, body.dock.dock-nearest #nearestStations,
                body.dock.dock-routes #routesBar, body.dock.dock-train #trainPanel,
                body.dock.dock-board #boardPanel, body.dock.dock-freight #freightPanel,
        body.dock.dock-settings #settingsPanel {
                    right: 10px !important;
                }
            }
        }

        /* Routes: day-type picker and the upcoming-change note */
        .routes-days { display: flex; gap: 6px; }
        .routes-day {
            flex: 1; padding: 7px 6px; font-family: inherit; font-size: 13px; font-weight: 700;
            border: 2px solid #0b2d27; border-radius: 6px; background: #fff; color: #0b2d27;
            cursor: pointer;
        }
        .routes-day:hover { background: #eef4f2; }
        .routes-day[disabled] { opacity: .38; cursor: default; background: #fff; }
        .routes-day.on { background: #0b2d27; color: #fff; }
        .routes-change {
            font-size: 12.5px; line-height: 1.45; color: #6a4b00;
            background: #fff6e0; border: 1px solid #f0d79a; border-radius: 6px; padding: 8px 10px;
        }

/* "A to B" search: the no-direct-service explanation, with a way forward rather than
   a dead end. Lives in the search panel's message area. */
.pair-miss { font-weight: 400; margin-top: 4px; }
.pair-alt {
    display: block; width: 100%; margin-top: 9px; padding: 8px 10px;
    border: 2px solid #0b2d27; border-radius: 6px; background: #fff; color: #0b2d27;
    font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
}
.pair-alt:hover { background: #eef4f2; }
.suggest-tag.is-journey { background: #0b2d27; }   /* journey A → B */

/* Headcode search: the "scheduled but not yet running" answer, and the picker when a
   headcode is shared by more than one service (72% of them are). */
.hc-note { font-weight: 400; font-size: 12.5px; color: #55665f; margin-top: 5px; }
.hc-row {
    text-align: left; font-size: 13px; color: #0b2d27; padding: 5px 0;
    border-bottom: 1px solid #e6ecea;
}
.hc-row:last-of-type { border-bottom: none; }
.hc-row strong { font-variant-numeric: tabular-nums; margin-right: 6px; }
.hc-pick { display: block; width: 100%; text-align: left; }

/* Freight panel reuses the station board's card and rows. The unplotted workings are rows
   that deliberately do not respond: there is no position to fly to, so a hover or a click
   would promise something we cannot deliver. */
.board-row.is-off { cursor: default; color: #55665f; background: #fbfcfb; }
.board-row.is-off:hover { background: #fbfcfb; }
.board-row.is-off .board-time { color: #55665f; }
/* Freight rows put the journey on its own second line, where it has the width to wrap
   instead of being ellipsed beside the headcode. */
.freight-panel .fr-route { color: #0b2d27; font-size: 12.5px; white-space: normal; }

/* "Live updates paused" pill. Shown when the poll has stopped because nobody has touched
   the page for a while (core/config.js LIVE_IDLE_MS); any interaction resumes it, so this
   is a status message that happens to be tappable rather than the only way back.
   Bottom-centre: the panels dock down the left edge and the zoom readout sits bottom-left,
   so this is the one free strip. */
#liveIdle {
  position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 1400;
  max-width: calc(100vw - 24px);
  background: rgba(11, 45, 39, .93); color: #fff; border: 0; border-radius: 999px;
  padding: 9px 17px; font: 600 13.5px/1 -apple-system, BlinkMacSystemFont, 'Segoe UI',
  'Helvetica Neue', Arial, sans-serif; cursor: pointer;
  box-shadow: 0 6px 22px rgba(11, 45, 39, .3);
}
#liveIdle:hover { background: #164038; }
#liveIdle[hidden] { display: none; }
/* On a phone the train panel occupies exactly this strip (bottom: 12px; left: 10px;
   right: 76px), so a bottom-anchored pill lands underneath the one panel most likely to be
   open while a tab is left running. Move it to just below the search box instead, tracking
   the search panel's real height via --search-bottom the way .nearest-stations does, so a
   disambiguation list pushes it down rather than covering it. */
@media (max-width: 600px) {
  #liveIdle {
    bottom: auto; top: calc(var(--search-bottom, 71px) + 13px);
    font-size: 12.5px; padding: 8px 14px;
  }
}

/* "What else unit N works today" on the train card. A native <details>, so it opens with
   no JavaScript; the legs are filled in on first open from live/diagrams.json. */
.popup-diagram { margin-top: 8px; border-top: 1px solid #e3e6e4; padding-top: 7px; }
.popup-diagram > summary { cursor: pointer; font-size: 12.5px; font-weight: 600;
  color: #0b2d27; list-style: none; }
.popup-diagram > summary::-webkit-details-marker { display: none; }
.popup-diagram > summary::after { content: ""; display: inline-block; width: 5px; height: 5px;
  margin-left: 6px; vertical-align: 2px; border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor; transform: rotate(45deg); }
.popup-diagram[open] > summary::after { transform: rotate(-135deg); vertical-align: 0; }
.popup-diagram-body { font-size: 12.5px; color: #45504c; margin-top: 6px; }
.popup-diagram-list { list-style: none; margin: 0; padding: 0; max-height: 190px; overflow-y: auto; }
.popup-diagram-list li { padding: 5px 0; border-bottom: 1px solid #f0f2f1; }
.popup-diagram-list .pd-top { display: flex; align-items: baseline; gap: 8px; }
.popup-diagram-list .pd-t { font-variant-numeric: tabular-nums; color: #5b6670;
  font-size: 12px; min-width: 34px; }
.popup-diagram-list .pd-hc { font-weight: 700; font-variant-numeric: tabular-nums;
  color: #0b2d27; text-decoration: none; }
.popup-diagram-list .pd-hc:hover { text-decoration: underline; }
/* Mileage pinned right, so the column reads down the card whatever the headcode width. */
.popup-diagram-list .pd-mi { margin-left: auto; font-variant-numeric: tabular-nums;
  color: #5b6670; font-size: 12px; white-space: nowrap; }
.popup-diagram-list .pd-route { color: #45504c; font-size: 12px; margin-top: 1px; }
/* Where the day has got to. Legs before the one on the card are dimmed rather than
   hidden: the whole diagram is the interesting part, and hiding half of it to answer
   "where am I" would trade one question for another. */
.popup-diagram-list li.is-done { opacity: .45; }
.popup-diagram-list li.is-now { background: #eef4f2; border-radius: 6px;
  margin: 0 -6px; padding: 5px 6px; box-shadow: inset 3px 0 0 #0b2d27; }
.popup-diagram-list li.is-now .pd-mi { color: #0b2d27; font-weight: 700; }
.popup-diagram-total { margin-top: 7px; font-size: 12.5px; color: #0b2d27; }
.popup-diagram-note { margin-top: 7px; font-size: 11.5px; color: #5b6670; }

/* "Station page" link in the station popup. A text link under the two buttons rather than
   a third button: the popup already wraps to fit two full-width buttons and a third would
   crowd it. Full width so it lines up with them. */
.popup-station-page {
  flex: 0 0 100%; border-top: 1px solid #edf0ee; padding-top: 9px; margin-top: 1px;
  font-size: 13.5px; font-weight: 700;
  color: #0b2d27; text-decoration: underline; text-underline-offset: 3px;
  white-space: nowrap;          /* it is short enough now that wrapping means a bug */
}
.popup-station-page:hover { color: #164038; }

/* Brand mark (assets/brand/trainmap-logo-colour.svg, inlined by scripts/build_nav.py).
   flex: none, or the header's justify-content squeezes it; the gap stops it reading as
   part of the "T". */
.site-header .brand { display: inline-flex; align-items: center; gap: 9px; }
.site-header .brand-mark { width: 26px; height: 26px; flex: none; }
.site-header .menu-panel .m-brand { display: flex; align-items: center; gap: 9px; }
.site-header .menu-panel .brand-mark { width: 22px; height: 22px; flex: none; }
.site-footer .foot-mark { display: block; width: 30px; height: 30px; margin: 0 auto 10px; }
