/* ============================================================
   PROJECT YAMANOTE — zen night-Tokyo theme
   ============================================================ */

:root {
  color-scheme: dark;
  --bg: #080b09;
  --panel-bg: #090d0a;
  --card: #0e1410;
  --card-lcd: #050705;
  --edge: #223019;
  --edge-soft: #1b2616;

  --ink: #e6ede0;
  --ink-bright: #f6faf0;
  --ink-mid: #b0bfa7;
  --ink-dim: #9aa893;
  --ink-faint: #7f8d78;
  --ink-ghost: #5f6d59;

  --uguisu: #9acd32;          /* Yamanote green */
  --uguisu-soft: #b7dd6e;
  --uguisu-pale: #d3ef9a;
  --uguisu-dim: #5d8a1f;
  --amber: #e8b44a;
  --warn: #e8896a;

  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

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

/* `hidden` must beat the display rules below, which would otherwise
   override the attribute's default display:none */
[hidden] { display: none !important; }

html, body { height: 100%; }

body {
  background: radial-gradient(1200px 800px at 30% 20%, #0d130e 0%, var(--bg) 65%);
  color: var(--ink);
  font-family: 'Zen Kaku Gothic New', 'Hiragino Kaku Gothic ProN',
    'Noto Sans JP', 'Yu Gothic', system-ui, sans-serif;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }

button { font-family: inherit; }
::selection { background: var(--uguisu); color: var(--bg); }
:focus-visible { outline: 2px solid var(--uguisu-pale); outline-offset: 4px; }

/* ---------------- header ---------------- */

/* Six things live in this row and the window is not always wide. Left
   unchecked it simply overflowed — measured at 1024px it wanted 1232,
   which put the fullscreen control and everything near it 208px past
   the right edge, off screen entirely. So: it wraps rather than
   overflows, and the pieces give ground in order of how much they
   matter (labels before glyphs, the tagline before the name). */
header {
  /* Keep the clock and title off the animated SVG's paint surface. On a
     DPR-2 board the shared root repainted on every map frame; this small,
     stable layer only needs to paint when the header itself changes. */
  transform: translateZ(0);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--edge-soft);
  flex: 0 0 auto;
  flex-wrap: wrap;
}

.brand { display: flex; align-items: center; gap: 14px; }

.brand-mark {
  width: 26px; height: 26px;
  border: 5px solid var(--uguisu);
  border-radius: 50%;
  box-shadow: 0 0 16px rgba(154, 205, 50, 0.45);
}

.brand-text { display: flex; flex-direction: column; gap: 3px; }

.brand-text h1 {
  font-size: 16px;
  letter-spacing: 0.3em;
  font-weight: 700;
  color: #f1f6ea;
  white-space: nowrap;
}

.brand-sub { font-size: 12.5px; color: var(--ink-dim); letter-spacing: 0.1em; }

.head-right {
  display: flex; align-items: center; gap: 14px;
  /* the backstop: if it still cannot fit, it stacks at the right edge
     instead of walking off it */
  flex-wrap: wrap; justify-content: flex-end;
  margin-left: auto;
}

.status-pill {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 14px 6px 11px;
  border: 1px solid #2a3a20;
  border-radius: 999px;
  background: var(--card);
}

.status-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--amber);
  box-shadow: 0 0 8px rgba(232, 180, 74, 0.6);
}

.status-pill.live .status-dot {
  background: #7fe07f;
  box-shadow: 0 0 8px rgba(127, 224, 127, 0.6);
}

.status-label {
  font-size: 12px; font-weight: 500; letter-spacing: 0.16em;
  color: var(--amber);
}

.status-pill.live .status-label { color: #7fe07f; }

.pill-rule { width: 1px; height: 14px; background: #2a3a20; }

.pill-count {
  font-size: 12px; color: #b9c7b0;
  font-family: var(--mono); white-space: nowrap;
}

.note-pill {
  font-size: 12px; letter-spacing: 0.1em; color: #e8a3a3;
  border: 1px solid #4a2828; background: #1a1010;
  border-radius: 8px; padding: 6px 12px; white-space: nowrap;
}

.clock-block { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }

/* the board takes the whole wall on request — a header control, like
   the clock it sits beside. The panel fold sits with it: same family,
   same spot, complementary job. */
#btn-panel,
#btn-fullscreen {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 13px;
  border: 1px solid #2a3a20;
  border-radius: 999px;
  background: var(--card);
  color: var(--ink-dim);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease;
}

#btn-panel:hover,
#btn-panel:focus-visible,
#btn-fullscreen:hover,
#btn-fullscreen:focus-visible {
  color: var(--uguisu-soft);
  border-color: var(--uguisu-dim);
}

/* folded away: the control stays lit so the way back is obvious */
#btn-panel.on {
  color: var(--uguisu-soft);
  border-color: var(--uguisu-dim);
}

#fs-glyph, #panel-glyph { font-size: 14px; line-height: 1; }

#clock {
  font-size: 26px; line-height: 1;
  color: var(--uguisu-pale);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

.clock-sub { font-size: 11px; letter-spacing: 0.24em; color: var(--ink-faint); }

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

/* Scope the map/video stack below the header. Do not isolate train-window:
   its backdrop and controls must still straddle the map's z-index. */
main { flex: 1 1 auto; display: flex; min-height: 0; isolation: isolate; }

/* the panel folded away: the map and its window take the whole width.
   display:none (not width:0) so nothing inside keeps layout, focus or
   scroll — the board is genuinely alone on the wall. */
body.panel-hidden #panel { display: none; }

#map-wrap {
  flex: 1 1 auto;
  min-width: 0;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}

#map-stage {
  position: relative;
  z-index: 10;                     /* above the train-window backdrop */
  height: 100%;
  aspect-ratio: 760 / 1080;
  max-width: 100%;
}

#map { position: absolute; inset: 0; width: 100%; height: 100%; }

#labels { position: absolute; inset: 0; pointer-events: none; }

/* THE HEART'S OWN SIGNAGE (owner design, 2026-08-28). While you ride,
   the top of the loop's quiet interior carries the ride itself, as a
   centred stack in the board's own voices: the hop being filmed in
   the strip's amber LED, whose loop you are on in tracked mono, and —
   the centrepiece — the hop's destination over its JY marker, drawn
   the way the real line draws station numbering: name above, numbered
   roundel below. It takes no pointer (the click that steps you off
   passes through it). The old tap-to-step-off hint and the standing
   山手線 / YAMANOTE LINE / IN SERVICE centre text are both retired by
   owner directive; while riding this stack is the only voice. Idle,
   the invitation streams once in the space between the loop and panel. */
#interior-idle {
  position: absolute; z-index: 11;
  width: 256px; max-width: calc(100% - 32px);
  display: grid; text-align: center;
  color: var(--uguisu-pale); font: 14px/1.8 var(--mono); letter-spacing: 0.04em;
  text-shadow: 0 0 5px rgba(183, 221, 110, 0.45), 0 0 16px rgba(154, 205, 50, 0.25);
  pointer-events: none;
}
#interior-idle[data-repositioning="true"] { visibility: hidden; }
.invite-measure, .invite-stream { grid-area: 1 / 1; white-space: pre-line; }
.invite-measure { visibility: hidden; }
.invite-line { display: block; min-height: 1.8em; }
.invite-line[lang="ja"] {
  font-family: 'Zen Kaku Gothic New', 'Yu Gothic', sans-serif;
  font-size: 15px; letter-spacing: 0.02em;
}
.invite-stream .invite-line::after {
  content: ''; display: inline-block; width: 1px; height: 1em;
  margin-left: 2px; vertical-align: -0.12em; background: currentColor;
  opacity: 0; transition: opacity 250ms ease;
}
#interior-idle[data-streaming="true"] .invite-stream .invite-line::after { opacity: 0.85; }
#map-wrap.invitation-footer { padding-bottom: 132px; }
@media (prefers-reduced-motion: reduce) {
  .invite-stream .invite-line::after { transition: none; }
}
#interior-ride {
  position: absolute;
  /* one centimetre off the rails (owner directive, 2026-08-29: the
     stack sat too close to the ring's crown and 巣鴨's label) —
     38px ≈ 1cm at CSS 96dpi, absolute on purpose so the breathing
     room holds at every board size */
  left: var(--loop-cx-ride, 50%); top: calc(16.5% + 38px);
  transform: translateX(-50%);
  pointer-events: none;
  display: flex; flex-direction: column; align-items: center; gap: 13px;
  text-align: center;
  white-space: nowrap;
  animation: ir-in 1.2s ease both;
}

@keyframes ir-in { from { opacity: 0; } to { opacity: 1; } }

