/* ===================================================================
   Design tokens — from docs/hud_mockup.html, contrast-adjusted per the
   spec's own instruction ("контраст плотнее, чем в мокапе" — real video
   backgrounds vary, unlike the mockup's flat synthetic feed). Change
   values here, not in individual component rules below.
   =================================================================== */
:root {
  --black: #0b0b0b;
  --panel: #111111;
  --grey: #171717;
  --white: #ffffff;
  --line: rgba(255, 255, 255, 0.16);
  --line-soft: rgba(255, 255, 255, 0.08);

  /* Bumped from the mockup's .55/.32 — verified against --panel via WCAG:
     dim ~7.2:1, dimmer ~4.8:1 (AA normal-text minimum is 4.5:1) */
  --dim: rgba(255, 255, 255, 0.62);
  --dimmer: rgba(255, 255, 255, 0.52);

  /* Amber is the single signature accent — used for everything active.
     Red is reserved for alarm states only, never decorative. */
  --amber: #ffc90f;
  --red: #ff3b30;

  --mono: "Share Tech Mono", ui-monospace, monospace;
  --disp: "Tektur", system-ui, sans-serif;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 16px;
  --space-4: 24px;

  --transition-fast: 120ms ease-out;

  /* The video's actual displayed rect (letterboxed to 16:9 inside the
     viewport, since #video uses object-fit: contain) — computed and kept
     live by app.js's updateVideoFrame(), not static CSS. Screen corners,
     the reticle/crosshair, and the tick rail are positioned off these so
     they frame the video itself, not the wider viewport background.
     Defaults to full-viewport so there's a sane frame before JS runs. */
  --frame-left: 0px;
  --frame-top: 0px;
  --frame-width: 100vw;
  --frame-height: 100vh;
}

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  height: 100%;
}
body {
  background: var(--black);
  color: var(--white);
  font-family: var(--mono);
  overflow: hidden;
}

.u {
  text-transform: uppercase;
}
.dim {
  color: var(--dim);
}
.nodata {
  color: var(--dimmer) !important;
}

/* ===================================================================
   Video — `contain`, not `cover`: the feed is 720p (see CLAUDE.md); on
   any viewport wider/taller than 16:9, `cover` would both crop the edges
   of the frame AND upscale past the source's native resolution to fill
   the gap, softening it further. `contain` shows the whole frame at its
   real aspect ratio, at most 1:1-or-down-scaled, never blown up beyond
   what the stream actually delivers.

   The dot-grid + gradient below is the element's own CSS background —
   it only shows in the letterbox gaps `contain` leaves empty. Once video
   is playing it never overlaps the actual frame, so it can't obscure the
   view; before the stream connects, it's what's shown instead of plain
   black.
   =================================================================== */
#video {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Grain dot is a rasterized SVG circle, not a tiled radial-gradient —
     Safari rasterizes very small tiled radial-gradients inconsistently
     (hard circles instead of a soft 1px dot), since it recomputes the
     gradient per tile rather than sampling a bitmap. A baked dot sidesteps
     that entirely; same 26px tile / 1px dot / 0.04 alpha as before, just
     immune to the Safari bug. Vignette layer below is unchanged from the
     original — the darkening panel is a separate layer, sized/positioned
     to --frame-* (the actual computed video rect, app.js) via
     background-position/size, so only that rect darkens rather than the
     full-viewport background this element also renders as chrome. */
  background:
    linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)) var(--frame-left) var(--frame-top) /
      var(--frame-width) var(--frame-height) no-repeat,
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ccircle cx='13' cy='13' r='1' fill='white' fill-opacity='0.04'/%3E%3C/svg%3E")
      0 0 / 26px 26px,
    radial-gradient(120% 90% at 50% 40%, #16181a 0%, #0d0e0f 55%, #070707 100%);
}

