/* Hallmark - fixed-viewport wallboard (no scroll, no hero, no nav, no footer;
 *   none of the catalogue macrostructures apply, so none is claimed)
 * genre: utilitarian - theme: custom
 * axes: dark paper - johnston-derived display - cool accent
 * pre-emit critique: P5 H5 E4 S5 R4 V5
 * contrast: every text pair >= 4.5:1, verified by test/contrast.test.js
 *
 * Two instruments bolted together and joined by a Victoria-line seam: the
 * network at a glance on the left, the one decision you are making on the
 * right. Tokens live in tokens.css; nothing here inlines a colour or a font.
 * Sized in container units so it fits a phone or a 65-inch wallboard without
 * a breakpoint per size.
 */

*,
*::before,
*::after { box-sizing: border-box; }

html,
body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: var(--tunnel);
  color: var(--ink);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

:focus-visible {
  outline: 3px solid var(--seam);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------ shell */

/* Oversized by the drift distance and pulled back, so the slow shift below
   never exposes an edge. */
.board {
  display: grid;
  grid-template-columns: minmax(0, 37fr) 6px minmax(0, 63fr);
  width: calc(100% + 4px);
  height: calc(100dvh + 4px);
  margin: -2px;
  animation: burn-in-drift 1800s linear infinite;
}

/*
 * Two pixels, once every half hour. A wallboard shows the same layout in the
 * same place for years, and a static high-contrast edge is exactly what marks
 * a panel permanently. This is imperceptible to a reader -- it is not motion
 * design, it is looking after the hardware -- so it runs under
 * prefers-reduced-motion too.
 */
@keyframes burn-in-drift {
  0% { transform: translate(0, 0); }
  25% { transform: translate(2px, 1px); }
  50% { transform: translate(0, 2px); }
  75% { transform: translate(-2px, 1px); }
  100% { transform: translate(0, 0); }
}

.seam { background: var(--seam); }

.panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.panel--network { background: var(--tunnel); }
.panel--departures { background: var(--rail); }

.panel__head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 1.4vh;
  padding: 1.6vh 1.8vh;
  border-bottom: 1px solid var(--tunnel-rule);
}

.panel--departures .panel__head { border-bottom-color: var(--rail-rule); }

.panel__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(17px, 3vh, 46px);
  letter-spacing: 0.005em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.roundel {
  flex: 0 0 auto;
  height: clamp(20px, 3.6vh, 54px);
  width: auto;
}

.doublearrow {
  flex: 0 0 auto;
  height: clamp(16px, 2.6vh, 38px);
  width: auto;
  color: var(--ink);
}

.summary {
  margin: 0 0 0 auto;
  font-size: clamp(12px, 1.9vh, 28px);
  font-weight: 500;
  color: var(--ink-3);
  white-space: nowrap;
}

.summary[data-worst='good'] { color: var(--good); }
.summary[data-worst='minor'] { color: var(--minor); }
.summary[data-worst='severe'] { color: var(--severe); }
.summary[data-worst='closed'] { color: var(--closed); }

.clock {
  margin-left: auto;
  font-size: clamp(20px, 4vh, 62px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.01em;
}

.clock__sep { animation: tick var(--dur-tick) var(--step-tick) infinite; }

@keyframes tick {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0.25; }
}

.panel__body {
  flex: 1 1 auto;
  min-height: 0;
  container-type: size;
  display: flex;
  flex-direction: column;
}

.panel__foot {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 2vh;
  padding: 1vh 1.8vh;
  border-top: 1px solid var(--tunnel-rule);
  font-size: clamp(10px, 1.55vh, 21px);
  color: var(--ink-3);
  min-height: 0;
}

.panel--departures .panel__foot { border-top-color: var(--rail-rule); }

.freshness {
  flex: 0 0 auto;
  white-space: nowrap;
}

.freshness[data-state='stale'] { color: var(--minor); }
.freshness[data-state='down'] { color: var(--severe); }