/* the hop, in the same amber LED voice as the band's strip, ruled
   off underneath like a departure board's header line. Sized for the
   WORST leg, not the average one (owner directive, 2026-08-29): at
   27px, 高輪ゲートウェイ → 品川 lay across the rails — 20px seats
   every leg on the loop inside the ring's top with air to spare, and
   the destination carries the emphasis instead of sheer size. */
.ir-leg {
  padding-bottom: 10px;
  border-bottom: 1px solid rgba(232, 180, 74, 0.32);
  /* the hop's two scripts ride as one column with the destination's
     roundel standing the full height of both at their right hand
     (owner directive, 2026-08-29: as tall as SHINAGAWA and the kanji
     above it — flex stretch, so the lines set the roundel's height) */
  display: flex; align-items: stretch; justify-content: center; gap: 11px;
}
.ir-leg-lines {
  display: flex; flex-direction: column; justify-content: center;
}
.ir-leg-ja {
  font-size: 20px; font-weight: 700; letter-spacing: 0.08em;
  color: #ffd27d;
  text-shadow: 0 0 6px var(--bg), 0 0 18px rgba(232, 180, 74, 0.35);
}

.ir-leg-en {
  font-size: 11px; letter-spacing: 0.16em; color: #c8a869;
  font-family: var(--mono); margin-top: 3px;
  text-shadow: 0 0 5px var(--bg), 0 0 5px var(--bg);
}


/* THE DESTINATION, LIT (owner directive, 2026-08-29): the featured
   station name moved out of its own block and INTO the hop itself —
   the Y of X → Y burns white with the uguisu halo the old centrepiece
   wore, underlined like a platform sign's next-stop marker, in both
   scripts. The X stays in the amber LED voice, so the eye reads the
   hop and lands where the train will. */
#ir-leg-to {
  color: #ffffff;
  text-shadow:
    0 0 6px var(--bg), 0 0 6px var(--bg), 0 0 3px var(--bg),
    0 0 20px rgba(154, 205, 50, 0.55);
  text-decoration: underline;
  text-decoration-color: rgba(154, 205, 50, 0.85);
  text-decoration-thickness: 2px;
  text-underline-offset: 5px;
}

#ir-leg-to-en {
  color: #f0ead2;
  text-decoration: underline;
  text-decoration-color: rgba(154, 205, 50, 0.55);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.ir-loop {
  /* tracked wide enough to read as signage, tight enough to stay off
     the rails — at 0.3em it ran edge to edge into 上野's label */
  font-size: 12.5px; letter-spacing: 0.22em; color: var(--uguisu-soft);
  font-family: var(--mono);
  text-shadow: 0 0 5px var(--bg), 0 0 5px var(--bg);
}

/* the destination's own number, at the destination's right hand
   (owner directive, 2026-08-29, remade same day: the line-sized chip
   was too slight — the roundel stands as tall as both scripts of the
   hop together, JY stacked over the number the way JR East's own
   station signs draw it). Height comes free from the flex stretch. */
.ir-next-jy {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; row-gap: 1px;
  border: 2px solid var(--uguisu); color: var(--uguisu-pale);
  border-radius: 8px; font-weight: 700;
  padding: 2px 8px; font-family: var(--mono);
  background: rgba(8, 12, 8, 0.55);
  box-shadow: 0 0 12px rgba(154, 205, 50, 0.3),
    inset 0 0 7px rgba(154, 205, 50, 0.1);
  white-space: nowrap;
}
.ir-jy-line { font-size: 11px; letter-spacing: 0.12em; line-height: 1; }
.ir-jy-num { font-size: 19px; letter-spacing: 0.03em; line-height: 1; }

/* ---------------- the panel fold ---------------- */

/* A tab on the seam. Sits at the right edge of the board — against the
   panel's border while the panel is open, against the wall once it is
   folded away — and is the only control in #map-wrap that takes a
   pointer, so it has to sit above the film (z 5) and the stage (z 10). */
#panel-handle {
  position: absolute;
  right: 0; top: 50%;
  transform: translateY(-50%);
  z-index: 14;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 15px 6px;
  border: 1px solid #2a3a20;
  border-right: none;
  border-radius: 8px 0 0 8px;
  background: var(--card);
  color: var(--ink-dim);
  cursor: pointer;
  pointer-events: auto;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}

#panel-handle:hover,
#panel-handle:focus-visible {
  color: var(--uguisu-soft);
  border-color: var(--uguisu-dim);
}

#panel-handle-glyph { font-size: 15px; line-height: 1; }

/* one column of vertical type, the way a platform sign runs: パネル
   upright, PANEL laid on its side beneath it */
.handle-text {
  writing-mode: vertical-rl;
  text-orientation: mixed;
  line-height: 1;
}

.handle-ja { font-size: 11px; letter-spacing: 0.2em; }

.handle-en {
  font-size: 9px; letter-spacing: 0.24em;
  font-family: var(--mono); color: var(--ink-ghost);
  margin-top: 7px;
}

/* folded away: the handle stays lit and points back toward the panel */
body.panel-hidden #panel-handle {
  color: var(--uguisu-soft);
  border-color: var(--uguisu-dim);
  border-right: 1px solid var(--uguisu-dim);
  border-radius: 8px;
  right: 6px;
}

#panel {
  flex: 0 0 400px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px;
  border-left: 1px solid var(--edge-soft);
  overflow-y: auto;
  background: var(--panel-bg);
}

.card {
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 16px;
  padding: 18px 20px;
}

.section-label { font-size: 11.5px; letter-spacing: 0.2em; color: var(--ink-faint); }

/* ---------------- map: station labels ---------------- */

.st-label {
  position: absolute;
  white-space: nowrap;
  line-height: 1.2;
}

/* Ordinary stops sit back a little so the six 主要駅 can carry the map:
   quieter ink, lighter weight, smaller type. */
.st-label .ja {
  font-size: 13.5px;
  font-weight: 500;
  color: #c6d4bd;
  letter-spacing: 0.04em;
  text-shadow: 0 0 5px var(--bg), 0 0 5px var(--bg), 0 0 2px var(--bg);
}

/* 主要駅 — Tokyo, Ueno, Ikebukuro, Shinjuku, Shibuya, Shinagawa.
   These are the landmarks you orient by, so they are deliberately a
   different weight class rather than a size nudge: nearly twice the type
   size of an ordinary stop, pure white, and lifted off the dark ground
   with a soft uguisu glow. */
.st-label.major .ja {
  font-size: 23.5px;
  font-weight: 700;
  color: #ffffff;
  letter-spacing: 0.06em;
  text-shadow:
    0 0 6px var(--bg), 0 0 6px var(--bg), 0 0 3px var(--bg),
    0 0 18px rgba(154, 205, 50, 0.55);
}

.st-label .en {
  font-size: 9.5px;
  color: var(--ink-dim);
  letter-spacing: 0.14em;
  font-family: var(--mono);
  text-transform: uppercase;
  margin-top: 2px;
  text-shadow: 0 0 5px var(--bg), 0 0 5px var(--bg);
}

.st-label.major .en {
  font-size: 13px;
  font-weight: 500;
  color: var(--uguisu-soft);
  letter-spacing: 0.2em;
  margin-top: 3px;
  text-shadow: 0 0 6px var(--bg), 0 0 6px var(--bg), 0 0 3px var(--bg);
}

/* the station the ridden train is heading for */
.st-label.focus .ja { color: var(--uguisu-pale); }
.st-label.major.focus .ja { color: var(--uguisu-pale); }

/* ---------------- map: track and stations ---------------- */

.loop-fill { fill: rgba(154, 205, 50, 0.035); stroke: none; }

.loop-glow {
  fill: none; stroke: var(--uguisu);
  stroke-width: 12; stroke-opacity: 0.08; stroke-linejoin: round;
}

.rail {
  fill: none; stroke: #6f8850;
  stroke-width: 1.6; stroke-opacity: 0.9; stroke-linejoin: round;
  transition: stroke 0.8s ease;
}