/* Shown centered in the video frame until a stream is actually playing —
   otherwise there's no way to tell "not connected yet" from "connected
   but the car's camera feed is just black". Visibility toggled in
   app.js off the same video.connectionState poll that already drives
   the STREAM pill; not a new hook into video.js. Slow pulse, not the
   fast <150ms interaction feedback used elsewhere — this is an idle/
   waiting state, not a response to input. Respects prefers-reduced-motion
   via the global rule further down. */
.video-offline {
  position: fixed;
  top: calc(var(--frame-top) + var(--frame-height) / 2);
  left: calc(var(--frame-left) + var(--frame-width) / 2);
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.24em;
  color: var(--dimmer);
  pointer-events: none;
  z-index: 4;
  transition: opacity var(--transition-fast);
}
.video-offline.hidden {
  opacity: 0;
}
.video-offline__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--dimmer);
  animation: video-offline-pulse 1.6s ease-in-out infinite;
}
@keyframes video-offline-pulse {
  0%, 100% { opacity: 0.25; }
  50% { opacity: 1; }
}

/* Static noise texture — a single pre-rendered SVG image, not a
   per-frame effect, so it costs nothing beyond one paint. */
.noise {
  position: fixed;
  inset: 0;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  z-index: 5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ===================================================================
   Screen corner brackets — frame the video's actual displayed rect
   (--frame-*, kept live by app.js's updateVideoFrame()), not the raw
   viewport — with object-fit:contain letterboxing the feed, a
   viewport-anchored bracket would float disconnected from the frame on
   any non-16:9 screen.
   =================================================================== */
.screen-corner {
  --corner-size: 34px;
  position: fixed;
  width: var(--corner-size);
  height: var(--corner-size);
  border: 1px solid rgba(255, 255, 255, 0.45);
  pointer-events: none;
  z-index: 3;
}
.screen-corner.tl {
  top: calc(var(--frame-top) + 20px);
  left: calc(var(--frame-left) + 20px);
  border-right: 0;
  border-bottom: 0;
}
.screen-corner.tr {
  top: calc(var(--frame-top) + 20px);
  left: calc(var(--frame-left) + var(--frame-width) - 20px - var(--corner-size));
  border-left: 0;
  border-bottom: 0;
}
.screen-corner.bl {
  top: calc(var(--frame-top) + var(--frame-height) - 20px - var(--corner-size));
  left: calc(var(--frame-left) + 20px);
  border-right: 0;
  border-top: 0;
}
.screen-corner.br {
  top: calc(var(--frame-top) + var(--frame-height) - 20px - var(--corner-size));
  left: calc(var(--frame-left) + var(--frame-width) - 20px - var(--corner-size));
  border-left: 0;
  border-top: 0;
}

/* ===================================================================
   Center reticle — decorative signature element, centered on the video
   frame (not the viewport). Thin, and now deliberately faint (opacity)
   on top of already-low-alpha strokes, so it barely registers over the
   actual driving view rather than competing with it.
   =================================================================== */
.cross-h {
  position: fixed;
  top: calc(var(--frame-top) + var(--frame-height) / 2);
  left: calc(var(--frame-left) + var(--frame-width) / 2);
  width: 220px;
  height: 1px;
  background: rgba(255, 255, 255, 0.28);
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 2;
  opacity: 0.4;
}
.cross-v {
  position: fixed;
  top: calc(var(--frame-top) + var(--frame-height) / 2);
  left: calc(var(--frame-left) + var(--frame-width) / 2);
  width: 1px;
  height: 36px;
  background: rgba(255, 255, 255, 0.28);
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 2;
  opacity: 0.4;
}
.reticle {
  position: fixed;
  top: calc(var(--frame-top) + var(--frame-height) / 2);
  left: calc(var(--frame-left) + var(--frame-width) / 2);
  width: 150px;
  height: 150px;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 2;
  opacity: 0.4;
}
.reticle svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* ===================================================================
   Left tick rail — decorative altimeter-style scale, height/position
   relative to the video frame. Ticks generated in app.js (structure
   only, no data).
   =================================================================== */
.rail {
  position: fixed;
  left: calc(var(--frame-left) + 30px);
  top: calc(var(--frame-top) + var(--frame-height) / 2);
  transform: translateY(-50%);
  height: calc(var(--frame-height) * 0.44);
  width: 26px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  pointer-events: none;
  z-index: 2;
}
.rail .tk {
  height: 1px;
  background: var(--line);
  width: 10px;
}
.rail .tk.long {
  width: 22px;
  background: rgba(255, 255, 255, 0.3);
}

/* ===================================================================
   Top bar — eyebrow/pills (left), link readout (right)
   =================================================================== */
.top {
  position: fixed;
  top: 30px;
  left: 70px;
  right: 70px;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 20px;
  font-size: 11px;
  letter-spacing: 0.14em;
  z-index: 4;
}
.eyebrow {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.tag {
  font-family: var(--disp);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.22em;
}
.code {
  color: var(--dimmer);
  font-size: 10px;
  letter-spacing: 0.2em;
}

.pills {
  display: flex;
  gap: 8px;
  margin-top: 12px;
  flex-wrap: wrap;
}
.pill {
  font-size: 9.5px;
  letter-spacing: 0.18em;
  padding: 4px 10px;
  color: var(--dimmer);
  border: 1px solid var(--line);
  clip-path: polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px);
  background: rgba(0, 0, 0, 0.35);
  transition: color var(--transition-fast), border-color var(--transition-fast);
}
.pill.on {
  color: var(--amber);
  border-color: rgba(255, 201, 15, 0.55);
}
.pill .dot {
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  margin-right: 7px;
  vertical-align: middle;
}

.top-right {
  text-align: right;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}
.readout {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 11px;
}
.readout .k {
  color: var(--dimmer);
  font-size: 9.5px;
  letter-spacing: 0.2em;
}
.v {
  font-size: 15px;
  letter-spacing: 0.06em;
  font-family: var(--mono);
}

.bars {
  display: inline-flex;
  gap: 3px;
  align-items: flex-end;
  height: 13px;
}
.bars i {
  width: 4px;
  background: var(--amber);
  opacity: 0.9;
  display: block;
}
.bars i:nth-child(1) { height: 5px; }
.bars i:nth-child(2) { height: 7px; }
.bars i:nth-child(3) { height: 9px; }
.bars i:nth-child(4) { height: 11px; }
.bars i:nth-child(5) { height: 13px; }
.bars i.off {
  background: rgba(255, 255, 255, 0.18);
}

/* ===================================================================
   Quality state — classes are set by hud.js (textContent/className
   writes every render), so the glyph lives in CSS ::before content,
   never injected DOM, and never wiped by those writes. Not just color:
   distinct glyph shape per state too.
   =================================================================== */
.quality {
  display: inline-flex;
  align-items: center;
}
.quality::before {
  margin-right: 5px;
}
.quality-good { color: var(--amber); }
.quality-good::before { content: "\25CF"; }
.quality-fair { color: var(--amber); opacity: 0.72; }
.quality-fair::before { content: "\25B2"; }
.quality-poor { color: var(--red); }
.quality-poor::before { content: "\2715"; }
.quality-unknown { color: var(--dimmer); }
.quality-unknown::before { content: "\25CB"; }

/* ===================================================================
   Bottom clusters — telemetry (left), session (right)
   =================================================================== */
.bottom {
  position: fixed;
  left: 70px;
  right: 70px;
  bottom: 36px;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 24px;
  z-index: 4;
}

.frame {
  position: relative;
  padding: 14px 18px;
  background: rgba(0, 0, 0, 0.4);
}
.frame::before, .frame::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  border: 1px solid rgba(255, 255, 255, 0.4);
}
.frame::before { top: 0; left: 0; border-right: 0; border-bottom: 0; }
.frame::after { bottom: 0; right: 0; border-left: 0; border-top: 0; }

.tele {
  display: flex;
  align-items: flex-end;
  gap: 26px;
}
.speed {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.speed .lab {
  color: var(--amber);
  font-size: 9.5px;
  letter-spacing: 0.24em;
}
.speed .big {
  font-family: var(--disp);
  font-weight: 700;
  font-size: 56px;
  line-height: 0.82;
  letter-spacing: 0.01em;
}
.speed .unit {
  color: var(--dim);
  font-size: 10px;
  letter-spacing: 0.28em;
}

.gauge {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
}
.gauge .lab {
  color: var(--dimmer);
  font-size: 9px;
  letter-spacing: 0.2em;
}
.stat {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.stat .k {
  color: var(--dimmer);
  font-size: 9px;
  letter-spacing: 0.2em;
}

/* ===================================================================
   Throttle vbar / steering hbar. Fill transform is written every
   animation frame by hud.js via inline style.transform — deliberately
   NO CSS transition on the fill elements, it would fight the per-frame
   updates and break the "same frame" echo requirement.

   Our throttle is signed (forward/reverse), unlike the mockup's
   unsigned example — kept the center-anchored bidirectional fill from
   the first pass: solid amber growing up = forward, hollow amber
   outline growing down = reverse (distinguishable by fill style, not
   just position, since amber is the only accent color available).

   Steering: hud.js sets `transform: translateX(<pct>%)` on #steering-bar
   itself, and CSS percentages in translateX are relative to the
   element's OWN width, not the track's — so #steering-bar is sized as
   an invisible positioning box (184px) whose width makes that 50%
   land at a sensible ~80px travel, with the actual visible 16px knob
   drawn as its ::after, centered within it. This composes correctly
   with hud.js's inline transform without needing to touch hud.js.
   =================================================================== */
.vbar {
  width: 10px;
  height: 70px;
  border: 1px solid var(--line);
  position: relative;
  background: rgba(0, 0, 0, 0.3);
}
.vbar::before {
  content: "";
  position: absolute;
  left: -3px;
  right: -3px;
  top: 50%;
  height: 1px;
  background: rgba(255, 255, 255, 0.35);
}
#throttle-bar {
  position: absolute;
  left: 1px;
  right: 1px;
  bottom: 50%;
  height: 50%;
  background: var(--amber);
  opacity: 0.9;
  transform-origin: bottom;
  transform: scaleY(0);
  transition: none;
}
#throttle-bar.reverse {
  bottom: auto;
  top: 50%;
  transform-origin: top;
  background: transparent;
  border: 1px solid var(--amber);
  opacity: 1;
}

.steer {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
  min-width: 200px;
}
.steer .lab {
  color: var(--dimmer);
  font-size: 9px;
  letter-spacing: 0.2em;
}
.hbar {
  width: 200px;
  height: 10px;
  border: 1px solid var(--line);
  position: relative;
  background: rgba(0, 0, 0, 0.3);
}
.hbar .mid {
  position: absolute;
  left: 50%;
  top: -3px;
  bottom: -3px;
  width: 1px;
  background: rgba(255, 255, 255, 0.35);
}
#steering-bar {
  position: absolute;
  left: 50%;
  top: 1px;
  bottom: 1px;
  width: 184px;
  margin-left: -92px;
  pointer-events: none;
  transition: none;
}
#steering-bar::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 16px;
  margin-left: -8px;
  background: var(--amber);
  opacity: 0.9;
}

