/* ---------- Fonts (self-hosted, see public/fonts/) ---------- */

@font-face {
  font-family: "Playfair Display";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/playfair-display-400.woff2") format("woff2");
}

@font-face {
  font-family: "Playfair Display";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/playfair-display-700.woff2") format("woff2");
}

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/inter-400.woff2") format("woff2");
}

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/inter-700.woff2") format("woff2");
}

/* ---------- Tokens ---------- */

:root {
  --color-bg: #0a0a0a;
  --color-panel-bg: #141414;
  --color-text: #f5f5f5;
  /* Secondary text colour is TBD from Footsteps (DESIGN.md) - placeholder until that token exists. */
  --color-text-muted: #999999;
  --color-on-time: #4caf50;
  --color-late: #f0ad4e;
  --color-unknown: #888888;
  /* Digital departure board (trains panel) - modern station LED/LCD look */
  --color-board-bg: #000000;
  --color-led-amber: #ffb703;
  --color-led-red: #ff4d4d;
  --font-display: "Playfair Display", Georgia, serif;
  --font-body: "Inter", Arial, sans-serif;
}

/* ---------- Base ---------- */

html, body {
  margin: 0;
  width: 100%;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  /* Mobile Safari and Chrome will otherwise inflate text on their own
     judgement, which moves type off the sizes every layout here is
     calculated from. fitBoardRows covers the phone boards if it happens
     anyway; this stops it happening in the first place. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* Scrolling is a safety valve, not part of the design. On a screen with
   room for the whole layout - the TV it was designed for - nothing
   scrolls. In a browser with chrome eating the top of the viewport, it
   would rather give up a little scroll than hide a departure. */
body {
  overflow-y: auto;
}

/* The browser hides [hidden] elements with a `display: none` of the
   lowest possible precedence, so *any* author rule setting `display` beats
   it and the element stays on screen with its attribute set. Several here
   do - .leave-by is inline-flex, .leave-by-row is flex, .service-message
   is -webkit-box for line clamping - so hiding them from JS silently did
   nothing: a leave-by alternative with no reachable service left a lone
   walking icon behind, and a disruption message could never be dismissed.
   This is the standard fix, and it has to outrank those rules to work. */
[hidden] {
  display: none !important;
}

h2 {
  margin: 0 0 1vh 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 3.2vh;
  font-style: normal;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* ---------- Safe area + grid ---------- */

/* Was `position: fixed; inset: 0`, which sizes to the *visual* viewport
   while the `vh` units inside resolve against the *large* viewport - the
   two differ by exactly the browser chrome, and on iOS Safari by design.
   The shortfall landed entirely on the boards row and clipped the trains
   panel down to one visible departure on an iPad and in the Fire Stick's
   browser. Now it's in normal flow with a minimum of one screen, so it
   fills the screen when it fits and grows when it doesn't. */
.safe-area {
  min-height: 100vh;
  min-height: 100dvh;
  padding: 3vh 3vw;
  box-sizing: border-box;
}

.grid {
  width: 100%;
  min-height: calc(100vh - 6vh);
  min-height: calc(100dvh - 6vh);
  display: grid;
  grid-template-areas:
    "header header"
    "trains buses"
    "weather weather"
    "ticker ticker";
  /* Header, weather and ticker are `auto` - sized by their own content
     rather than a vh figure, so none of them can claim height the
     container doesn't actually have. The boards row takes what's left,
     but never less than `--boards-min`: that floor is what stops a
     short viewport from silently squeezing departures off the trains
     board, which is exactly what fixed vh row heights used to do. When
     the floor doesn't fit, the page scrolls instead of hiding a train.

     Weather sits in its own strip rather than the header to make room for
     the hourly forecast; the ticker holds one line of text and is sized
     to it. */
  --boards-min: 46vh;
  grid-template-rows: auto minmax(var(--boards-min), 1fr) auto auto;
  grid-template-columns: 1fr 1fr;
  column-gap: 2vw;
  row-gap: 0.7vh;
  transition: opacity 1s ease;
}

/* Portrait, or anything close to square: two half-width panels leave the
   boards too narrow for their own columns, so everything goes single
   file and the page scrolls. Each panel sizes to its content, so nothing
   is clipped - on a tablet held upright you scroll, rather than losing a
   departure. */
@media (max-aspect-ratio: 1 / 1) {
  .grid {
    grid-template-areas:
      "header"
      "trains"
      "buses"
      "weather"
      "ticker";
    grid-template-columns: 1fr;
    grid-template-rows: auto;
    row-gap: 1.2vh;
  }

  /* Height is content-driven here, so the row lists have no reason to
     give anything up. */
  .retro-rows {
    flex: none;
    min-height: auto;
    overflow: visible;
  }

  .boards-stack {
    row-gap: 1.6vh;
  }

  /* vh-sized type on a tall, narrow screen runs large - pull the biggest
     elements back so the clock doesn't dominate the fold. */
  .clock-block--primary .clock {
    font-size: 6vh;
  }

  .clock-block--secondary .clock {
    font-size: 3.6vh;
  }

  .date-line {
    font-size: 2.2vh;
  }
}

body.degraded-page .grid {
  opacity: 0.85;
}

.panel {
  background: var(--color-panel-bg);
  border-radius: 0.6vh;
  padding: 0.8vh 1.6vw;
  box-sizing: border-box;
  overflow: hidden;
  transition: opacity 1s ease;
}

.panel.degraded {
  opacity: 0.55;
}

/* ---------- Header: clocks + date ---------- */

.header {
  grid-area: header;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.clock-block {
  display: flex;
  flex-direction: column;
}

.clock-block--secondary {
  align-items: flex-end;
  text-align: right;
}

.clock {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1;
}

.clock-block--primary .clock {
  font-size: 8vh;
}

.clock-block--secondary .clock {
  font-size: 4.6vh;
}

.date-line {
  font-family: var(--font-display);
  font-size: 2.8vh;
  color: var(--color-text-muted);
  margin-top: 0.6vh;
}

.city-label {
  font-size: 2.4vh;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-top: 0.5vh;
  display: flex;
  align-items: center;
  gap: 0.6vw;
}

.clock-block--secondary .city-label {
  flex-direction: row-reverse;
}

.call-window {
  display: inline-block;
  width: 1.4vh;
  height: 1.4vh;
  border-radius: 50%;
  background: var(--color-unknown);
}

.call-window.call-ok {
  background: var(--color-on-time);
}

.call-window.call-late {
  background: var(--color-late);
}

.sydney-temp {
  font-size: 2.2vh;
  text-transform: none;
  letter-spacing: 0;
  color: var(--color-text);
}

.sydney-temp.degraded-text {
  color: var(--color-text-muted);
}

/* ---------- Trains / Buses ---------- */

.trains {
  grid-area: trains;
}

.buses {
  grid-area: buses;
}

.route {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 2.4vh;
  color: var(--color-text-muted);
}

/* ---------- Trains: modern digital departure board (LED/LCD style) ---------- */

.retro-board {
  background: var(--color-board-bg);
  border: 1px solid rgba(255, 183, 3, 0.15);
}

.retro-board-title {
  margin: 0 0 0.3vh 0;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 2.6vh;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-led-amber);
  text-shadow: 0 0 0.6vh rgba(255, 183, 3, 0.5);
}

/* Two boards sharing one panel, same size, now stacked rather than side by
   side. Side-by-side gave each board only a quarter of the screen width,
   which is what forced the character cells down to 1.5vh - below the 2.2vh
   floor DESIGN.md sets for secondary text - while two thirds of the panel's
   *height* sat empty. Stacking trades the spare height for the width the
   cells actually needed. Both boards still get identical space, so neither
   reads as the lesser one. */
/* Both board panels are flex columns all the way down, so that when
   content grows - a delay reason on every row, an NRCC disruption
   message - the squeeze lands on the row lists, which clip, and never on
   the leave-by line or the service message. Clipping the third departure
   on a bad day is survivable; clipping "leave by 08:14" or the reason for
   the disruption is not. Before this, a fully-delayed board overflowed the
   panel by ~230px and took the return board's leave-by row with it. */
.trains,
.buses {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.boards-stack {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-rows: 1fr 1fr;
  row-gap: 0.6vh;
}

.retro-board-col {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.retro-board-col .route,
.retro-columns,
.leave-by-row,
.service-message,
.panel-status {
  flex: none;
}

/* The flexible part: gives up height first, clipping from the bottom. */
.retro-rows {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}


.retro-board-col--return {
  border-top: 1px solid rgba(255, 183, 3, 0.15);
  padding-top: 1vh;
}

.retro-board-col .route {
  display: block;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 2vh;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  margin-bottom: 0.3vh;
}

/* Column tracks are shared by the header row and the data rows, and are
   sized off the character cells below (2.1vh wide each): time/arr are 5
   characters, status 9, plat 2. The trailing 1fr is the countdown column,
   which is plain text rather than cells. ARR carries ~2vh of extra
   breathing room before PLAT. */
.retro-columns,
.retro-row {
  grid-template-columns: 10.5vh 18.9vh 12.5vh 4.2vh 1fr;
  column-gap: 0.5vw;
}

.retro-columns {
  display: grid;
  font-size: 1.9vh;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: 0 0.3vw 0.2vh;
}

/* Matches the data cell's pad above so the "In" heading sits over its own
   column rather than against "Plat". */
.retro-columns span:last-child {
  padding-left: 1vh;
  margin-bottom: 0.2vh;
  border-bottom: 1px solid rgba(255, 183, 3, 0.2);
}

.retro-rows {
  border-radius: 0.4vh;
  padding: 0.2vh 0.6vw;
}

.retro-row {
  display: grid;
  font-size: 1.9vh;
  /* Split rather than the "gap" shorthand - this row has its own implicit
     vertical rows (fields, reason, calling-at) and a vw-based row-gap
     would size vertical spacing off the wrong axis, throwing off how
     "Calling at" centers between two rows depending on aspect ratio. */
  row-gap: 0.25vh;
  align-items: center;
  padding: 0.3vh 0;
  border-top: 1px solid rgba(255, 183, 3, 0.12);
}

.retro-row:first-child {
  border-top: none;
}

/* One line, ellipsised. Delay reasons run long ("...due to a fault with
   the signalling system earlier in the journey"), and wrapping them was
   the single biggest source of the overflow the flex rules above now
   absorb - the first few words carry the information at a glance anyway. */
.retro-row .reason {
  grid-column: 1 / -1;
  margin-top: 0.4vh;
  font-size: 2vh;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Clipped to the data columns rather than the whole row: it used to span
   to `-1`, so the text swept in from the far right edge of the panel,
   across the countdown column and all the empty space beyond it. Stopping
   at the last character-cell column keeps the movement inside the board's
   own alignment and leaves the space to its right free. */
.retro-row .calling-at {
  grid-column: 1 / 5;
  margin-top: 0.3vh;
  margin-bottom: 0.1vh;
  overflow: hidden;
  white-space: nowrap;
  /* Fade both ends rather than guillotining them. The text is longer than
     the space by design - it's a marquee - but a hard cut mid-letter at
     the panel edge reads as text spilling off the screen rather than as
     something deliberately scrolling, which is exactly how it looked on a
     phone. Fading makes the clip legible as intent. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 5%, #000 92%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 5%, #000 92%, transparent 100%);
}

/* Buses have one fewer column - the span stops after Arr. */
.retro-row--buses .calling-at {
  grid-column: 1 / 4;
}

/* Always scrolls, by request - a deliberate, standing exception to
   DESIGN.md's no-motion rule (recorded there). Slides fully in from the
   right and fully out to the left (translateX is relative to the text's
   own width here, not the viewport's), so it works at any text length
   without measuring anything in CSS. */
.calling-at-text {
  display: inline-block;
  font-size: 2vh;
  color: var(--color-led-amber);
  white-space: nowrap;
  animation: calling-at-scroll 14s linear infinite;
}

@keyframes calling-at-scroll {
  0% { transform: translateX(100%); }
  100% { transform: translateX(-100%); }
}

.retro-row.cancelled .flap-status .flap-static {
  color: var(--color-led-red);
}

/* Buses: same Time/[[middle]]/Arr column widths as the trains board even
   though "Line" only needs a couple of characters where trains' "Status"
   needs nine - both panels are equal width with the same padding, so
   matching widths means the two boards' columns line up with each other,
   not just internally. No Plat column; the countdown's trailing 1fr starts
   right after Arr instead. */
.retro-columns--buses,
.retro-row--buses {
  grid-template-columns: 10.5vh 18.9vh 12.5vh 1fr;
}

/* Countdown to departure. Plain text, not character cells, and
   deliberately never flickers: it changes every minute on every row, and
   the LED flicker at that rate would be a standing animation in all but
   name - exactly what DESIGN.md's Motion section rules out. */
.time-until {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 2.2vh;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
  white-space: nowrap;
  /* The grid's column-gap is vw-based, which is only a few px on a narrow
     portrait screen - enough for this to butt right up against the
     platform number. A vh-based pad keeps them apart at any width. */
  padding-left: 1vh;
}

/* No separate "imminent" colour: red already means cancelled on this
   board, amber means live data and green means on time. The word "due"
   carries the urgency without spending a fourth meaning on the palette.
   "late" is the one exception, and it borrows the existing amber rather
   than introducing anything new - it is by definition the live-tracking
   case, which is what amber already means here. */
.retro-row.overdue .time-until {
  color: var(--color-led-amber);
}

/* Been and gone, but still before its scheduled time. Dimmed rather than
   coloured: red means cancelled and amber means live tracking, and this is
   neither - it's a row that is still worth reading precisely so you don't
   set off for it, then stops being worth anything once its time passes. */
.retro-row.departed {
  opacity: 0.45;
}

.retro-row.departed .time-until {
  color: var(--color-text);
  letter-spacing: 0.04em;
}

.retro-row.cancelled .time-until {
  color: var(--color-led-red);
}

.flap-field {
  display: inline-grid;
  grid-auto-flow: column;
  gap: 0.05vw;
  /* Without this, a field whose track is wider than its own content (ARR's
     deliberately is, for breathing room before PLAT) stretches to fill
     it - and that extra space doesn't sit as trailing space after the
     digits, it gets distributed into the gaps between them instead,
     loosening the character spacing itself rather than the column gap. */
  justify-self: start;
}

.flap-char {
  position: relative;
  width: 2.1vh;
  height: 3vh;
  overflow: hidden;
}

.flap-static {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 2.4vh;
  font-variant-numeric: tabular-nums;
  color: var(--color-led-amber);
  text-shadow: 0 0 0.5vh currentColor;
}

/* Value-change flicker, styled for an LED/LCD board rather than a
   mechanical flap - a quick brightness pulse, staggered per character.
   Uses currentColor so a cancelled row's red status flickers red, not
   amber. */
.flap-static.flicker {
  animation: led-flicker 0.4s ease-out;
}

@keyframes led-flicker {
  0% { opacity: 0.15; text-shadow: none; }
  35% { opacity: 1; text-shadow: 0 0 1vh currentColor; }
  60% { opacity: 0.3; text-shadow: none; }
  100% { opacity: 1; text-shadow: 0 0 0.5vh currentColor; }
}

/* margin-top:auto pins this to the bottom of its board, so it sits in a
   stable place rather than floating up when a board happens to be short -
   and so it's the row list above, not this, that gives up space when a
   board is over-full. */
.leave-by-row {
  display: flex;
  align-items: baseline;
  gap: 1.2vw;
  margin-top: auto;
  padding-top: 0.5vh;
}

.leave-by-label {
  font-size: 2.2vh;
  font-weight: 700;
  margin-right: 1vw;
}

.leave-by {
  font-size: 2.4vh;
  font-weight: 700;
  display: inline-flex;
  align-items: baseline;
  gap: 0.5vw;
}

.leave-by-icon {
  font-size: 0.85em;
}

/* Darwin's NRCC service messages - the whole point of the trains panel on
   a bad day, and previously fetched and then dropped on the floor. Amber
   rather than muted grey: when this is present it matters more than the
   board it sits under. */
.service-message {
  margin-top: 0.6vh;
  font-size: 1.9vh;
  line-height: 1.35;
  color: var(--color-led-amber);
  border-left: 0.4vh solid var(--color-led-amber);
  padding-left: 0.8vw;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.panel-status {
  margin-top: 0.5vh;
  font-size: 1.9vh;
  color: var(--color-text-muted);
  min-height: 1.9vh;
}

/* ---------- Weather strip ---------- */

/* A wide, short band rather than the header one-liner it replaced: laid
   out horizontally (now | sun times | next 8 hours) because that's the
   shape the space actually is, and because an hourly forecast reads
   naturally left-to-right as time passing. */
.weather-strip {
  grid-area: weather;
  display: flex;
  align-items: center;
  gap: 1.6vw;
}

.weather-now {
  flex: none;
  display: flex;
  align-items: center;
  gap: 0.8vw;
}

.weather-now-icon {
  font-size: 5.5vh;
  line-height: 1;
}

.weather-now-temp {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 4.6vh;
  line-height: 1;
}

.weather-now-desc {
  font-size: 2vh;
  color: var(--color-text-muted);
  margin-top: 0.3vh;
}

.rain-gear {
  font-size: 1.9vh;
  font-weight: 700;
  margin-top: 0.4vh;
  color: var(--color-led-amber);
}

.weather-sun {
  flex: none;
  font-size: 1.9vh;
  color: var(--color-text-muted);
  display: flex;
  flex-direction: column;
  gap: 0.6vh;
  border-left: 1px solid rgba(255, 255, 255, 0.12);
  padding-left: 1.2vw;
}

.sun-icon {
  font-size: 0.9em;
}

.weather-hours {
  flex: 1;
  min-width: 0;
  display: flex;
  justify-content: space-between;
  gap: 0.8vw;
  border-left: 1px solid rgba(255, 255, 255, 0.12);
  padding-left: 1.2vw;
}

.weather-hours li {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2vh;
}

.hour-label {
  font-size: 1.8vh;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

.hour-icon {
  font-size: 2.6vh;
  line-height: 1.1;
}

.hour-temp {
  font-size: 2.2vh;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Only shown when it's worth showing - a 0% row of "0%" across eight
   columns is noise. See renderWeather in app.js. */
.hour-precip {
  font-size: 1.7vh;
  color: #6ba9e0;
  font-variant-numeric: tabular-nums;
  min-height: 1.7vh;
}

/* ---------- News ticker ---------- */

.ticker {
  grid-area: ticker;
  display: flex;
  align-items: center;
  gap: 1.2vw;
}

.ticker-badge {
  flex: none;
  background: #bb1919; /* BBC red, for recognisability - not an official BBC asset, just the same convention every rolling-news ticker uses */
  color: #ffffff;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 2vh;
  padding: 0.7vh 1vw;
  border-radius: 0.4vh;
}

.ticker-viewport {
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

/* Two .ticker-text copies side by side (index.html), animated together.
   Seamless loop: -50% of the pair's own width is exactly one copy's width,
   so the moment the first copy exits left, the second is already sitting
   where the first started - no gap, unlike a single copy sliding fully off
   and back on (calling-at-scroll below), which is fine for a short per-row
   string but leaves a long empty stretch for a string this much longer
   than the viewport. */
.ticker-track {
  display: flex;
  width: max-content;
  animation: ticker-loop linear infinite;
}

.ticker-text {
  display: inline-block;
  white-space: nowrap;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 2.6vh;
  color: var(--color-text);
}

@keyframes ticker-loop {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* ---------- Narrow screens: the phone board layout ---------- */

/* Keyed to screen width, not to which URL was opened. The main dashboard
   on a phone is the same shape problem as /mini, and it was broken there
   for exactly as long as this was gated on `body.mini`: at 412px the TV's
   column widths need 484px in a 387px panel, so Plat sat 51px off the
   right edge and the countdown 130px off it, with no scrollbar to admit
   either.
   
   The trimming that makes /mini a *shortcut* rather than a layout - no
   ticker, no hourly forecast, no clock - stays under `body.mini` further
   down. This block is only about making the boards fit. */
@media (max-width: 560px) {
  body {
    overflow-x: hidden;
    /* Every horizontal dimension on the boards derives from this one
       value, so the whole row scales together. It starts as a
       width-proportional guess and is then corrected against what
       actually rendered - see fitBoardRows in app.js. A guess alone
       isn't enough: if the webfont fails to load, or the browser
       inflates text, the fixed-size character cells stay put while the
       countdown and labels grow, and the row spills off the side. */
    --mini-cell: 4.3vw;
  }

  .trains,
  .buses {
    display: block;
  }

  .boards-stack {
    display: block;
  }

  .retro-board-col {
    min-height: 0;
  }

  .retro-board-col--return {
    margin-top: 2.5vh;
    padding-top: 2vh;
  }

  .retro-rows {
    flex: none;
    min-height: auto;
    overflow: visible;
  }

  .leave-by-row {
    margin-top: 0;
  }

  .retro-board-title {
    font-size: 4.4vw;
  }

  .retro-board-col .route {
    font-size: 3.4vw;
  }

  /* Two lines per service rather than one. Five columns across a phone
     forces ~13px glyphs; stacking arrival and platform underneath buys
     back enough width for ~20px, and there is vertical room going spare
     once each board simply scrolls. Time, status and the countdown - the
     three things you opened it for - stay together on the first line. */
  .retro-row,
  .retro-row--buses {
    grid-template-columns: auto auto minmax(0, 1fr);
    column-gap: calc(var(--mini-cell) * 0.58);
    row-gap: 0.4vh;
    padding: 1.1vh 0;
  }

  .flap-time {
    grid-column: 1;
    grid-row: 1;
  }

  .flap-status,
  .flap-line {
    grid-column: 2;
    grid-row: 1;
  }

  .time-until {
    grid-column: 3;
    grid-row: 1;
    justify-self: end;
    padding-left: 0;
    font-size: calc(var(--mini-cell) * 0.98);
  }

  .flap-arr {
    grid-column: 1;
    grid-row: 2;
  }

  .flap-plat {
    grid-column: 2;
    grid-row: 2;
  }

  /* The header row labels a five-column layout that no longer exists. */
  .retro-columns {
    display: none;
  }

  /* Only three tracks here, so the narrowed spans used on the TV would
     reach past the end of the grid. A phone has no width to spare
     anyway - let it run the full row. */
  .retro-row .calling-at,
  .retro-row--buses .calling-at {
    grid-column: 1 / -1;
  }

  .flap-char {
    width: var(--mini-cell);
    height: calc(var(--mini-cell) * 1.53);
  }

  .flap-static {
    font-size: calc(var(--mini-cell) * 1.14);
  }

  /* With the column header row gone, "18:14  1" on the second line needs
     to say what it is. Switching the field from inline-grid to inline-flex
     lets a ::before label sit alongside the character cells instead of
     becoming another cell in their track. */
  .flap-arr,
  .flap-plat {
    display: inline-flex;
    align-items: center;
    gap: calc(var(--mini-cell) * 0.23);
  }

  .flap-arr::before {
    content: "ARR";
  }

  .flap-plat::before {
    content: "PLAT";
  }

  .flap-arr::before,
  .flap-plat::before {
    font-size: calc(var(--mini-cell) * 0.65);
    font-weight: 700;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
  }

  /* An empty row holds the board's shape on the TV while the next
     departure loads. Here the boards flow rather than filling a fixed
     panel, so it contributes nothing but a stray pair of ARR and PLAT
     labels with no values beside them - the labels are pseudo-elements on
     the fields, and render whether or not the field has any characters. */
  .retro-row.empty {
    display: none;
  }

  .retro-row .reason,
  .calling-at-text {
    font-size: 3.2vw;
  }

  .leave-by-label {
    font-size: 3.6vw;
  }

  .leave-by {
    font-size: 4vw;
  }

  .leave-by-row {
    gap: 4vw;
    padding-top: 1.2vh;
    /* The "for HH:MM" labels push this past a phone's width - let it take
       a second line rather than clipping a leave-by time. */
    flex-wrap: wrap;
    row-gap: 0.6vh;
  }

  .service-message,
  .panel-status {
    font-size: 3.2vw;
  }
}

/* ---------- Mini: what /mini drops ---------- */

/* Purely about content, not layout - the boards above already fit by the
   time any of this applies. /mini is the ten-second check before leaving
   the house, so it keeps current conditions and the three boards and
   drops everything that earns its place on a screen you glance at all
   day. */
body.mini .safe-area {
  padding: 2vh 3vw;
}

body.mini .grid {
  grid-template-areas:
    "header"
    "weather"
    "trains"
    "buses";
  grid-template-columns: 1fr;
  grid-template-rows: auto;
  row-gap: 1.6vh;
}

/* The phone puts the time in its own status bar, so the big clock would be
   the second copy on screen. The date and Sydney stay - neither is up
   there. */
body.mini .clock-block--primary .clock {
  display: none;
}

body.mini .date-line {
  font-size: 4.4vw;
  margin-top: 0;
}

body.mini .clock-block--secondary .clock {
  font-size: 5vw;
}

body.mini .city-label,
body.mini .sydney-temp {
  font-size: 3.2vw;
}

/* Current conditions only - temperature, outlook, today's high and low,
   and the umbrella if one is wanted. Sticky, so it stays put while the
   three boards scroll past it. */
body.mini .weather-strip {
  position: sticky;
  top: 0;
  z-index: 2;
  gap: 3vw;
}

body.mini .weather-hours,
body.mini .weather-sun,
body.mini .ticker,
body.mini .rain-gear-text {
  display: none;
}

body.mini .weather-now-icon {
  font-size: 9vw;
}

body.mini .weather-now-temp {
  font-size: 8vw;
}

body.mini .weather-now-desc {
  font-size: 3.4vw;
}

/* Pulled out to the right-hand end of the pane. It lives inside the text
   column in the DOM (where the full sentence belongs on the TV), so here
   it's positioned against the strip instead of reflowing onto a line of
   its own beneath the outlook. */
body.mini .rain-gear {
  position: absolute;
  right: 4vw;
  top: 50%;
  transform: translateY(-50%);
  margin: 0;
  font-size: 9vw;
}

/* Which departure a leave-by time actually gets you. Two modes with
   different lead times routinely land on different services, and then the
   slower one shows the later time - walk 18:32 beside drive 18:15 on a
   board headed 18:36. Both correct, since the walk is for the 19:06, but
   unreadable without saying so. */
.leave-by-for {
  font-size: 0.72em;
  font-weight: 400;
  color: var(--color-text-muted);
  margin-left: 0.3vw;
}