.st-dot { fill: #0b0f0b; stroke: var(--uguisu); stroke-width: 1.8; }

/* major stops: fat white-hot ring plus a halo, so the six anchor points
   of the loop are obvious before you read a single name */
.station.major .st-dot {
  stroke: var(--uguisu-pale);
  stroke-width: 4;
  fill: #12190a;
  filter: drop-shadow(0 0 5px rgba(154, 205, 50, 0.65));
}

.st-halo {
  fill: none;
  stroke: var(--uguisu);
  stroke-width: 1.2;
  stroke-opacity: 0.3;
}

.station.focus .st-dot { fill: var(--uguisu-pale); stroke: #ffffff; }

.station.pulse .st-dot { animation: stpulse 1.6s ease-out 1; }

@keyframes stpulse {
  0%   { stroke: #eaffc4; stroke-width: 5; }
  100% { stroke: var(--uguisu); stroke-width: 1.8; }
}

/* majors pulse from and settle back to their own heavier ring — the
   shared keyframe would end by shrinking them to an ordinary stop */
.station.major.pulse .st-dot { animation: stpulse-major 1.6s ease-out 1; }

@keyframes stpulse-major {
  0%   { stroke: #eaffc4; stroke-width: 8.5; }
  100% { stroke: var(--uguisu-pale); stroke-width: 4; }
}

/* Visibility rides on a class, not the opacity attribute: a CSS
   `opacity` here would override the attribute and strand the ring at
   the viewBox origin. */
#target-ring {
  fill: none; stroke: var(--uguisu-pale);
  stroke-width: 1.6;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}

#target-ring.on { opacity: 0.85; }

/* ---------------- map: E235 markers ---------------- */

.train { cursor: pointer; }
.t-hit { fill: transparent; stroke: transparent; stroke-width: 12; vector-effect: non-scaling-stroke; pointer-events: none; }
.train:focus-visible { outline: none; }
.train:focus-visible .t-hit { stroke: var(--uguisu-pale); stroke-width: 2; }
#map.riding .train:focus-visible { opacity: 1; }
@media (pointer: coarse) {
  .t-hit { pointer-events: all; }
}

.t-body { fill: url(#grad-body); stroke: #39434a; stroke-width: 0.45; }

/* door bands: translucent green film over stainless, so they read
   metallic rather than as flat paint */
.t-stripe { fill: url(#grad-stripe); }

.t-windows { fill: #16202a; opacity: 0.78; }

/* flat 切妻 cab caps */
.t-mask { fill: #1b1d1f; }
.t-rear { opacity: 0.9; }

.t-head {
  fill: #fffbdc;
  filter: drop-shadow(0 0 1.6px rgba(255, 247, 190, 0.95));
}

.t-tail-lamp {
  fill: #e2483a;
  filter: drop-shadow(0 0 1.2px rgba(226, 72, 58, 0.8));
}

.t-trail, .t-beam { pointer-events: none; }

.train.dwelling .t-head { opacity: 0.45; filter: none; }
.train.dwelling .t-stripe { animation: doorbreath 2.4s ease-in-out infinite; }

/* only opacity is animated — animating `fill` between a url() gradient
   and anything else is not interpolable and silently does nothing */
@keyframes doorbreath {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.5; }
}

.train:hover .t-body { stroke: #cfe9a0; stroke-width: 0.9; }

/* ---------------- map: the train you're riding ---------------- */

/* Everything else recedes. Nothing else on the board is at full
   strength, so there is no question which train the sound belongs to.
   (0.3, not lower: ghosts must survive sitting on moving footage.) */
.train { transition: opacity 0.35s ease; }
#map.riding .train { opacity: 0.3; }
#map.riding .train:hover { opacity: 0.75; }   /* still findable to switch to */
#map.riding .train.selected { opacity: 1; }

/* the marker itself is scaled up in its transform attribute (see
   renderTrains) — a CSS transform here would have to fight the
   attribute, and `transform-box` on an SVG group is not dependable */
.train.selected .t-body { stroke: #ffffff; stroke-width: 1.3; }
.train.selected { filter: drop-shadow(0 0 11px rgba(190, 255, 110, 0.95)); }
.train.selected .t-windows { fill: #1d3550; opacity: 0.95; }
.train.selected .t-stripe { filter: brightness(1.3); }

.t-ring, .t-halo-sel {
  fill: none;
  stroke: var(--uguisu-pale);
  opacity: 0;
  pointer-events: none;
  vector-effect: non-scaling-stroke;
}

/* steady halo, so the selection reads between pings */
.t-halo-sel { stroke-width: 1; }
.train.selected .t-halo-sel { opacity: 0.32; }

/* Sonar ping. Expansion animates `r` rather than a transform: on an SVG
   circle the transform origin depends on `transform-box` support, and
   getting it wrong throws the ring across the map. `r` has no such
   ambiguity, and where it isn't animatable the opacity pulse still
   reads as a beacon. */
.t-ring { stroke-width: 1.8; }
.train.selected .t-ring { animation: ridering 2.4s ease-out infinite; }
.train.selected .t-ring-late { animation-delay: 1.2s; }

@keyframes ridering {
  0%   { r: 9px;  opacity: 0.9; }
  70%  { opacity: 0.25; }
  100% { r: 27px; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .train.selected .t-ring { animation: none; opacity: 0.45; }
}

/* ---------------- panel: idle ---------------- */

.empty-ja {
  font-size: 19px; font-weight: 500; letter-spacing: 0.06em;
  margin-bottom: 10px; color: #f1f6ea;
}

.empty-en {
  font-size: 14px; color: var(--ink-mid); line-height: 1.75;
  text-wrap: pretty;
}

.empty-stats {
  margin-top: 18px; padding-top: 16px;
  border-top: 1px solid var(--edge-soft);
  display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
}

.stat { display: flex; flex-direction: column; gap: 4px; }

.stat-label {
  font-size: 12px; letter-spacing: 0.14em; color: var(--ink-faint);
  white-space: nowrap;
}

.stat-value { font-size: 20px; color: var(--uguisu-pale); font-family: var(--mono); }

.stat-note {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; gap: 3px;
  font-size: 12.5px; color: var(--ink-faint); letter-spacing: 0.04em;
}

.stat-note .en {
  font-size: 11.5px; color: var(--ink-ghost); font-family: var(--mono);
  letter-spacing: 0.02em;
}

/* ---------------- panel: in-car display ---------------- */

.lcd { background: var(--card-lcd); border-color: #2a3a20; padding: 20px; }

.lcd-head {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: 18px;
}

.line-band {
  background: var(--uguisu); color: #0f1600;
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.06em;
  border-radius: 5px; padding: 5px 12px;
}

#btn-leave {
  min-height: 36px;
  background: #131a11; color: #c9d7c0;
  border: 1px solid #2a3a20; border-radius: 9px;
  font-size: 13px; padding: 8px 14px; cursor: pointer; white-space: nowrap;
}

#btn-leave:hover { background: #1c2617; color: #f1f6ea; }

.lcd-state { display: flex; align-items: baseline; gap: 12px; margin-bottom: 4px; }

#ride-state-ja {
  font-size: 17px; color: var(--uguisu-pale);
  letter-spacing: 0.18em; font-weight: 500;
}

#ride-state-en { font-size: 11.5px; color: #8b9884; letter-spacing: 0.3em; }

#ride-name-ja {
  font-size: 52px; font-weight: 700; line-height: 1.15;
  color: var(--ink-bright); letter-spacing: 0.02em;
}

.lcd-station-sub {
  display: flex; align-items: center; gap: 12px; margin: 6px 0 16px;
}

#ride-name-en {
  font-size: 17px; color: var(--ink-mid);
  letter-spacing: 0.1em; font-family: var(--mono);
}

.jy-badge {
  border: 2px solid var(--uguisu); color: var(--uguisu-pale);
  border-radius: 7px; font-size: 12.5px; font-weight: 700;
  padding: 3px 8px; letter-spacing: 0.04em; font-family: var(--mono);
}

.progress-block {
  display: flex; flex-direction: column; gap: 7px; margin-bottom: 16px;
}

.progress-track {
  height: 6px; border-radius: 3px; background: #16200f; overflow: hidden;
}

#progress-fill {
  height: 100%; width: 100%;
  background: linear-gradient(90deg, var(--uguisu-dim), var(--uguisu-soft));
  border-radius: 3px;
  transform-origin: left center;
  transform: scaleX(0);
  transition: transform 0.3s linear;
}

.progress-meta {
  display: flex; justify-content: space-between;
  font-size: 12.5px; color: #8b9884; font-family: var(--mono);
}

#progress-from { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#progress-eta { color: var(--ink-mid); }

.doors-callout {
  display: flex; align-items: center; gap: 12px;
  background: #1a1408; border: 1px solid #3d3218;
  border-radius: 10px; padding: 12px 14px; margin-bottom: 14px;
}

.doors-badge {
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.1em;
  color: #0f1600; background: var(--amber);
  border-radius: 5px; padding: 4px 9px; white-space: nowrap;
}

.doors-text { font-size: 13.5px; color: #e8c98a; line-height: 1.5; }

#transfers-block { margin-bottom: 16px; }
#transfers-block .section-label { display: block; margin-bottom: 8px; }

.chips { display: flex; flex-wrap: wrap; gap: 7px; }

/* transfer chips read in both scripts: the line as the announcement says
   it, and the name printed on the signage you're looking for */
.chip {
  display: flex; flex-direction: column; gap: 1px;
  background: #16200f; border: 1px solid #2a3a20;
  border-radius: 12px; padding: 5px 11px 6px;
}

.chip-ja { font-size: 12.5px; color: #dbe6d3; letter-spacing: 0.02em; }

.chip-en {
  font-size: 10.5px; color: var(--ink-faint);
  font-family: var(--mono); letter-spacing: 0.06em;
}

.chip-en:empty { display: none; }

.meta-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px;
  border-top: 1px solid var(--edge-soft); padding-top: 14px;
}

.meta { display: flex; flex-direction: column; gap: 3px; }
.meta-label { font-size: 11.5px; letter-spacing: 0.16em; color: var(--ink-faint); }

.meta-label .en {
  font-size: 10px; letter-spacing: 0.14em; color: var(--ink-ghost);
  font-family: var(--mono);
}

.meta-value {
  display: flex; flex-direction: column; gap: 2px;
  font-size: 14px; color: #dbe6d3;
}

/* the English under a value: same colour, stepped back */
.value-en {
  font-size: 11px; font-family: var(--mono); letter-spacing: 0.02em;
  opacity: 0.7;
}

#ride-delay.late { color: var(--warn); }
#ride-delay.ontime { color: var(--uguisu-soft); }

/* ---------------- panel: announcements ---------------- */

.section-head { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; }

.cap-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: #2a3a20;
  transition: background 0.3s ease;
}

#caption.active .cap-dot { background: var(--uguisu); }

/* One sentence per .cap-line: the Japanese, and the English that says
   the same thing sitting directly beneath it, tied together by a rule
   down the left. Reading either language top-to-bottom follows the
   announcement in order. */
#cap-lines { display: flex; flex-direction: column; gap: 12px; }

.cap-line {
  border-left: 2px solid var(--edge);
  padding-left: 11px;
  transition: border-color 0.3s ease;
}

#caption.active .cap-line { border-left-color: var(--uguisu-dim); }

.cap-ja {
  font-size: 16.5px; line-height: 1.6; color: var(--ink-ghost);
  letter-spacing: 0.02em; text-wrap: pretty;
  transition: color 0.3s ease;
}

.cap-en {
  margin-top: 3px;
  font-size: 13px; line-height: 1.55;
  color: #5c6857; text-wrap: pretty;
  transition: color 0.3s ease;
}

.cap-en:empty { display: none; }

#caption.active .cap-ja { color: #f1f6ea; }
#caption.active .cap-en { color: var(--ink-mid); }

/* whichever language is being spoken lights up, line by line */
#cap-lines.speaking-ja .cap-ja,
#cap-lines.speaking-en .cap-en { color: var(--uguisu-pale); }

/* the announcer has finished; the words stay up, stepped back */
#cap-lines.spent { opacity: 0.62; }

/* A chime or a melody is a SOUND, not a sentence — named in small caps
   with a signal glyph, so it never reads as something that was said. */
.cap-line.marker { border-left-style: dotted; }
.cap-line.marker .cap-ja {
  font-size: 14px; letter-spacing: 0.06em; color: var(--uguisu-soft);
}
.cap-line.marker .cap-ja::before { content: '♪ '; opacity: 0.75; }
.cap-line.marker .cap-en {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Who is speaking: the recording, or this board. The card is as honest
   about the words as the window's sill is about the picture. */
.cap-source {
  margin-left: auto;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.13em;
  color: var(--ink-ghost);
  border: 1px solid var(--edge); border-radius: 3px;
  padding: 2px 6px; white-space: nowrap;
  cursor: help;
}

.cap-source.tape {
  color: var(--uguisu-soft);
  border-color: var(--uguisu-dim);
}

/* ---- the cue rail ----
   One tick per intercom event across this leg of the recording, placed
   at its own timestamp, with a playhead riding the film clock. Solid =
   a second measured off the tape; hollow = an estimated offset. */

#cap-track { margin-top: 16px; }

.cap-rail {
  position: relative; height: 16px;
  border-bottom: 1px solid var(--edge);
}

.cap-tick {
  position: absolute; bottom: -1px;
  width: 2px; height: 8px; margin-left: -1px;
  background: var(--uguisu-dim);
  border-radius: 1px;
}

.cap-tick.measured { background: var(--uguisu); height: 11px; }

.cap-tick.marker {
  height: 5px; width: 5px; margin-left: -2.5px; bottom: -3px;
  border-radius: 50%;
  background: none; border: 1px solid var(--uguisu-dim);
}

.cap-tick.marker.measured { background: var(--uguisu); border-color: var(--uguisu); }

.cap-playhead {
  position: absolute; top: 0; bottom: -1px;
  width: 1px; margin-left: -0.5px;
  background: var(--uguisu-pale);
  box-shadow: 0 0 6px var(--uguisu);
  transition: left 0.2s linear;
}

/* What the tape does next, in the card's own idiom: the Japanese, the
   English under it, and the wait on the right — with the second of the
   recording it lands at, a timestamp you could scrub to yourself. */
.cap-coming {
  display: flex; align-items: baseline; gap: 10px;
  margin-top: 8px;
}

.cap-coming-text {
  display: flex; flex-direction: column; gap: 1px; min-width: 0;
}

.cap-next-ja, .cap-next-en {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.cap-next-ja { font-size: 11.5px; color: var(--ink-faint); }

.cap-next-en {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.08em;
  color: var(--ink-ghost);
}

.cap-next-in {
  margin-left: auto; font-size: 11px; color: var(--ink-ghost);
  white-space: nowrap;
}

/* Sound stays beside the ride display, including with the panel folded. */
#audio-controls {
  position: absolute; top: 18px; right: 16px; z-index: 13;
  width: 28px; box-sizing: border-box; padding: 4px 0;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  border: 0; border-radius: 6px;
  background: rgba(7, 12, 7, 0.45);
  pointer-events: auto;
}
.vw-hud #audio-controls {
  position: static; grid-column: 2; grid-row: 1;
  align-self: stretch; contain: size; min-height: 0;
  padding: 2px 0; gap: 3px;
}
.vw-hud #audio-controls #vol { flex: 1 1 0; height: 0; min-height: 0; }
.vw-hud #audio-controls #btn-sound { flex: 0 0 20px; min-height: 20px; padding: 1px 5px; }
.vw-hud #audio-controls #vol-label { flex: 0 0 auto; line-height: 12px; }
/* The compact strip is too short to set a usable slider height. */
#train-window:not(.ride-hud) #audio-controls { min-height: 128px; }
#btn-sound {
  width: 28px; min-height: 28px; padding: 5px;
  background: transparent; border: 0; border-radius: 5px;
  color: #dbe6d3; font-size: 10px; cursor: pointer; white-space: nowrap;
}
#btn-sound svg { display: block; width: 18px; height: 18px; }
#btn-sound.muted .sound-waves { display: none; }
#btn-sound:hover { background: #1c2617; }
#btn-sound.muted { color: var(--ink-faint); }
#vol {
  writing-mode: vertical-lr; direction: rtl;
  width: 20px; height: 86px; margin: 0;
  accent-color: var(--uguisu); cursor: pointer;
}
#vol-label { font-size: 9px; color: var(--ink-dim); text-align: center; }

/* the live-feed key form is retired: the token lives on the back end
   (api/odpt.js) and the board connects by itself */

/* ---------------- the train window ----------------
   Real footage of the strip being ridden, laid in as a full-bleed
   BACKDROP — the board's sky. GEOGRAPHY DECIDES CLARITY: from a
   carriage seat Tokyo is what you see looking outward across the
   window line, so the film runs clear OUTSIDE the loop's real
   alignment and frosts over INSIDE it, where the carriage — the
   board, its labels, its quiet green heart — lives. The dividing
   line is the Yamanote itself: three frost panes, masked to the
   loop's true shape (masks are geometry-built in videowindow.js),
   each inset a little deeper and blurred a little harder, grade the
   clarity from crisp city at the rails to deep calm glass at the
   centre. The E235 LED strip and sill notes anchor to the side of
   the loop the train is on; the far side of the sky calms down so
   the ridden side carries the picture. */

/* No z-index here: the window must NOT form a stacking context, so its
   backdrop can sit under the map (z 5 < 10) while the HUD, sill and
   unmute button sit over it (z 12) — otherwise the one tappable thing
   in the sky ends up underneath the board at sizes where they overlap,
   and the tap falls through to the SVG and steps you off the train.
   (During the entrance fade, opacity < 1 briefly flattens the window
   below the stage — invisible in practice, everything is fading.) */
#train-window {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease;
}

/* Parked (the `hidden` attribute, between rides), the window keeps its
   LAYOUT: the warm player boots inside it, and a player born into a
   display:none ancestor is laid out 0×0 — the "1920-px box the embed
   measures" was, for the warm player, no box at all. visibility:hidden
   keeps the box and paints nothing (no frost readback, no clicks); the
   attribute still means "not riding" to the app and the tests. */
#train-window[hidden] {
  display: block !important;
  visibility: hidden;
}

#train-window.open { opacity: 1; }

/* THE FAREWELL (videowindow.farewell, 2026-09-21): the ridden set has
   been withdrawn and the card is reading the out-of-service piece. The
   film keeps rolling while the whole window dims out over those
   seconds — after `.open` so it wins the tie — and the park follows
   from leave(). Never a cut on the event, never through black: the
   board's own night shows through as the sky fades. */
#train-window.closing { opacity: 0; transition: opacity 3s ease 2.2s; }

/* transparent at rest: before footage rolls (and when a feed dies) the
   sky is simply the board's own night, never a black sheet */
.vw-backdrop {
  position: absolute;
  inset: 0;
  z-index: 5;                      /* behind #map-stage (z-index 10) */
  overflow: hidden;
}

/* the player is oversized past the wrap and centred (see _fitVideo);
   graded only lightly — this is the CLEAR side of the dividing line,
   the city itself; the frost pane does the quieting inside. The film
   rises only once footage actually rolls.

   NO FILTER ON THIS BOX, deliberately (2026-08-27). It used to carry
   brightness(0.82): a per-frame GPU pass over the whole oversized
   film layer (~2100 CSS px wide × device pixels, 50 times a second),
   and a "this frame is filtered" mark on the player's ancestor chain
   for any strict visibility heuristic the embed runs — both paid in
   exactly the currency the choppiness report was written in. The same
   0.82 dim now composites ONCE, in the scrim above the glass stack,
   where it costs a static gradient. */
.vw-video {
  position: absolute;
  opacity: 0;
  transition: opacity 1.2s ease;
}

#train-window.rolling .vw-video { opacity: 1; }

/* No quality curtain, on purpose (videowindow.js, THE PICTURE TARGET):
   rolling footage is ALWAYS on the glass, whatever rung it opened at.
   A picture beats no picture — and a visible player is the only kind
   the embed's ABR reliably climbs for, so showing the film IS the
   quality strategy. */

/* Two panes of the same window: the live recording, and — while a
   boundary approaches — the next recording warming muted beneath it.
   A recording change is a dissolve between these two panes, never an
   in-place reload with its black flash.

   A WARMING understudy sits at 0.02, not 0 (2026-08-27): embeds cap
   the quality of playback they consider invisible (the curtain's
   lesson, videowindow.js THE PICTURE TARGET #3), and a standby that
   warmed behind opacity:0 swapped a low-rung stream onto the glass at
   every dissolve — each crossfade knocked the picture back down the
   ladder it had just climbed. At 2% over a dark film the understudy
   is imperceptible, but it is rendering, and a rendering player is
   one the ABR will feed properly before it goes live. The 2% applies
   ONLY while the slot is genuinely warming (.warming, managed beside
   the staging code): a slot merely PARKED after a crossfade rests at
   true zero, so no frozen frame ghosts over the rolling film for the
   rest of the leg (the review computed the worst case: a bright
   platform still over dark night footage, visible on a wall board). */
.vw-slot {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.65s ease;
  pointer-events: none;
}

.vw-slot.warming { opacity: 0.02; }

.vw-slot.live { opacity: 1; }
.vw-slot.outgoing { opacity: 1; }

/* a PLATFORM cut (the elastic platform's loop or skip between two
   frames of the same standing scene) crossfades slower than a drift
   correction: the last frames of the outgoing stretch dissolve into
   the corrected one, never through black (owner's rule, 2026-09-03) */
#train-window.platform-cut .vw-slot { transition: opacity 1.4s ease; }

.vw-slot iframe,
.vw-slot video,
.vw-slot .vw-player-host {
  width: 100%;
  height: 100%;
  border: 0;
  pointer-events: none;            /* the glass is scenery, not UI */
  display: block;
}

/* THE INTERIOR IS SIGNAGE NOW. Still ONE loop-shaped pane, still the
   mask built in videowindow.js (the loop's shape painted three times
   at accumulating alpha, pinned to the map stage, one composite pass).
   But the pane is no longer soft frost: INSIDE the Yamanote the city
   re-renders as a giant station-board LED MATRIX — a lattice of round
   apertures over a light blur, every aperture a phosphor pixel lit by
   the footage behind it, washed faintly uguisu, rastered by hairline
   rows. Outside the loop the film runs clear and photographic; inside
   it becomes the dot-matrix signage the board itself is made of. Mask
   alpha attenuates lattice, wash and filter together, so the effect
   still deepens toward the centre the way the frost used to. The wash
   doubles as the fallback where backdrop-filter is unavailable — the
   interior still pixelates and dims, it just loses the blur. */
.vw-frost {
  position: absolute;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.1s ease;
  background:
    /* the lattice: dark cell walls, round apertures showing the city */
    radial-gradient(circle,
      rgba(0, 0, 0, 0) 0 1.9px, rgba(4, 8, 5, 0.88) 2.6px 100%)
      0 0 / 6px 6px,
    /* hairline raster rows, barely there */
    repeating-linear-gradient(0deg,
      rgba(154, 205, 50, 0.05) 0 1px, rgba(0, 0, 0, 0) 1px 6px),
    /* the phosphor wash: the board's quiet green heart */
    linear-gradient(rgba(26, 38, 18, 0.34), rgba(14, 24, 11, 0.42));
  -webkit-backdrop-filter: blur(2.5px) saturate(0.7) brightness(0.8) contrast(1.2);
  backdrop-filter: blur(2.5px) saturate(0.7) brightness(0.8) contrast(1.2);
}

/* the glass fogs over just after the film appears, like breath */
#train-window.open .vw-frost { opacity: 1; transition-delay: 0.15s; }

/* honour reduced transparency: the lattice stays (it is paint, not
   transparency trickery), the live blur goes */
@media (prefers-reduced-transparency: reduce) {
  .vw-frost {
    background:
      radial-gradient(circle,
        rgba(0, 0, 0, 0) 0 1.9px, rgba(4, 8, 5, 0.92) 2.6px 100%)
        0 0 / 6px 6px,
      linear-gradient(rgba(14, 22, 11, 0.62), rgba(10, 17, 8, 0.68));
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* THE LOW-POWER GLASS (videowindow.js decides; measured, never
   assumed). Every backdrop-filter over the film is a per-frame
   readback of a 50 fps video, and the 2026-08-27 field ledger caught a
   board paying for them at 29 fps of page — which makes EVERY stream
   look jumpy and feeds the embed's ABR the dropped frames it
   performance-caps on. When the ride's own fps ledger proves this
   machine is slowing (two sub-50, real five-second windows), the
   window sheds exactly the readbacks and per-frame filters, keeps
   every painted look it can (the frost falls back to the same
   lattice+wash the reduced-transparency path ships), and remembers the
   choice per machine. Healthy machines never see any of this. */
body.low-power .vw-frost {
  background:
    radial-gradient(circle,
      rgba(0, 0, 0, 0) 0 1.9px, rgba(4, 8, 5, 0.92) 2.6px 100%)
      0 0 / 6px 6px,
    linear-gradient(rgba(14, 22, 11, 0.62), rgba(10, 17, 8, 0.68));
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

body.low-power .vw-ride {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  background:
    linear-gradient(158deg, rgba(7, 12, 7, 0.88), rgba(3, 6, 3, 0.93));
}

body.low-power .vw-unmute {
  grid-column: 1;
  backdrop-filter: none;
  background: rgba(8, 12, 8, 0.94);
}

/* a drop-shadow on a group whose transform changes every frame is a
   per-frame filter rasterization; the ping rings and halo carry the
   selection on their own */
body.low-power .train.selected { filter: none; }

/* Moving SVG filters compete with video compositing even after the
   glass blur is shed. A same-stream field comparison measured ~30 fps
   with them, 60 without, and ~34 when restored. Keep the painted
   halos, rings, fills and labels; release these per-frame filter passes
   whenever the film is the main picture. Idle-board lighting stays. */
#map.riding * { filter: none; }
/* Keep moving markers on their own composited layers over the film.
   On the same 1440p stream, removing this promotion lost 24.9% of
   frames; restoring it lost none. The ring/door animations and label
   shadows stay intact. Release these layers when the rider steps off. */
#map.riding .train { will-change: transform; }

/* the glass breathes with the ride: dwelling at a platform the frost
   eases back a little — the world pauses with you — and condenses
   again as the train pulls away. Pane opacity attenuates blur and
   tint together, so the ease reads as glass clearing, not a dimmer. */
#train-window.open.dwelling .vw-frost { opacity: 0.78; }

/* hold the film down into the board at the edges: heavy under the
   header and at the bottom rail, open through the middle where the
   city rides past. These stops also CARRY the film's base dim — the
   brightness(0.82) retired from .vw-video composed into each one
   (α' = 1 − 0.82·(1 − α)), so the glass looks the same and the film
   layer itself stays filter-free (see .vw-video). */
.vw-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(8, 11, 9, 0.82) 0%,
    rgba(8, 11, 9, 0.28) 30%,
    rgba(8, 11, 9, 0.25) 66%,
    rgba(8, 11, 9, 0.90) 100%);
}

/* the far side of the sky steps back, so the clearest city is beside
   the arc being ridden and the eye knows which side the seat faces.
   Both biases exist at once and crossfade on an arc crossing — a
   hard cut would make the whole sky blink when the train rounds
   Tabata or Shinagawa */
.vw-scrim::before,
.vw-scrim::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1.2s ease;
}

.vw-scrim::before {
  background: linear-gradient(to left,
    transparent 44%, rgba(8, 11, 9, 0.42) 100%);
}

.vw-scrim::after {
  background: linear-gradient(to right,
    transparent 44%, rgba(8, 11, 9, 0.42) 100%);
}

#train-window.right .vw-scrim::before { opacity: 1; }
#train-window.left  .vw-scrim::after  { opacity: 1; }

/* one faint streak of carriage light, so it still reads as glass */
.vw-glare {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(115deg,
    transparent 30%,
    rgba(255, 255, 255, 0.04) 40%,
    rgba(255, 255, 255, 0.01) 47%,
    transparent 56%);
}

/* THE BOARDING SHUTTER (videowindow.js): every new ride opens from
   black, and the black falls open only once playback is verified
   rolling — or at the hard deadline. The raise is quick (a boarding
   moment, usually covered by the window's own entrance fade anyway);
   the fall is the reveal, 0.8 s, sized so deadline + fall ≤ 2.3 s —
   the owner's cap on the delay (2026-08-30, tightened from 3.0 s: the
   boarding fades in from black WITHIN 2.3 seconds, then the film
   never pauses). A SIBLING over the glass stack, never
   a style on the player itself: a hidden player is one the embed's
   ABR starves (that story is told at .vw-video and in THE PICTURE
   TARGET), so the film keeps rendering at full standing underneath. */
.vw-shutter {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: #000;
  opacity: 0;
  transition: opacity 0.8s ease;
}

.vw-shutter.up { opacity: 1; transition: opacity 0.25s ease; }

/* corner HUD: the E235 LED strip, with the one-tap unmute under it.
   Above the board (z 12 > 10) so the unmute button is genuinely
   tappable wherever the stage reaches — the HUD box itself stays
   pointer-transparent, so the map underneath keeps its hover/click */
.vw-hud {
  position: absolute;
  top: 18px;
  z-index: 12;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 28px;
  align-items: start;
  gap: 4px;
  pointer-events: none;
}

#train-window.right .vw-hud { right: 0; align-items: start; }
#train-window.left  .vw-hud { right: 0; }
/* Center the slider between the strip and the panel's outer edge. */
#train-window:not(.ride-hud) .vw-hud { column-gap: 10px; right: 10px; }

/* the film's strip: now the top row of the band rather than a box of
   its own — the slab carries the ground, the border and the blur */
.vw-led {
  /* Rasterize the letters separately from the live glass underneath.
     Reuse their surface while the movie and the map keep changing. */
  transform: translateZ(0);
  display: flex;
  align-items: center;
  gap: 12.3px;
  padding: 7.8px 14.6px;
}

/* THE HOP SITS ON THE MIDDLE OF THE BAND. Three columns, not three
   items in a row: the flanks take an equal share of whatever is left
   over, so the hop is centred on the slab itself rather than on the
   space between two labels of unequal length. The flanks keep their
   content width when there is nothing spare — they never shrink into
   an ellipsis to buy the middle room. */
.vw-led-line {
  flex: 1 1 0;
  font-size: 10.6px;
  letter-spacing: 0.22em;
  color: var(--uguisu-soft);
  white-space: nowrap;
}

.vw-led-strip {
  display: flex;
  flex: 0 1 auto;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  min-width: 0;
}

.vw-strip-ja {
  font-size: 16.8px;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: #ffd27d;                  /* amber LED, like the real strip */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
  text-shadow: 0 0 8px rgba(255, 210, 125, 0.45);
}

.vw-strip-en {
  /* bigger glyphs paid for out of tracking: at 0.2em the size bump
     pushed the longest hop (TAKANAWA GATEWAY → SHINAGAWA) 15px off
     the band's centre — same width budget, more letter */
  font-size: 10.6px;
  letter-spacing: 0.14em;
  color: #b08a4a;
  font-family: var(--mono);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.vw-led-dir {
  flex: 1 1 0;
  text-align: right;
  font-size: 10.6px;
  letter-spacing: 0.14em;
  color: var(--ink-dim);
  white-space: nowrap;
}

/* THE BAND ON THE GLASS.
   Sizing (owner directive, 2026-08-28): every metric in this slab —
   type, paddings, gaps, width, radii, the track — carries a uniform
   ×1.12 over the original design. Hence the decimals: they are the
   originals scaled, not hand-picked values. The one departure is the
   eta label, retuned separately for legibility (see its own rule).
   One slab, not two boxes. The film's strip (line, hop, direction) is
   its top row; folding the side panel opens the ride rows underneath —
   state, station, JY number, countdown, progress — because folding is
   what takes the ride card away. Wide and flat by design: it lies
   along the top edge of the window like a real 行先表示器 rather than
   stacking into the picture, and it runs well under full opacity so
   the city keeps moving through it. The blur is a small box in a
   corner, not a full-stage pane — the frost's own comment explains why
   that distinction matters to the frame budget. */
.vw-ride {
  border-radius: 11px;
  border: 1px solid rgba(154, 205, 50, 0.13);
  background:
    linear-gradient(158deg, rgba(7, 12, 7, 0.58), rgba(3, 6, 3, 0.74));
  -webkit-backdrop-filter: blur(7px) saturate(0.88);
  backdrop-filter: blur(7px) saturate(0.88);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.32);
  overflow: hidden;
  opacity: 0.765;
  transition: opacity 0.7s ease;
}

/* on a platform the film is standing still and there is nothing to see
   through the glass — so the band comes forward a little */
#train-window.dwelling .vw-ride { opacity: 0.825; }

/* opened, it takes a fixed width: wide enough for the station and the
   countdown to sit side by side rather than stack */
#train-window.ride-hud .vw-ride { width: 405px; }

.vw-ride-rows {
  transform: translateZ(0);
  padding: 11.2px 14.6px 12.3px;
  border-top: 1px solid rgba(154, 205, 50, 0.10);
}

.vw-ride-body {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 15.7px; margin-bottom: 10.1px;
}

/* the station side takes what is left after the countdown, and gives
   ground from the English name first: 高輪ゲートウェイ is eight
   characters of Japanese before its Latin even starts, and the band is
   not wide enough for both at every size */
.vw-ride-where {
  /* min-content, EXPLICITLY (2026-08-28): overflow:hidden below zeroes
     the automatic flex minimum, so without this floor the column could
     be shrunk under the nameline's true minimum once the romaji had
     already been hidden (data-len long) — and the JY badge paid,
     clipped to "JY" while dwelling at 高輪ゲートウェイ. min-content
     keeps name + badge whole (the romaji still contributes nothing to
     it through its OWN min-width: 0, staying the shock absorber), and
     the eta column yields by wrapping its caption (.vw-ride-eta). */
  min-width: min-content;
  flex: 1 1 auto;
  /* a backstop, not the mechanism: the name is stepped down to fit
     (see below), and if some future station out-runs even that it is
     clipped at this column's edge rather than laid over the countdown */
  overflow: hidden;
}

.vw-ride-state {
  display: flex; align-items: baseline; gap: 9px;
}

/* The state pair — 次は NEXT / ただいま NOW AT — promoted to a real
   headline (owner directive, 2026-08-28: "should be a very legible
   element"). The literal 5× ask (59px) would out-shout the station
   name and break the slab's wide-and-flat contract, so this is the
   ceiling the band affords: ~2.4×, semibold, brighter English —
   loudly legible, still subordinate to the station below it. */
#hud-state-ja {
  font-size: 28px; color: var(--uguisu-pale);
  letter-spacing: 0.12em; font-weight: 600;
  line-height: 1;
  /* a headline never breaks mid-word: the field caught ただいま
     folding onto two lines when the dwell caption squeezed the
     column — nowrap makes the row push instead, and the eta column's
     999× yield absorbs it like everything else */
  white-space: nowrap;
}

#hud-state-en {
  font-size: 15px; color: var(--ink-mid); letter-spacing: 0.24em;
  font-family: var(--mono); font-weight: 600;
  white-space: nowrap;
}

.vw-ride-nameline {
  display: flex; align-items: baseline; gap: 10.1px;
  margin-top: 2.2px;
  /* like the column above it: min-content, not 0 — a zero here made
     the whole where-column's floor zero, and the JY badge got clipped
     in the long-name dwell squeeze. The romaji's own min-width: 0
     (below) is what keeps ellipsis-squeeze possible; the name and the
     badge are the two things this row must never surrender. */
  min-width: min-content;
}

/* THE NAME IS STEPPED DOWN TO FIT, the way the real strip does it.
   Most of the loop is two or three characters; 新大久保 is five and
   高輪ゲートウェイ is eight, which at full size is wider than the whole
   station column and used to lay its JY badge straight over the
   countdown. Set from the name's own length in app.js — a station name
   is the one thing on this band that must never be truncated. */
.vw-ride-name {
  font-size: 32.5px; font-weight: 700; line-height: 1.1;
  color: var(--ink-bright); letter-spacing: 0.02em;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
  white-space: nowrap; flex: 0 0 auto;
}

.vw-ride-name[data-len="mid"] { font-size: 26.9px; }
.vw-ride-name[data-len="long"] { font-size: 22.4px; }

/* at the longest names the Latin has room for about one letter, which
   reads as a typo rather than a translation — so it stands down and
   lets the Japanese have the column */
.vw-ride-name[data-len="long"] ~ .vw-ride-en { display: none; }

/* first to give way when the name is a long one */
.vw-ride-en {
  font-size: 11.8px; color: var(--ink-mid);
  letter-spacing: 0.09em; font-family: var(--mono);
  /* whole, like the name (owner directive, 2026-08-28: "Gotand…" is
     not a station). The romaji used to be the row's ellipsis shock
     absorber; the eta column's 999× yield (its caption wraps) is the
     absorber now, verified across all thirty stations in both label
     states. */
  flex: 0 0 auto;
  white-space: nowrap;
}

.vw-ride-jy {
  border: 1.5px solid var(--uguisu); color: var(--uguisu-pale);
  border-radius: 4.5px; font-size: 10.5px; font-weight: 700;
  padding: 2.2px 5.6px; letter-spacing: 0.04em; font-family: var(--mono);
  align-self: center; flex: 0 0 auto;
}

/* The countdown gets its own end of the band, but not the eye. At 30px
   in pale uguisu with a glow it was the loudest thing on the glass,
   louder than the station you are actually going to — and it changes
   every second, so it pulled the eye every second. It reads at the
   same weight as the station's English name now: the same sage ink,
   the same quiet mono, sized to sit under the station rather than
   beside it. The progress bar is already carrying "how far" in green;
   the number only has to be legible. */
.vw-ride-eta {
  display: flex; flex-direction: column; align-items: flex-end;
  /* yields FIRST, and by three orders of magnitude: the caption below
     got real type size, and its dwell wording beside the loop's one
     LONG name (高輪ゲートウェイ, 停車中) is wider than the band can
     seat on one line. Flex shrink distributes deficit by factor ×
     base size, and intrinsic min-content floors proved unreliable
     across the nested flex/overflow chain here — so the yield order
     is stated outright: this column absorbs the squeeze (the caption
     wraps to a second right-aligned line) while the name and its JY
     badge keep their room. The AUTO minimum (no min-width: 0) is the
     brake: the countdown and the caption's longest word never crush. */
  flex: 0 999 auto;
  padding-bottom: 2.2px;
}

.vw-ride-eta-num {
  font-size: 19px; font-weight: 400; line-height: 1;
  color: var(--ink-mid); font-family: var(--mono);
  font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
  /* enough to hold it over a white sky, not enough to glow */
  text-shadow: 0 1px 5px rgba(0, 0, 0, 0.45);
}

/* The countdown's caption ("ただいま停車 · AT PLATFORM"), raised again
   past the band's uniform scale (owner field report, 2026-08-28: still
   barely readable). Legibility here was a triple penalty — tiny type,
   the palette's dimmest ink, and 0.2em tracking scattering 8px mono
   caps — so all three move: larger type, one step brighter ink, and
   tighter tracking, which also buys back the width the longer dwell
   caption needs to stay one line beside a long station name. Still
   the quietest voice in the band, on purpose: it explains the number
   above it and must never compete with it. */
.vw-ride-eta-label {
  font-size: 10px; color: var(--ink-faint); letter-spacing: 0.13em;
  font-family: var(--mono); margin-top: 4.5px;
  text-align: right;
}

.vw-ride-track {
  height: 3.9px; border-radius: 2.2px;
  background: rgba(154, 205, 50, 0.12);
  overflow: hidden;
}

#hud-fill {
  /* Changing width on every panel tick made the entire glass/text row
     lay out and repaint throughout each 300ms animation. Scale just the
     painted bar so the compositor can animate it without repainting text. */
  height: 100%; width: 100%;
  background: linear-gradient(90deg, var(--uguisu-dim), var(--uguisu-soft));
  border-radius: 2px;
  transform-origin: left center;
  transform: scaleX(0);
  transition: transform 0.3s linear;
}

/* Opened, the band stands beside the LINE rather than beside the
   window: --hud-left is measured in _layout() from the loop's own
   rightmost point, so the band keeps the same distance from the track
   whatever the window does. It also stops swapping corners — the strip
   alone follows the arc the train is riding, but a slab this wide
   changing sides mid-ride reads as a fault, not as choreography.
   Both classes, so this outranks the plain dock rules above. */
#train-window.ride-hud.left .vw-hud,
#train-window.ride-hud.right .vw-hud {
  right: auto;
  left: var(--hud-left, 16px);
  align-items: flex-start;
}