.right-b {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-end;
}
.lap {
  display: flex;
  gap: 20px;
  align-items: flex-end;
}
.lap .col .k {
  color: var(--dimmer);
  font-size: 9px;
  letter-spacing: 0.2em;
}
.lap .col .v {
  font-family: var(--disp);
  font-weight: 600;
  font-size: 22px;
  letter-spacing: 0.04em;
  line-height: 1;
}

/* ===================================================================
   Boot / access panel — prominent at load (video stays visible around
   it, per Prompt 7: no token still means video-only), fades out once
   the control datachannel is authorized rather than staying parked in
   a corner.
   =================================================================== */
.boot-panel {
  position: fixed;
  top: 96px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  background: rgba(11, 11, 11, 0.78);
  border: 1px solid var(--line);
  padding: var(--space-3) var(--space-4);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  min-width: 260px;
  text-align: center;
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.boot-panel__tag {
  font-family: var(--disp);
  font-weight: 600;
  letter-spacing: 0.22em;
  font-size: 12px;
  color: var(--amber);
}
.boot-panel.authorized {
  opacity: 0;
  pointer-events: none;
  transform: translateX(-50%) translateY(-8px);
}

.token-form {
  display: flex;
  gap: var(--space-2);
  pointer-events: auto;
}
#token-input {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
  color: var(--white);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  padding: 8px 10px;
  width: 160px;
  outline: none;
  text-transform: uppercase;
}
#token-input:focus {
  border-color: var(--amber);
}
#token-input::placeholder {
  color: var(--dimmer);
}
.token-form button {
  font-family: var(--disp);
  font-size: 11px;
  letter-spacing: 0.14em;
  padding: 8px 16px;
  border: 1px solid var(--amber);
  background: transparent;
  color: var(--amber);
  cursor: pointer;
  clip-path: polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px);
  transition: background var(--transition-fast), color var(--transition-fast);
  white-space: nowrap;
}
.token-form button:hover {
  background: var(--amber);
  color: var(--black);
}
.boot-line {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.05em;
  color: var(--dim);
  min-height: 1.4em;
  word-break: break-word;
}

