  :root {
    --board: #101418;        /* board casing */
    --panel: #171d23;        /* row panel */
    --line:  #232c34;
    --amber: #ffb400;        /* LED amber, the busway PID colour */
    --amber-dim: #7a5a10;
    --text:  #e8edf1;
    --muted: #8fa0ad;
    --live:  #35d07f;
  }
  * { box-sizing: border-box; margin: 0; }
  body {
    background: var(--board);
    color: var(--text);
    font-family: "Archivo", system-ui, sans-serif;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    padding: 24px 16px 48px;
  }
  /* Narrow by default (board only). Widens once the map is alongside, using
     most of the viewport so a wide monitor isn't mostly empty margin. */
  main { width: 100%; max-width: 620px; }
  main.wide { max-width: min(1760px, 96vw); }

  header { margin-bottom: 20px; }
  .titlebar {
    display: flex; align-items: baseline; gap: 12px;
    flex-wrap: wrap; margin-top: 2px;
  }
  .titlebar[hidden] { display: none; }
  .change {
    background: none; border: 2px solid var(--amber); border-radius: 999px;
    color: var(--text); font: inherit; font-size: 18px;
    padding: 4px 18px; cursor: pointer; white-space: nowrap;
  }
  .change:hover { background: var(--amber); color: #000; }
  .change:focus-visible { outline: 2px solid var(--amber-dim); outline-offset: 2px; }
  /* Every time on the page is the network's wall clock, and six networks now
     span three Australian timezones — say which one, pinned to the right
     edge of the titlebar with a monochrome globe for "timezone". */
  .tzbadge {
    margin-left: auto;               /* right-justified in the flex titlebar */
    display: flex; align-items: center; gap: 7px;
    border: 1px solid var(--line); border-radius: 999px;
    color: var(--muted); font-size: 16px; letter-spacing: 0.06em;
    padding: 3px 14px; white-space: nowrap;
  }
  .tzbadge[hidden] { display: none; }
  .tzbadge .tz-icon {
    font-family: "Noto Emoji", system-ui, sans-serif;   /* monochrome face */
    font-size: 18px;
  }
  h1 { font-size: 22px; font-weight: 700; line-height: 1.25; }
  /* The little goose flies you home — back to the landing and its search.
     Same amber ink as the hero bird. */
  .home-goose {
    color: var(--amber); text-decoration: none;
    align-self: center;
  }
  .home-goose svg { height: 26px; display: block; }
  .home-goose[hidden] { display: none; }
  /* "Change address" carries the goose: line art in the button's text
     colour, so it inverts with the label on hover. */
  #change-stop { display: inline-flex; align-items: center; gap: 8px; align-self: center; }
  #change-stop svg { height: 20px; display: block; }
  #change-stop[hidden] { display: none; }
  .home-goose:hover svg { filter: drop-shadow(0 0 6px rgba(255, 180, 0, 0.55)); }
  .home-goose:focus-visible { outline: 2px solid var(--amber-dim); outline-offset: 2px; }

  .search { position: relative; margin-bottom: 22px; }
  .search[hidden] { display: none; }
  .search-row { display: flex; gap: 10px; }
  .search-field { position: relative; flex: 1; }
  .near-me {
    background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
    color: var(--muted); font: inherit; font-size: 13px; white-space: nowrap;
    padding: 0 16px; cursor: pointer;
  }
  .near-me[hidden] { display: none; }
  .near-me:hover { color: var(--text); border-color: var(--amber-dim); }
  .near-me:focus-visible { outline: 2px solid var(--amber-dim); outline-offset: 2px; }
  .search-close {
    position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
    background: none; border: 0; color: var(--muted);
    font-size: 22px; line-height: 1; padding: 4px 8px; cursor: pointer;
  }
  .search-close:hover { color: var(--text); }
  .search-close[hidden] { display: none; }
  /* Keep typed text clear of the close button. */
  .search-field:has(.search-close:not([hidden])) input { padding-right: 42px; }
  .search input {
    width: 100%;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 15px;
    padding: 12px 14px;
    outline: none;
  }
  .search input:focus { border-color: var(--amber-dim); }
  .search-results {
    position: absolute; inset-inline: 0; top: calc(100% + 4px);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 8px;
    /* Never past the bottom of the screen: fitResults() measures the real
       gap to the viewport edge and tightens this; the vh value is only the
       pre-measurement fallback. Overflow scrolls inside the dropdown. */
    max-height: 45vh; overflow-y: auto;
    z-index: 10;
  }
  .search-results button {
    display: flex; align-items: center; gap: 10px;
    width: 100%; text-align: left;
    background: none; border: 0; color: var(--text);
    font: inherit; font-size: 14px;
    padding: 10px 14px; cursor: pointer;
  }
  .search-results button:hover, .search-results button:focus-visible {
    background: #1f2731; outline: none;
  }
  .search-results .rmain {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* A stop result carries its stop glyph, pinned to the row's right edge. */
  .search-results .ricon {
    flex: none; margin-left: auto;
    font-family: "Noto Emoji", system-ui, sans-serif;
    font-variant-emoji: text;
    font-size: 20px; line-height: 1; color: var(--muted);
  }
  .search-results .sid {
    font-family: "IBM Plex Mono", monospace;
    color: var(--muted); font-size: 12px; margin-left: 6px;
  }

  /* Journey-planner itinerary cards. */
  .itin {
    border: 1px solid var(--line); border-radius: 10px;
    padding: 10px 12px; margin: 0 0 10px;
    cursor: pointer;
  }
  .itin:hover { border-color: var(--amber-dim); }
  .itin.selected { border-color: var(--amber); background: var(--panel); }
  .itin .it-head {
    display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
    margin-bottom: 6px;
  }
  .itin .it-times { font-weight: 700; font-size: 16px; }
  .itin .it-sub { color: var(--muted); font-size: 12px; }
  .itin .it-risk {
    color: #e5484d; font-size: 12px;
    font-family: "IBM Plex Mono", monospace;
  }
  /* Legs reuse the arrivals row's parts (.badge/.mode/.num/.src-col) so the
     two boards speak one visual language — only the layout shell differs. */
  .itin { position: relative; }
  .itin .it-leg {
    display: grid;
    /* badge | departure description | source mark | time — a fixed badge
       column (like .row's) so descriptions line up across legs. */
    grid-template-columns: 130px 1fr auto auto;
    gap: 12px; align-items: center;
    padding: 5px 0; font-size: 13px;
    border-left: 3px solid var(--vcolor); padding-left: 10px;
  }
  .itin .it-detail {
    display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0;
    color: var(--vcolor);   /* the route description speaks its leg colour */
  }
  /* Departure time in its own right-aligned column, leg-coloured. */
  .itin .it-times2 {
    display: flex; flex-direction: column; gap: 2px; flex: none;
    align-items: flex-end; min-width: 46px;
    font-family: "IBM Plex Mono", monospace; font-size: 13px;
    color: var(--vcolor);
  }
  /* The journey's arrival: one dedicated row under the legs, plain ink. */
  .itin .it-arrive {
    display: flex; justify-content: space-between; align-items: baseline;
    padding: 6px 0 0 10px; margin-top: 2px;
    border-top: 1px solid var(--line);
    font-size: 13px; color: var(--muted);
  }
  .itin .it-arrive b {
    font-family: "IBM Plex Mono", monospace; color: var(--text);
  }
  /* Countdown to set-off, top right — same object as an arrivals row's.
     In the header row's flow (margin-left:auto), so it can never collide
     with the leg time columns below. */
  .itin .it-eta { margin-left: auto; align-self: flex-start; }
  .itin.started { border-color: #2ecc71; }
  .itin .it-actions { display: flex; justify-content: center; margin-top: 8px; }
  .itin .it-start {
    background: none; border: 2px solid var(--amber); border-radius: 999px;
    color: var(--text); font: inherit; font-size: 18px; font-weight: 700;
    padding: 5px 34px; cursor: pointer;
  }
  .itin .it-start:hover { background: var(--amber); color: #000; }
  .itin .it-start.remove { color: #e5484d; border-color: #e5484d; }
  .itin .it-start.remove:hover { background: #e5484d; color: #000; }

  /* The stop the arrivals belong to: glyph + name atop the board. */
  .board-head {
    display: flex; align-items: center; gap: 10px;
    font-weight: 700; font-size: 15px;
    padding: 2px 6px 10px; margin-bottom: 8px;
    border-bottom: 1px solid var(--line);
  }
  /* × closes the stop card back to the pinned address — the search box's
     borderless close, same language. */
  .board-head .bh-close {
    margin-left: auto; flex: none;
    background: none; border: 0; color: var(--muted);
    font-size: 22px; line-height: 1; padding: 0 4px; cursor: pointer;
  }
  .board-head .bh-close:hover { color: var(--text); }
  /* Monochrome, bright white — the exact ink of the viewed-stop marker on
     the map (LANDMARK_SELECTED_INK), so heading and marker read as one.
     The Noto Emoji face is what makes it monochrome: without it, browsers
     that ignore VS15 (Chrome) substitute the system COLOUR emoji font. */
  .board-head .bh-icon {
    font-size: 18px; color: #ffffff;
    font-family: "Noto Emoji", system-ui, sans-serif;
  }

  /* --- landing ----------------------------------------------------------- */
  /* First load, before any stop is chosen: nothing but the logo and the
     search. The board/map split is NOT display:none-d — MapLibre needs a
     sized container at construction or it never fires 'load' — it is parked
     off-screen, still sized, so the map warms up underneath the landing. */
  body.landing header, body.landing .status { display: none; }
  body.landing #split {
    position: fixed; left: -200vw; top: 0;
    width: min(1200px, 96vw);
    visibility: hidden; pointer-events: none;
  }
  body.landing .search { max-width: 560px; margin: 0 auto; }
  #hero { display: none; }
  body.landing #hero {
    display: flex; flex-direction: column; align-items: center;
    margin: max(14vh, 48px) 0 34px;
  }
  /* The goose: the emoji glyph's own outlines, lifted from the subset font
     into an inline SVG so the body can take a solid fill — green under the
     gold line art (green and gold, for Australia). Same shape everywhere,
     no colour-emoji font roulette. drop-shadow glows where text-shadow
     can't reach an SVG. */
  #hero .goose { color: var(--amber); }
  #hero .goose svg {
    height: 96px; display: block;
    filter: drop-shadow(0 0 28px rgba(255, 180, 0, 0.35));
  }
  .goose-body { fill: #00843d; }   /* Australian national green */

  .board {
    border: 1px solid var(--line);
    border-radius: 12px;
    overflow: hidden;
  }
  .row {
    display: grid;
    /* badge | destination | time-source + alerts | countdown */
    grid-template-columns: 130px 1fr 64px 96px;
    align-items: center;
    gap: 14px;
    padding: 14px 16px;
    background: var(--panel);
    border-top: 1px solid var(--line);
  }
  .row:first-child { border-top: 0; }

  /* Split badge: mode glyph | route number. Black plate, white border and
     divider; the glyph and the number both inherit the vehicle colour via
     `color`, so the badge matches the row stripe and the map marker. */
  .badge {
    justify-self: start;
    display: inline-flex;
    align-items: stretch;
    background: #000;
    border: 1px solid #fff;
    border-radius: 8px;
    overflow: hidden;
    font-weight: 700;
    color: var(--text);
    line-height: 1;
  }
  .badge .mode {
    display: flex; align-items: center; justify-content: center;
    padding: 6px 9px;
    border-right: 1px solid #fff;
  }
  /* Noto Emoji is monochrome, so the glyph inherits the badge's `color`.
     Listing it first means only these codepoints come from it. */
  .badge .mode {
    font-family: "Noto Emoji", "Archivo", system-ui, sans-serif;
    font-size: 22px;
    /* Belt and braces with the U+FE0E in the markup: newer engines honour this
       property, older ones only the variation selector. Both say the same
       thing — render these as text, not as colour emoji. */
    font-variant-emoji: text;
  }
  .badge .num {
    display: flex; align-items: center; justify-content: center;
    padding: 6px 10px; font-size: 22px; min-width: 48px;
  }
  .badge .num.wide { font-size: 15px; line-height: 1.15; padding: 6px 8px; }
  /* Named lines (Melbourne trains: "Williamstown") rather than numbers: clamp
     to the badge column instead of spilling into the destination. */
  .badge .num.xwide {
    font-size: 11px; line-height: 1.1; padding: 6px 6px;
    max-width: 74px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: block; align-content: center;
  }
  .dest { min-width: 0; }
  .dest .headsign {
    font-size: 15px; font-weight: 500;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .dest .sub {
    font-family: "IBM Plex Mono", monospace;
    font-size: 11px; color: var(--muted); margin-top: 3px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* Every service carries its own colour — that is its identity, and it must
     not change as the service gains or loses a live position. */
  .row { border-left: 3px solid var(--vcolor); padding-left: 13px; }
  /* Any row can be picked to draw its route. */
  .row { cursor: pointer; }
  .row:hover { background: #1c242d; }
  .row:focus-visible { outline: 2px solid var(--vcolor); outline-offset: -2px; }
  /* The service whose route is currently drawn. */
  .row.selected {
    background: color-mix(in srgb, var(--vcolor) 14%, var(--panel));
    border-left-width: 6px; padding-left: 10px;
  }
  /* Flashed when its marker is clicked on the map, in its own colour so the
     pairing is unambiguous. */
  .row.flash { animation: rowflash 0.73s ease-in-out 3; }
  @keyframes rowflash {
    50% { background: color-mix(in srgb, var(--vcolor) 24%, var(--panel)); }
  }
  /* Trip stop-list: when a service is selected its full calling pattern
     overlays the arrivals list, leaving the badge column visible so the
     board still reads as a board. The thick line down the left is the same
     route trace the map draws, in the same colour; each stop is a station
     dot on it. */
  .board { position: relative; }
  .trip-panel {
    position: absolute; inset: 0 0 0 160px;   /* clear of the badge column */
    /* top is set inline to sit just below the board heading (its height
       varies with name wrapping) — this is only the no-heading fallback. */
    display: flex; flex-direction: column;
    background: var(--panel);
    border-left: 1px solid var(--line);
    z-index: 5;
  }
  /* Stacked (phone): the overlay would smother the whole narrow board, so
     the timeline slots into the flow UNDER the service's own row instead
     (renderTripPanel picks the insertion point to match). */
  @media (max-width: 899px) {
    .trip-panel {
      position: static; inset: auto;
      border-left: 0;
      border-top: 1px solid var(--line);
      border-bottom: 1px solid var(--line);
      max-height: 48vh;
    }
  }
  .tp-head {
    flex: none; display: flex; align-items: center; gap: 10px;
    padding: 10px 14px; border-bottom: 1px solid var(--line);
    font-size: 13px; color: var(--muted); white-space: nowrap;
  }
  .tp-head .tp-dest {
    color: var(--text); font-weight: 500;
    overflow: hidden; text-overflow: ellipsis;
  }
  /* 🛜/📅 beside the badge — the board's source column, relocated here
     while the panel covers it. */
  .tp-head .tp-src {
    font-family: "Noto Emoji", system-ui, sans-serif;
    font-variant-emoji: text;
    font-size: 18px; color: var(--vcolor); flex: none;
  }
  .tp-close {
    margin-left: auto; flex: none;
    background: none; border: 0; color: var(--muted);
    font: inherit; font-size: 20px; line-height: 1; cursor: pointer;
    padding: 2px 6px;
  }
  .tp-close:hover { color: var(--text); }
  .tstops {
    flex: 1; overflow-y: auto;
    margin: 10px 14px 10px 24px;
    scrollbar-width: thin; scrollbar-color: var(--line) transparent;
  }
  /* The line, its dots and the vehicle markers all live INSIDE the scroll
     content (a border-left on the scroll container clipped anything hung
     off it to a sliver). The track is the positioning context. */
  /* Height follows the stops, not the panel: the line must END at the
     terminus, not run on to the panel floor when the panel is taller. */
  .ttrack { position: relative; padding: 4px 0 4px 30px; }
  .ttrack::before {                   /* the route line, full list height */
    content: ""; position: absolute; left: 9px; top: 0; bottom: 0;
    width: 6px; background: var(--vcolor);
  }
  .tstop {
    position: relative;
    display: flex; justify-content: space-between; align-items: baseline;
    gap: 12px; padding: 5px 0; font-size: 13px;
  }
  .tstop::before {                    /* station dot, centred on the line */
    content: ""; position: absolute; left: -18px; top: 50%;
    width: 8px; height: 8px; transform: translate(-50%, -50%);
    border-radius: 50%; background: var(--panel);
    border: 2px solid var(--vcolor);
  }
  /* A live vehicle, sitting on the line at its position along the run.
     Same recipe as the badge: black plate, glyph in the service colour. */
  .tveh {
    position: absolute; left: 12px; transform: translate(-50%, -50%);
    width: 24px; height: 24px; border-radius: 50%;
    background: #000; border: 2px solid var(--vcolor);
    display: flex; align-items: center; justify-content: center;
    font-family: "Noto Emoji", system-ui, sans-serif;
    font-variant-emoji: text; font-size: 13px; line-height: 1;
    color: var(--vcolor);
    pointer-events: none; z-index: 2;
  }
  .tveh.me { border-color: #fff; }    /* the selected service itself */
  .tstop .tname {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* Every time in the service colour: whether the run is live or timetabled
     is said ONCE, by the 🛜/📅 beside the badge in the panel header (and by
     the vehicle marker being solid or ghosted) — not re-encoded per row.
     Grey is reserved for .past rows, where the whole row dims. */
  .tstop .ttime {
    font-family: "IBM Plex Mono", monospace; font-size: 12px;
    color: var(--vcolor); flex: none;
  }
  .tstop.past { opacity: 0.45; }
  .tstop.here .tname { color: var(--text); font-weight: 600; }
  .tstop.here::before { background: #fff; border-color: #fff; }
  .tstop.skipped .tname, .tstop.skipped .ttime {
    text-decoration: line-through; color: var(--muted);
  }
  /* Collapsed history: "⋯ 12 earlier stops" above the last three served. */
  .tmore {
    padding: 4px 0 6px; font-size: 11px;
    font-family: "IBM Plex Mono", monospace; color: var(--muted);
  }

  @keyframes pulse { 50% { opacity: 0.25; } }
  @media (prefers-reduced-motion: reduce) {
    .due { animation: none !important; }
    /* Still needs to be identifiable, so hold the tint rather than pulse it. */
    .row.flash {
      animation: none;
      background: color-mix(in srgb, var(--vcolor) 24%, var(--panel));
    }
  }

  .eta {
    font-family: "IBM Plex Mono", monospace;
    text-align: right;
    color: var(--amber);
    text-shadow: 0 0 12px rgba(255, 180, 0, 0.35);
  }
  .eta .num { font-size: 30px; font-weight: 600; line-height: 1; }
  .eta .unit { font-size: 11px; color: var(--amber-dim); letter-spacing: 0.1em; }
  /* The countdown carries the service colour too, so the row reads as one
     object. The amber glow is dropped — it would tint the colour. */
  .eta { color: var(--vcolor); text-shadow: none; }
  .eta .unit { color: var(--vcolor); opacity: 0.6; }
  /* 📶 realtime / 📅 timetable — its own column between destination and
     countdown, so neither the number nor "MIN" is shifted by it. */
  .src-col {
    font-family: "Noto Emoji", system-ui, sans-serif;
    font-size: 22px; line-height: 1;
    display: flex; align-items: center; justify-content: center; gap: 8px;
    color: var(--vcolor); opacity: 0.85;
    font-variant-emoji: text;
  }
  /* Disruption warning, beside the source mark. Amber — it is a warning, not
     part of the service's identity, so it does not take the vehicle colour. */
  .alert-mark {
    color: var(--amber); cursor: pointer; opacity: 1;
    background: none; border: 0; font: inherit; padding: 2px;
    font-variant-emoji: text;
  }
  .alert-mark:hover { text-shadow: 0 0 8px rgba(255, 180, 0, 0.6); }
  .alert-mark:focus-visible { outline: 2px solid var(--amber-dim); outline-offset: 2px; }

  /* Disruption details popup. */
  .alert-modal {
    position: fixed; inset: 0; z-index: 50;
    background: rgba(6, 9, 12, 0.72);
    display: flex; align-items: center; justify-content: center; padding: 24px;
  }
  .alert-modal[hidden] { display: none; }
  .alert-box {
    background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
    max-width: 560px; max-height: 76vh; overflow-y: auto;
    padding: 20px 22px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  }
  .alert-box h2 {
    font-size: 15px; margin-bottom: 12px; color: var(--amber);
    display: flex; align-items: center; gap: 8px;
  }
  .alert-box h2 .glyph { font-family: "Noto Emoji", sans-serif; font-variant-emoji: text; }
  .alert-item { border-top: 1px solid var(--line); padding: 12px 0; }
  .alert-item:first-of-type { border-top: 0; padding-top: 0; }
  .alert-item .effect {
    font-family: "IBM Plex Mono", monospace; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.12em; color: var(--amber);
    margin-bottom: 4px;
  }
  .alert-item .head { font-weight: 700; font-size: 14px; margin-bottom: 6px; }
  .alert-item .body {
    font-size: 13px; color: var(--muted); white-space: pre-line; line-height: 1.45;
  }
  .alert-close {
    margin-top: 14px; background: none; border: 1px solid var(--line);
    border-radius: 999px; color: var(--muted); font: inherit; font-size: 12px;
    padding: 4px 14px; cursor: pointer;
  }
  .alert-close:hover { color: var(--text); border-color: var(--amber-dim); }
  .eta .due {
    font-size: 22px; font-weight: 600; letter-spacing: 0.06em;
    animation: pulse 1.2s infinite;
  }

  .status {
    font-family: "IBM Plex Mono", monospace;
    font-size: 11px; color: var(--muted);
    margin-top: 12px; text-align: right;
  }
  .empty, .error {
    padding: 32px 16px; text-align: center;
    color: var(--muted); font-size: 14px;
    background: var(--panel);
  }
  .error { color: #ff8a7a; }

  /* Both first-run panels — board and map — share one empty state so they read
     as a matched pair: same height, text on top, a monochrome glyph naming the
     panel's job beneath it. --ph-height keeps the two boxes exactly level. */
  :root { --ph-height: 240px; }
  .placeholder {
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 16px; min-height: var(--ph-height); padding: 24px;
  }
  .placeholder .ph-icon {
    font-family: "Noto Emoji", system-ui, sans-serif;
    font-variant-emoji: text;      /* the monochrome outline, tinted like text */
    font-size: 46px; line-height: 1;
    color: var(--muted); opacity: 0.6;
  }

  /* --- board + map layout ------------------------------------------------ */
  /* Stacked by default; side by side only when there is room AND a map. The
     board keeps its natural width and the map takes the remaining space. */
  .split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; }
  @media (min-width: 900px) {
    /* The board is a fixed, comfortable reading width on the left; the map takes
       all the remaining width, so the space a wide screen frees up goes to the
       map rather than to stretched-out rows. */
    .split.side-by-side {
      grid-template-columns: minmax(0, 560px) minmax(0, 1fr);
      align-items: start;
    }
    /* Fill the viewport height too, so the map is as big as fits without
       scrolling. The board caps its own rows to what fits (fittingRowCount). */
    .split.side-by-side #map { height: calc(100vh - 160px); min-height: 460px; }
    /* The map column is sticky so it stays visible against a long board. */
    .split.side-by-side #map-wrap { position: sticky; top: 24px; }
  }

  /* --- map view ---------------------------------------------------------- */
  #map-wrap[hidden] { display: none; }
  #map-wrap { position: relative; }
  /* Covers the map until a stop is chosen. The map is built and loading
     underneath — MapLibre needs a sized container, and having it warm means it
     draws the moment a stop is picked — but an empty view of Brisbane says
     nothing, so it is not shown. */
  /* Layout (centering, min-height) comes from .placeholder; this rule only
     covers the map and matches the board's panel styling. */
  /* Edge markers: a board vehicle outside the current view gets a ringed
     glyph pinned to the map edge nearest to it, with an arrow on the rim
     aiming at the vehicle. (The ring shape is free for this job now that
     ghosts are no longer drawn ringed.) Clicking one flies to the vehicle. */
  #edge-markers {
    position: absolute; inset: 0;
    overflow: hidden; pointer-events: none; z-index: 1;
  }
  .edge-veh {
    position: absolute; width: 30px; height: 30px; margin: -15px 0 0 -15px;
    border-radius: 50%;
    background: rgba(12, 16, 19, 0.9);
    border: 2px solid var(--vcolor);
    display: flex; align-items: center; justify-content: center;
    font-family: "Noto Emoji", system-ui, sans-serif;
    font-variant-emoji: text; font-size: 15px; line-height: 1;
    color: var(--vcolor);
    pointer-events: auto; cursor: pointer;
  }
  .edge-veh::after {                  /* rim arrow, aimed by --angle */
    content: ""; position: absolute; left: 50%; top: 50%;
    border: 5px solid transparent; border-left: 8px solid var(--vcolor);
    border-right: 0;
    transform: translate(-50%, -50%) rotate(var(--angle)) translateX(21px);
  }

  .map-empty {
    position: absolute; inset: 0; z-index: 2; text-align: center;
    background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
    color: var(--muted); font-size: 14px;
  }
  .map-empty[hidden] { display: none; }
  /* While the placeholder is up the map matches the board placeholder's height
     exactly, so the two first-run panels sit level. Still sized, so MapLibre
     can measure its container and load underneath. */
  #map-wrap.awaiting #map,
  .split.side-by-side #map-wrap.awaiting #map {
    /* +2px: the board's 1px border wraps its --ph-height placeholder (outer
       242), whereas #map is border-box (border inside height). Matching the
       outer boxes keeps the two empty panels exactly level. */
    height: calc(var(--ph-height) + 2px); min-height: calc(var(--ph-height) + 2px);
  }
  #map {
    height: 360px; border-radius: 10px; overflow: hidden;
    background: #0c1013; border: 1px solid #232c35;
  }
  .map-caption {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: 12px; padding: 8px 2px 0; color: var(--muted); font-size: 13px;
  }
  /* `display: flex` above outranks the UA `[hidden] { display: none }`, so
     without this the caption keeps its ~22px of layout even while hidden —
     which, under the opaque .map-empty overlay, made the empty map panel a text
     row taller than the board. */
  .map-caption[hidden] { display: none; }
  .map-caption .hint { font-family: "IBM Plex Mono", monospace; font-size: 11px; }
  /* MapLibre's own chrome, toned down to match the board */
  .maplibregl-ctrl-attrib { background: rgba(12,16,19,.8) !important; }
  .maplibregl-ctrl-attrib a { color: #7f8c99 !important; }
  /* Zoom −/+ as a row, so the whole control stack stays compact. */
  .maplibregl-ctrl-group:has(> .maplibregl-ctrl-zoom-in) {
    display: flex; flex-direction: row-reverse;
  }
  .maplibregl-ctrl-group:has(> .maplibregl-ctrl-zoom-in) button + button { border-top: none; }
  .maplibregl-ctrl-group:has(> .maplibregl-ctrl-zoom-in) button:first-child {
    border-left: 1px solid #ddd;
  }
  /* Scale bar, restyled for the dark map. */
  .maplibregl-ctrl-scale {
    background: rgba(12,16,19,.75); color: #cfd8e3; border-color: #7f8c99;
    font-family: "IBM Plex Mono", monospace; font-size: 11px;
  }
  .maplibregl-popup-content {
    background: var(--panel); color: var(--text);
    font-family: Archivo, system-ui, sans-serif; font-size: 13px;
    border-radius: 8px; padding: 8px 10px; border: 1px solid #2b3742;
  }
  .maplibregl-popup-content .pop-route { font-weight: 700; }
  .maplibregl-popup-content .pop-sub {
    color: var(--muted); font-size: 11px;
    font-family: "IBM Plex Mono", monospace;
  }
  .maplibregl-popup-tip { border-top-color: var(--panel) !important; }

  /* --- Phone portrait. The arrivals grid carries ~364px of fixed columns
     (badge | src | countdown) before the destination gets a pixel, so a
     360px screen pushed the countdown off the edge. Shrink the fixed
     columns and let the destination keep flexing; same for planner legs. */
  @media (max-width: 560px) {
    .row {
      grid-template-columns: 92px minmax(0, 1fr) 30px 58px;
      gap: 8px; padding: 10px 8px 10px 9px;
    }
    .badge .mode { font-size: 16px; padding: 5px 6px; }
    .badge .num { font-size: 16px; min-width: 30px; padding: 5px 6px; }
    .badge .num.wide { font-size: 12px; padding: 5px 5px; }
    .badge .num.xwide { font-size: 10px; max-width: 50px; }
    .src-col { font-size: 16px; gap: 4px; }
    .eta .num { font-size: 22px; }
    .eta .unit { font-size: 9px; }
    .itin .it-leg { grid-template-columns: 92px minmax(0, 1fr) auto auto; gap: 8px; }
  }

  /* The rider on the map: GPS blue dot, white ring; the wedge is the
     compass heading (hidden until the device supplies one). */
  .me-marker { position: relative; width: 18px; height: 18px; }
  .me-dot {
    position: absolute; inset: 2px; border-radius: 50%;
    background: #4e8cff; border: 2px solid #fff;
    box-shadow: 0 0 6px rgba(0, 0, 0, 0.6);
  }
  .me-wedge {
    position: absolute; left: 50%; top: -8px; transform: translateX(-50%);
    width: 0; height: 0;
    border-left: 6px solid transparent; border-right: 6px solid transparent;
    border-bottom: 9px solid #4e8cff;
  }
  .me-wedge[hidden] { display: none; }