.vw-unmute {
  pointer-events: auto;
  background: rgba(8, 12, 8, 0.82);
  border: 1px solid var(--uguisu-dim);
  color: var(--uguisu-pale);
  border-radius: 999px;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  padding: 7px 14px;
  cursor: pointer;
  backdrop-filter: blur(4px);
}

.vw-unmute:hover { background: rgba(20, 30, 14, 0.9); }

/* notes live in the clear band on the ridden side — where the missing
   picture would have been — never over the loop's interior sign */
.vw-note,
.vw-offline {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 32%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  pointer-events: none;
}

#train-window.right .vw-note,
#train-window.right .vw-offline { right: 0; }
#train-window.left .vw-note,
#train-window.left .vw-offline { left: 0; }

.vw-offline-ja { font-size: 14px; letter-spacing: 0.14em; color: var(--ink-dim); }

.vw-offline-en {
  font-size: 10px;
  letter-spacing: 0.28em;
  color: var(--ink-ghost);
  font-family: var(--mono);
}

/* sill: the uguisu accent line, attribution and the honest notes,
   anchored to the same corner as the HUD */
.vw-sill {
  position: absolute;
  bottom: 14px;
  z-index: 12;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

#train-window.right .vw-sill { right: 20px; align-items: flex-end; }
#train-window.left  .vw-sill { left: 20px;  align-items: flex-start; }

.vw-sill::before {
  content: '';
  width: 230px;
  border-top: 2px solid var(--uguisu-dim);   /* the uguisu accent line */
}

/* the attribution earns a touch more presence than the sill's other
   notes — slightly larger, slightly bolder — and it is a LINK: it opens
   the very recording playing in the window (href kept current by the
   HUD). The window itself is pointer-events:none scenery, so the credit
   re-arms them, like the unmute button does. */
/* Two lines, and a hard cap on both. One nowrap line carrying channel
   AND title ran 525px along the bottom of the board. The channel is the
   credit; the tape's name is the useful extra, quieter and underneath. */
.vw-credit {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* the room beside the board on the docked side, measured in
     _layout(); floored so the channel is always legible, capped so the
     line never grows back into what it used to be */
  max-width: 260px;
  pointer-events: auto;
  text-decoration: none;
  transition: color 0.25s ease;
  color: var(--ink-faint);
}

.credit-who {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  font-family: var(--mono);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.credit-tape {
  font-size: 10.5px;
  letter-spacing: 0.04em;
  color: var(--ink-ghost);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.vw-credit:hover,
.vw-credit:focus-visible {
  color: var(--uguisu-soft);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.vw-quality {
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--amber);
  line-height: 1.5;
  max-width: 300px;
  text-wrap: balance;
}

#train-window.left .vw-credit {
  max-width: clamp(150px, calc(var(--sill-room-left, 260px) - 34px), 260px);
}

#train-window.right .vw-credit {
  align-items: flex-end;
  text-align: right;
  max-width: clamp(150px, calc(var(--sill-room-right, 260px) - 34px), 260px);
}
#train-window.right .vw-quality { text-align: right; }
#train-window.left  .vw-quality { text-align: left; }

#train-window.offline .vw-glare { opacity: 0.4; }

/* a dead feed steps the film all the way back (a stale last frame may
   linger behind the notice) — the board returns to its quiet night */
#train-window.offline .vw-video { filter: brightness(0.25) saturate(0.5); }

/* ---------------- the board under a lit sky ----------------
   While the window is open the board answers the film: the loop's
   glow lifts — the window frame lighting up — and the label layer
   gains a deeper shadow so station names hold against the clear city
   beyond the line. */

.loop-glow { transition: stroke-opacity 0.8s ease; }
#map-wrap.window-open .loop-glow { stroke-opacity: 0.2; }

#map-wrap.window-open .rail { stroke: #86a462; }

/* a tight halo plus a wide one — station names must hold even against
   bright daytime footage on the clear side of the line */
#map-wrap.window-open #labels {
  filter: drop-shadow(0 0 2px rgba(3, 5, 3, 0.95))
    drop-shadow(0 1px 7px rgba(3, 5, 3, 0.9));
}