/* ===================================================================
   Calibration overlay (DOM built by gamepad-calibration.js — restyled
   here via the classes it already applies, structure untouched)
   =================================================================== */
/* Must come before/win over the base rule below: the `hidden` HTML
   attribute sets display:none via the UA stylesheet, but our own
   `#calibration-container { display: flex }` rule has higher
   specificity and would silently override it, leaving this full-screen
   overlay visible at all times otherwise (regressed once already when
   this file was rewritten wholesale — the [hidden] override didn't
   carry over). */
#calibration-container[hidden] {
  display: none;
}
#calibration-container {
  position: fixed;
  inset: 0;
  background: rgba(11, 11, 11, 0.94);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-4);
  text-align: center;
  z-index: 10;
  font-family: var(--mono);
}
#calibration-container h3 {
  font-family: var(--disp);
  color: var(--amber);
  letter-spacing: 0.08em;
  margin: 0;
}
#calibration-container p {
  max-width: 480px;
  color: var(--dim);
  font-size: 13px;
  line-height: 1.5;
}
.calibration-row {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  font-size: 12px;
  color: var(--dim);
}
.calibration-row select,
.calibration-row input {
  font-family: var(--mono);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
  color: var(--white);
  padding: 4px 6px;
}
.calibration-readout {
  font-size: 11px;
  color: var(--dimmer);
  max-width: 90vw;
  overflow-wrap: break-word;
}
#calibration-container button {
  font-family: var(--disp);
  font-size: 12px;
  letter-spacing: 0.1em;
  padding: 8px 20px;
  border: 1px solid var(--amber);
  background: transparent;
  color: var(--amber);
  cursor: pointer;
  clip-path: polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px);
  transition: background var(--transition-fast), color var(--transition-fast);
}
#calibration-container button:hover {
  background: var(--amber);
  color: var(--black);
}