.source {
  margin-left: auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --------------------------------------------------------------- network */

.lines {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;

  /* Sized against how many lines are actually shouting. One disruption gets a
     row you can read across a room; fifteen still fit. --alerts comes from JS. */
  /* Constrained by height and width both. Height alone let three alerts on a
     laptop grow past the point where "Hammersmith & City" plus its status
     still fit the panel, and the name silently truncated. */
  font-size: clamp(13px, min(calc(25cqh / max(var(--alerts, 3), 3)), 6.2cqw), 40px);
}

.line {
  /* Grows into spare height so a single disruption is not a thin strip at the
     top, but capped so three do not become billboards. */
  flex: 1 1 auto;
  max-height: 4.4em;
  min-height: 0;
  overflow: hidden;
  display: grid;
  grid-template-columns: clamp(8px, 1.5cqw, 18px) minmax(0, 1fr) auto;
  grid-template-areas:
    'bar name state'
    'bar reason reason';
  align-content: center;
  align-items: baseline;
  column-gap: 1.6cqw;
  padding: 0.5em 3cqw 0.5em 0;
  border-bottom: 1px solid var(--tunnel-rule);
}

.line--alert { background: var(--tunnel-lift); }

.line__bar {
  grid-area: bar;
  align-self: stretch;
  background: var(--line, var(--closed));
}

/* Northern is black; on a dark ground it needs an edge to exist at all. */
.line--dark .line__bar {
  outline: 2px solid var(--bar-outline);
  outline-offset: -2px;
}

.line__name {
  grid-area: name;
  font-family: var(--font-display);
  font-weight: 400;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.line__state {
  grid-area: state;
  /* Subordinate to the name: the line is what you look for, the status is what
     you then read. It also buys back the width that "Hammersmith & City" plus
     "Part suspended" needs on a smaller screen. */
  font-size: 0.82em;
  font-weight: 500;
  white-space: nowrap;
  color: var(--ink-3);
}

.line--minor .line__state { color: var(--minor); font-weight: 600; }
.line--severe .line__state { color: var(--severe); font-weight: 600; }
.line--closed .line__state { color: var(--closed); font-weight: 600; }

/* One line that creeps sideways, the way a real CIS board reads a reason out.
   A single line always fits the row, so nothing is ever clipped vertically. */
.line__reason {
  grid-area: reason;
  margin: 0.1em 0 0;
  font-size: 0.68em;
  line-height: 1.3;
  color: var(--ink-2);
}

/* ------------------------------------------------- lines with nothing to say --
   TfL's own status page lists what is wrong and collapses the rest into one
   line. On a wall-mounted TV that is the difference between twenty names too
   small to read and three you can read from the far side of the room. The
   names stay visible, because "all other lines" is no use if you cannot tell
   whether yours is among them. */

.allclear {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* The block owns the whole lower zone and centres its content in it. Pinning
     to the bottom instead leaves the surplus as a band between the last alert
     and the heading, which reads as a gap rather than as layout. */
  justify-content: center;
  gap: 0.8cqh;
  padding: 1.4cqh 1.8cqw;
  overflow: hidden;
}

.allclear:not(:first-child) { border-top: 2px solid var(--tunnel-rule); }

/* Nothing is wrong anywhere: the block is the whole panel, so let it breathe. */
.allclear:only-child {
  justify-content: center;
  gap: 2cqh;
}

.allclear:only-child .allclear__head { font-size: clamp(14px, min(3.2vh, 5.5vw), 40px); }

.allclear:only-child .allclear__lines {
  font-size: max(12px, calc(clamp(12px, min(2.7vh, 4.6vw), 34px) * var(--chip-scale, 1)));
}

.allclear__head {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(12px, min(2.4vh, 4.2vw), 30px);
  color: var(--ink-2);
}

.allclear__lines {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 0 1 auto;
  min-height: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5em 1.4em;
  /* --chip-scale is stepped down by fitChips() when the names would not
     otherwise fit; nothing is ever silently cropped. */
  /* max() holds a readable floor: shrinking is allowed to make names fit,
     but not to the point of being unreadable -- past that, fitChips drops
     names and says how many. */
  font-size: max(11px, calc(clamp(11px, min(2vh, 3.6vw), 28px) * var(--chip-scale, 1)));
  color: var(--ink);
  overflow: hidden;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  white-space: nowrap;
}

/* Says how many names had to be dropped, so the list never lies by omission. */
.chip--more {
  color: var(--ink-3);
  font-style: normal;
}

.chip--more::before { display: none; }

/* The same colour bar as a full row, shrunk -- one visual language, two sizes. */
.chip::before {
  content: '';
  flex: 0 0 auto;
  width: 0.42em;
  height: 1.1em;
  background: var(--line, var(--closed));
}

.chip--dark::before {
  outline: 1px solid var(--bar-outline);
  outline-offset: -1px;
}

/* ------------------------------------------------------------ departures */

.panel--departures .panel__body { --dep-cols: 8.5cqw minmax(0, 1fr) 8.5cqw 14cqw; }

.colheads {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: var(--dep-cols);
  column-gap: 2cqw;
  padding: 0.9cqh 2cqw 0.9cqh 2cqw;
  font-size: clamp(10px, 2cqh, 22px);
  font-weight: 500;
  color: var(--ink-3);
  border-bottom: 1px solid var(--rail-rule);
}

.colheads > :nth-child(3) { text-align: center; }
.colheads > :nth-child(4) { text-align: right; }

.services {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 auto;
  min-height: 0;
  --rows: 8;
  display: grid;
  grid-template-rows: repeat(var(--rows), minmax(0, 1fr));
  font-size: clamp(11px, min(calc(36cqh / var(--rows)), 3.6cqw), 40px);
}

.svc {
  min-height: 0;
  display: grid;
  grid-template-columns: var(--dep-cols);
  grid-template-areas:
    'time dest plat eta'
    'time meta plat due';
  align-content: center;
  align-items: center;
  column-gap: 2cqw;
  padding: 0 2cqw;
  border-bottom: 1px solid var(--rail-rule);
}

.svc__time {
  grid-area: time;
  align-self: center;
  font-size: 1.12em;
  font-weight: 600;
  line-height: 1.05;
}

.svc__dest {
  grid-area: dest;
  font-size: 1.28em;
  font-weight: 600;
  line-height: 1.1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.svc__meta {
  grid-area: meta;
  display: flex;
  align-items: center;
  gap: 1.5em;
  margin-top: 0.15em;
  font-size: 0.62em;
  line-height: 1.2;
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
}

/* Operator shown as a colour flash, echoing the line bars opposite. */
.op {
  flex: 0 0 auto;
  padding-left: 0.5em;
  border-left: 0.2em solid var(--op, var(--op-default));
  color: var(--ink-2);
}

.svc__via {
  flex: 0 0 auto;
}

.svc__progress {
  flex: 0 0 auto;
  color: var(--ink-2);
}

/* ---------------------------------------------------------------- marquee --
   Anything that can outrun its box scrolls instead of being cut off: a
   disruption reason, a delay explanation, a National Rail message. The track
   only animates when it genuinely overflows -- a still line is calmer -- and
   it dwells at each end so the beginning and end can actually be read.
   JS sets --overflow (px) and --creep (seconds) from the measured distance,
   so every marquee moves at the same speed whatever its length. */

.marquee {
  display: block;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}

.marquee__track {
  display: inline-block;
  will-change: transform;
}

.marquee__track.is-scrolling {
  animation: creep var(--creep, 12s) ease-in-out infinite alternate;
}

@keyframes creep {
  0%, 14% { transform: translateX(0); }
  86%, 100% { transform: translateX(calc(-1px * var(--overflow, 0))); }
}

.svc__progress[data-imminent='true'] {
  color: var(--good);
  font-weight: 600;
}

.svc__detail {
  flex: 0 1 auto;
  min-width: 0;
}

.svc__plat {
  grid-area: plat;
  justify-self: center;
  align-self: center;
  min-width: 2.1em;
  padding: 0.06em 0.22em;
  border-radius: 2px;
  background: var(--enamel);
  color: var(--enamel-ink);
  font-size: 1.45em;
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
}

.svc__plat[data-platform='none'] {
  background: transparent;
  color: var(--ink-3);
  border: 2px solid var(--plat-none-rule);
  font-weight: 500;
}

.svc__eta {
  grid-area: eta;
  justify-self: end;
  align-self: end;
  font-size: 1.05em;
  font-weight: 600;
  line-height: 1.05;
  white-space: nowrap;
}

/* Sits under the expected time, quieter than it: the clock time is the fact,
   the countdown is the convenience. */
.svc__due {
  grid-area: due;
  justify-self: end;
  align-self: start;
  font-size: 0.62em;
  line-height: 1.2;
  color: var(--ink-3);
  white-space: nowrap;
}

.svc--ontime .svc__eta { color: var(--ink); }
.svc--late .svc__eta,
.svc--delayed .svc__eta { color: var(--amber); }

.svc--platchanged .svc__plat { background: var(--amber); }

.svc--cancelled .svc__time {
  color: var(--ink-3);
  text-decoration: line-through;
  text-decoration-thickness: 0.06em;
}

.svc--cancelled .svc__dest { color: var(--ink-2); }
.svc--cancelled .svc__eta { color: var(--cancel); }

.svc--cancelled .svc__plat {
  background: var(--enamel-off);
  color: var(--enamel-off-ink);
}

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

.ticker {
  flex: 1 1 auto;
  color: var(--amber);
}

/* -------------------------------------------------------- empty & errors */

.notice {
  margin: auto;
  padding: 2cqh 3cqw;
  font-size: clamp(13px, 3cqh, 30px);
  color: var(--ink-3);
  text-align: center;
}

.notice--error { color: var(--severe); }

/* ------------------------------------------------------------ responsive */

@media (max-aspect-ratio: 1 / 1), (max-width: 700px) {
  .board {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 38fr) 6px minmax(0, 62fr);
  }

  .panel--departures .panel__body { --dep-cols: 15cqw minmax(0, 1fr) 13cqw 21cqw; }

  .lines { font-size: clamp(12px, calc(22cqh / max(var(--alerts, 3), 3)), 26px); }
  .services { font-size: clamp(11px, min(calc(34cqh / var(--rows)), 4.4cqw), 30px); }
  .source { display: none; }
}

@media (max-width: 560px) {
  .panel__head { gap: 1vh; padding: 1.2vh 1.2vh; }
  .summary { font-size: clamp(11px, 1.6vh, 16px); }
  .svc__meta { gap: 0.9em; }
}

@media (prefers-reduced-motion: reduce) {
  .clock__sep { animation: none; }

  .marquee__track.is-scrolling {
    animation: none;
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