/* the interior sign leans forward a touch over the moving frost */
#map-wrap.window-open .interior-ja { color: #a3b09a; }
#map-wrap.window-open .interior-en { color: #8d9a85; }
#map-wrap.window-open .interior-sub { color: var(--ink-faint); }

@media (prefers-reduced-motion: reduce) {
  #train-window { transition: opacity 0.3s ease; }
  .vw-frost { transition: opacity 0.3s ease; }
  #train-window.open .vw-frost { transition-delay: 0s; }
}

/* ---------------- small screens ---------------- */

/* First the two header buttons drop their words and keep their glyphs
   (their titles still say what they do), then the brand drops its
   tagline. The countdown never gives ground: it is the reason the row
   got tight, and the number you are actually watching. */
@media (max-width: 1240px) {
  #panel-label, #fs-label { display: none; }
  #btn-panel, #btn-fullscreen { padding: 7px 10px; }
}

@media (max-width: 1080px) {
  .brand-sub { display: none; }
  header { gap: 12px; padding: 12px 14px; }
  .head-right { gap: 10px; }
}

@media (max-width: 980px) {
  /* the board is small here and a full-width band would be most of it
     (287 carries the wide band's ×1.12 directive down to this step) */
  #train-window.ride-hud .vw-ride { width: 287px; }
  .vw-ride-rows { padding: 9px 12.3px 11.2px; }
  .vw-ride-name { font-size: 24.6px; }
  .vw-ride-name[data-len="mid"] { font-size: 21.3px; }
  .vw-ride-name[data-len="long"] { font-size: 17.9px; }
  .vw-ride-eta-num { font-size: 16.8px; }
  /* the promoted state headline steps down with the band it leads */
  #hud-state-ja { font-size: 20px; }
  #hud-state-en { font-size: 11px; }
  .vw-ride-eta-label { font-size: 9.5px; }
  .vw-led { gap: 9px; padding: 6.7px 12.3px; }
  /* a narrow band has far less middle to give the hop — it steps down
     with everything else rather than ellipsing every long name */
  .vw-strip-ja { font-size: 14.6px; }
  .vw-strip-en { font-size: 9.5px; }
  /* the heart's signage breathes with the smaller stage too */
  .ir-leg-ja { font-size: 16px; }
  .ir-leg-en { font-size: 9.5px; }
  .ir-loop { font-size: 9.5px; }
  #interior-idle { font-size: 14px; }
  .ir-next-jy { padding: 1.5px 6px; border-width: 1.5px; border-radius: 6px; }
  .ir-jy-line { font-size: 9px; }
  .ir-jy-num { font-size: 15px; }

  main { flex-direction: column; }
  #map-wrap { min-height: 52vh; }
  #panel {
    flex: 1 1 auto; border-left: none;
    border-top: 1px solid var(--edge-soft);
  }
  header { flex-wrap: wrap; gap: 12px; }
  #ride-name-ja { font-size: 42px; }
  /* the map gets much narrower here — keep majors dominant, but not so
     large that six names cover the loop they sit on */
  .st-label.major .ja { font-size: 18px; }
  .st-label.major .en { font-size: 11px; }
  .st-label .ja { font-size: 12px; }

  /* Stacked, the panel is BELOW the board rather than beside it — so
     the handle moves to that seam and lies down, and its chevron points
     the way the panel will actually go. */
  #panel-handle {
    /* on the right of the seam, not the middle */
    top: auto; bottom: 0; left: auto; right: 18px;
    transform: none;
    flex-direction: row; align-items: baseline; gap: 9px;
    padding: 5px 15px;
    border: 1px solid #2a3a20;
    border-bottom: none;
    border-radius: 8px 8px 0 0;
  }
  #panel-handle-glyph { transform: rotate(90deg); }
  .handle-text {
    writing-mode: horizontal-tb;
    display: flex; align-items: baseline; gap: 8px;
  }
  .handle-en { margin-top: 0; }
  /* the handle is on the bottom seam here, and a right-docked sill
     lands in the same corner — the sill is passive text, so it moves */
  .vw-sill { bottom: 48px; }

  body.panel-hidden #panel-handle {
    right: 18px; bottom: 7px;
    border-bottom: 1px solid var(--uguisu-dim);
    border-radius: 8px;
  }
}