/* ===================================================================
   Touch zones (invisible drag areas from Prompt 7, unchanged) + the
   floating joystick visual shown while dragging — "as in games":
   appears under the finger, knob follows the drag, snaps away on
   release. Purely visual; the actual throttle/steering value is still
   computed by the same bindTouchZone delta math in app.js.
   =================================================================== */
.touch-zone {
  position: fixed;
  bottom: 0;
  width: 42%;
  height: 46%;
  touch-action: none;
  display: none;
}
#touch-throttle { left: 0; }
#touch-steering { right: 0; }

@media (pointer: coarse) {
  .touch-zone {
    display: block;
  }
  #touch-throttle {
    border-top: 1px solid var(--line);
    border-right: 1px solid var(--line);
  }
  #touch-steering {
    border-top: 1px solid var(--line);
    border-left: 1px solid var(--line);
  }
}

.joystick {
  position: fixed;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 7;
}
.joystick__track {
  position: absolute;
  border: 1px solid rgba(255, 255, 255, 0.35);
  background: rgba(0, 0, 0, 0.35);
}
.joystick__knob {
  position: absolute;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--amber);
  opacity: 0.85;
  left: -18px;
  top: -18px;
}
.joystick--vertical .joystick__track {
  width: 44px;
  height: 200px;
  left: -22px;
  top: -100px;
  border-radius: 22px;
}
.joystick--horizontal .joystick__track {
  width: 200px;
  height: 44px;
  left: -100px;
  top: -22px;
  border-radius: 22px;
}

/* ===================================================================
   Responsive — tablet and mobile.
   =================================================================== */
@media (max-width: 900px) {
  .top, .bottom { left: 24px; right: 24px; }
  .rail { display: none; }
  .speed .big { font-size: 40px; }
  .steer, .hbar { min-width: 130px; width: 130px; }
  #steering-bar { width: 114px; margin-left: -57px; } /* (130-16)/2 = 57 */
  .tele { gap: 14px; }
  .lap .col .v { font-size: 18px; }
  .cross-h { width: 140px; }
  .screen-corner { --corner-size: 24px; }
  /* .boot-panel's top is set by app.js's updateVideoFrame(), measured off
     the real .top bar height — not hardcoded here, see that comment. */
}

@media (max-width: 600px) {
  /* Decorative, not essential — drop it to keep the top bar to one
     compact row so the boot panel below has a predictable, small
     clearance to sit under rather than colliding with it. */
  .eyebrow .code { display: none; }
  .bottom { flex-direction: column; align-items: flex-start; gap: 16px; }
  .right-b { align-items: flex-start; }
  /* Vertical position comes from app.js (measured off .top's real
     height); only sizing is set per breakpoint here. */
  .boot-panel { min-width: 220px; padding: var(--space-2) var(--space-3); }
  .token-form { flex-direction: column; align-items: stretch; }
  #token-input { width: 100%; }
}

/* Phone landscape: short viewport height regardless of width (a landscape
   phone is often ≥600-900px wide, so the two width-only breakpoints above
   don't catch it — but ~350-420px of height is nowhere near enough room
   for the desktop-scale top/bottom bars, boot panel, and full-size
   reticle at once). Everything here is about vertical compactness; .top/
   .bottom stay row-based (there's width to spare in landscape) rather
   than the 600px breakpoint's column stack, which would cost even more
   height. */
@media (max-height: 480px) {
  .top { top: 10px; }
  .eyebrow { gap: 8px; }
  .eyebrow .code { display: none; }
  .pills { margin-top: 6px; gap: 6px; }
  .pill { padding: 3px 8px; }
  .rail { display: none; }
  .cross-h { width: 140px; }
  .cross-v { height: 24px; }
  .reticle { width: 90px; height: 90px; }
  .bottom { bottom: 10px; flex-direction: row; align-items: flex-end; gap: 16px; }
  .right-b { align-items: flex-end; }
  .tele { gap: 14px; }
  .frame { padding: 8px 12px; }
  .speed .big { font-size: 30px; }
  .vbar { height: 40px; }
  .steer, .hbar { min-width: 110px; width: 110px; }
  #steering-bar { width: 94px; margin-left: -47px; } /* (110-16)/2 = 47 */
  .lap .col .v { font-size: 16px; }
  .boot-panel { padding: var(--space-1) var(--space-2); min-width: 200px; }
  .token-form { flex-direction: row; }
  #token-input { width: 130px; }
}

/* ===================================================================
   Prompt 8 — latency debug mode. Off by default (.active toggles it,
   set from app.js on the ` key); pointer-events:none since neither has
   any interactive control.
   =================================================================== */