@media (max-width: 980px) {
  main { overflow-y: auto; overflow-x: hidden; }
  #map-wrap, #map-wrap.invitation-footer {
    flex: 0 0 auto;
    min-height: 0;
    padding: 30px 24px 48px;
  }
  #map-stage { width: min(100%, 460px); height: auto; flex: 0 1 460px; }
  #map-wrap.invitation-footer { padding-bottom: 132px; }
  #panel { flex: 0 0 auto; width: 100%; overflow: visible; }
  body.panel-hidden #map-wrap { margin-block: auto; }
}

@media (max-width: 600px) {
  header { gap: 10px; padding: 12px; }
  .brand-text h1 { font-size: 14px; letter-spacing: 0.15em; }
  .brand { gap: 10px; }
  .head-right { width: 100%; justify-content: space-between; gap: 6px; }
  .status-pill { gap: 6px; padding: 7px 9px; }
  .status-label { font-size: 10px; letter-spacing: 0.08em; }
  .pill-count { font-size: 10px; }
  #clock { font-size: 21px; }
  .clock-sub { font-size: 9px; letter-spacing: 0.1em; }
  #btn-panel, #btn-fullscreen { min-height: 36px; min-width: 36px; padding: 6px; }
  .st-label:not(.major) .en { display: none; }
  .st-label.major .ja { font-size: 16px; }
  .st-label.major .en { font-size: 9px; letter-spacing: 0.1em; }
  .st-label .ja { font-size: 11px; }
  .ir-leg-ja { font-size: 14px; }
  .ir-leg-en, .ir-loop { font-size: 8px; letter-spacing: 0.04em; }
  #train-window:not(.ride-hud) .vw-hud { top: 4px; }
  #map-wrap > #audio-controls { top: 4px; right: 16px; width: auto; flex-direction: row; gap: 8px; }
  #map-wrap > #audio-controls #btn-sound { width: 36px; min-height: 36px; padding: 9px; }
  #map-wrap > #audio-controls #vol { writing-mode: horizontal-tb; direction: ltr; width: 72px; height: 24px; }
}

.service-footer {
  flex: 0 0 auto;
  position: relative;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 36px;
  padding: 0 18px;
  border-top: 1px solid var(--edge-soft);
  background: var(--panel-bg);
  color: var(--ink-dim);
  font-size: 11px;
}
.service-footer a { color: var(--uguisu-pale); text-underline-offset: 3px; }
.service-footer summary { cursor: pointer; padding: 10px 0; white-space: nowrap; }
.about-content {
  position: absolute;
  right: 12px;
  bottom: calc(100% + 8px);
  width: min(470px, calc(100vw - 24px));
  max-height: min(520px, 70svh);
  overflow-y: auto;
  padding: 22px;
  border: 1px solid var(--edge);
  border-radius: 12px;
  background: var(--panel-bg);
  color: var(--ink-mid);
  font-size: 13px;
  line-height: 1.6;
}
.about-content h2 { margin-bottom: 14px; color: var(--ink-bright); font-size: 17px; }
.about-content p + p { margin-top: 12px; }
@media (max-width: 600px) {
  .service-footer { padding-inline: 12px; gap: 8px; font-size: 10px; }
}
@media (max-width: 420px) {
  .head-right .pill-count, .head-right .pill-rule { display: none; }
  .head-right { flex-wrap: nowrap; }
}