.latency-counter {
  display: none;
  position: fixed;
  /* Bottom edge of the frame, not the top: the boot/access panel lives
     near the top and is visible until a token is entered — a tester
     toggling debug mode before connecting would otherwise get the
     counter and boot panel overlapping. */
  top: calc(var(--frame-top) + var(--frame-height) - 12px);
  left: calc(var(--frame-left) + var(--frame-width) / 2);
  transform: translate(-50%, -100%);
  z-index: 8;
  font-family: var(--mono);
  font-size: 40px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: var(--amber);
  background: rgba(0, 0, 0, 0.6);
  padding: 4px 16px;
  border: 1px solid var(--amber);
  pointer-events: none;
}
.latency-counter.active {
  display: block;
}

.debug-panel {
  display: none;
  position: fixed;
  bottom: var(--space-3);
  right: var(--space-3);
  z-index: 8;
  background: rgba(11, 11, 11, 0.88);
  border: 1px solid var(--line);
  padding: var(--space-2) var(--space-3);
  font-family: var(--mono);
  font-size: 11px;
  min-width: 260px;
  pointer-events: none;
}
.debug-panel.active {
  display: block;
}
.debug-panel__label {
  font-family: var(--disp);
  color: var(--amber);
  letter-spacing: 0.14em;
  font-size: 11px;
  margin-bottom: var(--space-2);
}
.debug-panel__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 2px 0;
}
.debug-panel__key {
  color: var(--dimmer);
  letter-spacing: 0.06em;
  font-size: 10px;
}
.debug-panel__val {
  color: var(--white);
  font-variant-numeric: tabular-nums;
}
.debug-panel__note {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--line-soft);
  color: var(--dimmer);
  font-size: 9.5px;
  line-height: 1.4;
}

/* ===================================================================
   Unit status popup (web/src/status.js) — pilot-only diagnostic view of
   pi/webrtc_relay.py's GET /control/status. Button sits with the other
   top-right readouts; the panel itself is a centered modal (unlike
   .debug-panel's always-on corner box) since it's opened deliberately,
   not glanced at continuously.
   =================================================================== */
.status-button {
  margin-top: 6px;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.18em;
  padding: 5px 10px;
  color: var(--dimmer);
  background: rgba(0, 0, 0, 0.35);
  border: 1px solid var(--line);
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}
.status-button:hover {
  color: var(--amber);
  border-color: rgba(255, 201, 15, 0.55);
}

.status-panel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 20;
  width: min(420px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  overflow-y: auto;
  background: rgba(11, 11, 11, 0.94);
  border: 1px solid var(--line);
  padding: var(--space-3);
  font-family: var(--mono);
}
.status-panel__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--space-2);
}
.status-panel__title {
  font-family: var(--disp);
  color: var(--amber);
  letter-spacing: 0.14em;
  font-size: 13px;
}
.status-panel__close {
  background: none;
  border: none;
  color: var(--dim);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
}
.status-panel__close:hover {
  color: var(--white);
}
.status-panel__error {
  color: var(--red);
  font-size: 11px;
  margin-bottom: var(--space-2);
}
.status-panel__grid {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px var(--space-3);
  font-size: 11px;
}
.status-panel__grid .k {
  color: var(--dimmer);
  font-size: 9.5px;
  letter-spacing: 0.14em;
  align-self: center;
}
.status-panel__grid .v {
  font-size: 12px;
  text-align: right;
}
.status-badge.status-ok { color: var(--amber); }
.status-badge.status-bad { color: var(--red); }
.status-badge.status-unknown { color: var(--dimmer); }

.status-panel__logs-label {
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: 1px solid var(--line-soft);
  color: var(--dimmer);
  font-size: 9.5px;
  letter-spacing: 0.14em;
}
.status-panel__logs {
  margin-top: var(--space-2);
  max-height: 220px;
  overflow-y: auto;
  font-size: 10px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
.status-log-line {
  color: var(--dim);
}
.status-log-line--warning {
  color: var(--amber);
}
.status-log-line--error {
  color: var(--red);
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
}
