/*
 * Wound Up's look (DOS-452): the menus are made of what the 3D world is made
 * of. A panel is a slab of smoked glass lit along its top edge, as the race HUD
 * is; a button is a lacquer cap, and the one to press is a bar of polished
 * metal; a field is a well cut into the glass; an icon is a polished bead.
 * Nothing is flat: every surface is lit from above, as the studio lights it.
 *
 * Colour is by role, and the roles match the 3D world: cyan is dash, boost and
 * "go"; gold is the finish and a Perfect; the hot colour is a hazard or an
 * error. The trim is the only part a track's look swaps.
 */
:root {
  /* Plates and ink. main.ts reads --ink to know the stylesheet arrived. */
  --ink: #12141c;
  --plate: #24262e;
  --plate-hi: #34363f;
  --plate-lo: var(--well);
  --seam: rgba(246, 236, 217, 0.14);
  --seam-hi: rgba(246, 236, 217, 0.34);
  --cream: #f6ecd9;
  --cream-dim: rgba(246, 236, 217, 0.76);
  /* Trim: the graphite look's copper (src/client/themes/looks.ts), Do's favourite
     and so what the menus wear. In a race the HUD takes its track's metal: hud.css. */
  --trim: #e07a4a;
  --trim-hi: #ffc08a;
  --trim-lo: #8a4a32;
  /* Roles, shared with the world. */
  --boost: #2fe0ff;
  --boost-lo: #0b7f96;
  --finish: #f2c94c;
  --hot: #ff4b3a;
  --hot-lo: #8f1d14;
  --ramp: #ff8a3c;
  /* Second and third on a podium. */
  --silver: #c9ccd6;
  --copper: #c7743f;
  /* Ink at the strengths the HUD floats on the picture with. */
  --ink-veil: rgba(18, 20, 28, 0.82);
  --ink-shade: rgba(18, 20, 28, 0.5);
  /* The race HUD floats on the picture as acrylic (DOS-442). A dark room
     (graphite, and every older map) gets smoked glass and light type; a light
     room swaps these for milk glass and ink type (hud.css). */
  --glass: rgba(24, 25, 31, 0.62);
  --glass-edge: rgba(255, 255, 255, 0.2);
  --glass-shine: rgba(255, 255, 255, 0.1);
  --glass-shadow: 0 6px 18px rgba(8, 9, 14, 0.3);
  --hud-fg: #f6ecd9;
  --hud-dim: rgba(246, 236, 217, 0.68);
  /* Dash's icon and word, on glass. */
  --boost-ink: #2fe0ff;
  --hot-ink: #ff7a6b;
  /* Jump's lacquer cap, lit from above, and the word on it. */
  --cap-hi: #4a4e5b;
  --cap: #2a2c35;
  --cap-lo: #17181d;
  --cap-ink: #f6ecd9;

  --radius-s: 6px;
  --radius-m: 12px;
  --radius-l: 18px;
  /* Under anything you can push: a hard contact line and a soft fall of shadow. */
  --shadow-toy: 0 1px 0 rgba(8, 9, 14, 0.9), 0 7px 14px rgba(8, 9, 14, 0.36);
  --shadow-panel: 0 18px 44px rgba(8, 9, 14, 0.5);
  --font: "Rubik", ui-rounded, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: "Unbounded", "Rubik", system-ui, sans-serif;
  --font-mono: "Martian Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --edge: 12px;
  /* Motion: one set of curves and times for every screen. Exits and entrances
     ease out, a toy springing into place overshoots, and UI motion stays under
     300 ms. Only transform and opacity animate. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-spring: cubic-bezier(0.2, 1.4, 0.3, 1);
  --dur-press: 90ms;
  --dur-fast: 140ms;
  --dur-ui: 240ms;
  --dur-pop: 280ms;
  /* The race HUD's unit (hud.css): 1 px on a phone, growing with the screen's short
     side to about 2 px at 1080p, so the HUD keeps its share of a big screen, and
     shrinking below 390 px so a 320 px phone still fits the row and the item box. */
  --hud-u: min(0.2564vmin, clamp(1px, 0.18vmin, 2px));
  /* The corner the call-outs stack in (hud.css): under the item box, clear of the road. */
  --callout-top: calc(var(--safe-top) + 22px + var(--hud-u) * 76);
  --callout-right: calc(var(--safe-right) + 12px);

  /* A slab of smoked glass: the HUD's glass, thick enough to read type on. */
  --slab:
    linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.1),
      rgba(255, 255, 255, 0.02) 22%,
      transparent 60%
    ),
    rgba(22, 23, 29, 0.9);
  --slab-rim: rgba(255, 255, 255, 0.12);
  --slab-lit: inset 0 1px 0 rgba(255, 255, 255, 0.3);
  /* A lacquer cap, lit from above: what Jump is in a race. */
  --cap-face: linear-gradient(180deg, var(--cap-hi), var(--cap) 48%, var(--cap-lo));
  --cap-lit: inset 0 1px 0 rgba(255, 255, 255, 0.24);
  /* Polish, laid over any flat colour: bright sky down to a horizon line, then
     the floor's bounce up from below. */
  --gloss: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.55) 0%,
    rgba(255, 255, 255, 0.18) 12%,
    rgba(255, 255, 255, 0) 47%,
    rgba(8, 9, 14, 0.18) 53%,
    rgba(8, 9, 14, 0) 82%,
    rgba(255, 255, 255, 0.2) 100%
  );
  /* A well cut into the glass. */
  --well: rgba(8, 9, 14, 0.46);
  --well-cut: inset 0 2px 5px rgba(0, 0, 0, 0.5), 0 1px 0 rgba(255, 255, 255, 0.09);
  /* The seam round a part that sits in a panel. */
  --part-seam: rgba(8, 9, 14, 0.72);
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  background: var(--ink);
  font-family: var(--font);
  color: var(--cream);
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

#app {
  position: fixed;
  inset: 0;
}

#stage {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

button,
a,
input {
  touch-action: manipulation;
}

.screen {
  position: absolute;
  inset: 0;
  display: grid;
  /* A definite row, so a sheet's max-height: 100% really caps it at the screen. */
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  padding: calc(var(--safe-top) + var(--edge)) calc(var(--safe-right) + var(--edge))
    calc(var(--safe-bottom) + var(--edge)) calc(var(--safe-left) + var(--edge));
  pointer-events: none;
}

.screen[hidden] {
  display: none;
}

.screen--start,
.screen--join,
.screen--lobby,
.screen--finish,
.screen--pause,
.screen--records,
.screen--error {
  /* A veil, no blur: the world and its runners still turn behind the plate. */
  background: radial-gradient(120% 90% at 50% 0%, rgba(18, 20, 28, 0.35), rgba(18, 20, 28, 0.78));
  pointer-events: auto;
}

/* One plate per screen; it never scrolls the page, only its own list if it must. */
.sheet {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: min(440px, 100%);
  max-height: 100%;
  min-height: 0;
  padding: 20px 18px 18px;
  color: var(--cream);
  border-radius: 22px;
  background: var(--slab);
  border: 1px solid var(--slab-rim);
  box-shadow: var(--slab-lit), var(--shadow-panel);
  animation: panel-in var(--dur-ui) var(--ease-out) both;
}

@keyframes panel-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
}

.hub-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.brand {
  position: relative;
  display: grid;
  gap: 4px;
}

/* The name, cast: wide capitals in polished pearl over polished trim metal, each on its own line. */
.logo {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(46px, 15.5vw, 66px);
  font-weight: 900;
  line-height: 0.92;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  display: grid;
  justify-items: start;
}

.logo--small {
  font-size: clamp(30px, 9.5vw, 40px);
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.3em;
}

.logo__a,
.logo__b {
  display: block;
  color: transparent;
  background-clip: text;
  -webkit-background-clip: text;
  filter: drop-shadow(0 0.05em 0 var(--ink)) drop-shadow(0 0.12em 0.14em rgba(8, 9, 14, 0.35));
}

.logo__a {
  background-image: linear-gradient(
    180deg,
    #fff 8%,
    var(--cream) 46%,
    #c2b7a4 55%,
    var(--cream) 84%
  );
}

.logo__b {
  display: flex;
  align-items: center;
  background-image: linear-gradient(
    180deg,
    #ffe9d2 8%,
    var(--trim-hi) 22%,
    var(--trim) 48%,
    #b4552f 56%,
    var(--trim) 82%
  );
}

/* The wind-up key in the side of "Up": the shaft runs into the P. Its bow turns
   a notch at a time, as if someone were winding the game; the shaft stays still. */
.brand__key {
  flex: none;
  width: 0.95em;
  height: 0.76em;
  margin-left: -0.04em;
  overflow: visible;
  /* The letters are see-through ink over a gradient; the key is solid metal. */
  color: var(--trim);
}

.brand__key-collar {
  fill: var(--trim-hi);
}

.brand__key-bow {
  transform-box: fill-box;
  transform-origin: center;
  animation: key-wind 2.7s var(--ease-out) infinite;
}

/* Six notches a turn: each holds, then clicks on. Seen side on, a turning bow
   only gets narrower and wider, so the turn is a scale. */
@keyframes key-wind {
  0%,
  11% {
    transform: scaleY(1);
  }
  16.6%,
  27.6% {
    transform: scaleY(0.84);
  }
  33.3%,
  44.3% {
    transform: scaleY(0.48);
  }
  50%,
  61% {
    transform: scaleY(0.16);
  }
  66.6%,
  77.6% {
    transform: scaleY(0.48);
  }
  83.3%,
  94.3% {
    transform: scaleY(0.84);
  }
  100% {
    transform: scaleY(1);
  }
}

.panel__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 24px;
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.field {
  display: block;
}

.field[hidden] {
  display: none;
}

/* A well cut into the plate: darker than the face, a hairline round it. */
.field__input {
  width: 100%;
  min-height: 52px;
  padding: 8px 14px;
  font: inherit;
  font-size: 19px;
  font-weight: 700;
  color: var(--cream);
  background: var(--well);
  border: 1px solid var(--seam);
  border-radius: var(--radius-m);
  box-shadow: var(--well-cut);
  outline: none;
  transition: border-color var(--dur-fast) ease;
}

.field__input::placeholder {
  color: rgba(246, 236, 217, 0.46);
}

.field__input:focus-visible {
  border-color: var(--boost);
}

.field__input--code {
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.4em;
  font-size: 24px;
}

.stack {
  display: grid;
  gap: 10px;
}

/* A pusher: a raised plate with a trim rim that sinks its step when pressed. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 52px;
  padding: 8px 18px;
  font: inherit;
  font-size: 18px;
  font-weight: 800;
  line-height: 1.1;
  color: var(--cream);
  text-decoration: none;
  background: var(--cap-face);
  border: 1px solid var(--part-seam);
  border-radius: var(--radius-m);
  cursor: pointer;
  box-shadow: var(--cap-lit), var(--shadow-toy);
  transform: scale(1);
  /* The shadow snaps with the press; only the transform eases. */
  transition:
    transform var(--dur-press) var(--ease-out),
    background-color var(--dur-fast) ease;
  user-select: none;
  -webkit-user-select: none;
}

.btn[hidden] {
  display: none;
}

.btn:active:not(:disabled) {
  transform: scale(0.97);
  box-shadow:
    var(--cap-lit),
    0 1px 0 rgba(8, 9, 14, 0.9);
}

.btn:focus-visible,
.picker__select:focus-visible {
  outline: 3px solid var(--boost);
  outline-offset: 2px;
}

.btn:disabled {
  cursor: default;
  color: var(--cream-dim);
  border-color: var(--seam-hi);
  box-shadow: none;
}

.btn--big {
  min-height: 64px;
  font-family: var(--font-display);
  font-size: 19px;
  letter-spacing: -0.01em;
}

.btn--small {
  min-height: 44px;
  padding: 4px 14px;
  font-size: 15px;
}

.btn--chip {
  min-height: 44px;
  padding: 4px 14px;
  font-size: 14px;
  white-space: nowrap;
  border-radius: 999px;
  border-color: var(--glass-edge);
  /* A chip also floats over the world on its own, so it carries its own glass. */
  background: linear-gradient(180deg, var(--glass-shine), transparent 60%), var(--glass);
  box-shadow: none;
}

/* A chip that leads with a bead hugs it. */
.btn:has(> .bead) {
  padding-left: 8px;
}

.btn--chip:active:not(:disabled) {
  transform: scale(0.96);
  box-shadow: none;
}

/* The main thing to press on a screen is a bar of polished trim metal. */
.btn--primary,
.btn--go {
  color: var(--ink);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.7),
    var(--shadow-toy);
}

.btn--primary {
  background: var(--gloss), var(--trim);
}

/* Go is the dash colour: the same cyan that says "boost" on the road. */
.btn--go {
  background: var(--gloss), var(--boost);
}

.btn--primary:active:not(:disabled),
.btn--go:active:not(:disabled) {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.7),
    0 1px 0 rgba(8, 9, 14, 0.9);
}

.btn--ghost {
  background: transparent;
  border-color: var(--seam-hi);
  box-shadow: none;
}

.btn--ghost:active:not(:disabled) {
  transform: scale(0.97);
  box-shadow: none;
}

.icon {
  width: 20px;
  height: 20px;
  flex: none;
}

/* A form saying no: the hazard colour, as a stripe down its edge. */
.form-message {
  margin: 0;
  padding: 10px 12px 10px 14px;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--cream);
  background: var(--plate-lo);
  border: 2px solid var(--hot);
  border-left-width: 8px;
  border-radius: var(--radius-m);
}

.form-message[hidden] {
  display: none;
}

kbd {
  font-family: var(--font-mono);
  font-size: 0.82em;
  font-weight: 700;
  /* A keycap: the lacquer of a button, with a lip under it. */
  display: inline-block;
  min-width: 1.7em;
  text-align: center;
  background: var(--cap-face);
  color: var(--cap-ink);
  border: 1px solid var(--part-seam);
  border-radius: 6px;
  padding: 2px 6px;
  box-shadow:
    var(--cap-lit),
    0 2px 0 rgba(8, 9, 14, 0.8);
}

/* A control reads as button = action: its keycaps, then what they do. */
.key-hint {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-weight: 700;
}

.key-hint + .key-hint {
  margin-left: 14px;
}

@media (pointer: coarse) {
  .controls-hint {
    display: none;
  }
}

/* ---------- global overlays ---------- */

.net-banner,
.toast {
  position: fixed;
  left: 50%;
  z-index: 30;
  max-width: calc(100% - 28px);
  padding: 9px 16px;
  font-weight: 800;
  font-size: 15px;
  text-align: center;
  color: var(--cream);
  background: var(--slab);
  border: 1px solid var(--slab-rim);
  border-radius: 999px;
  box-shadow: var(--slab-lit), var(--shadow-toy);
  transform: translateX(-50%);
  pointer-events: none;
}

.net-banner {
  top: calc(var(--safe-top) + 10px);
  color: var(--ink);
  background: var(--gloss), var(--trim);
  border-color: var(--part-seam);
  animation: pulse 1.2s ease-in-out infinite;
}

.toast {
  bottom: calc(var(--safe-bottom) + 18px);
  border-radius: var(--radius-m);
}

.net-banner[hidden],
.toast[hidden] {
  display: none;
}

@keyframes pulse {
  50% {
    opacity: 0.7;
  }
}

.live {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

/* Reduce Motion: nothing moves and nothing loops. A text that shows only while
   its animation runs (the announcer, the "Perfect!" pop) keeps its time with a
   fade of its own, set with !important beside its rule. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  /* The key rests face on instead of stopping half turned. */
  .brand__key-bow {
    animation: none;
  }
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A native select dressed as a pusher, with its name engraved above it: the
   phone's own picker opens. */
.picker {
  position: relative;
  display: grid;
  gap: 5px;
  min-width: 0;
}

.picker__select {
  width: 100%;
  min-height: 48px;
  padding: 4px 34px 4px 12px;
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  color: var(--cream);
  /* The colour is for the open list, which takes no gradient. */
  background-color: var(--plate-hi);
  background-image: var(--cap-face);
  border: 1px solid var(--part-seam);
  border-radius: var(--radius-m);
  box-shadow: var(--cap-lit), var(--shadow-toy);
  appearance: none;
  -webkit-appearance: none;
  text-overflow: ellipsis;
}

.picker::after {
  content: "";
  position: absolute;
  right: 14px;
  bottom: 19px;
  width: 9px;
  height: 9px;
  border-right: 2.5px solid var(--trim);
  border-bottom: 2.5px solid var(--trim);
  transform: rotate(45deg);
  pointer-events: none;
}

/* The Dress up badge: a hanger on a disc of the player's colour, ringed in ink. */
/* An icon is a bead of polished metal with its mark cut in, lit as the world's
   chrome is: a hot spot high on the left and shade round the lower rim. The
   "Dress up" bead is enamel in the player's colour. */
.bead {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  color: var(--ink);
  background:
    radial-gradient(
      circle at 34% 24%,
      rgba(255, 255, 255, 0.95) 0 5%,
      rgba(255, 255, 255, 0.42) 16%,
      rgba(255, 255, 255, 0) 46%
    ),
    radial-gradient(circle at 50% 44%, rgba(8, 9, 14, 0) 50%, rgba(8, 9, 14, 0.44) 100%),
    var(--dot, var(--trim));
  box-shadow:
    0 1px 0 rgba(8, 9, 14, 0.85),
    0 3px 6px rgba(8, 9, 14, 0.4);
}

.bead .icon {
  width: 15px;
  height: 15px;
  filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.4));
}

.bead--small {
  width: 20px;
  height: 20px;
}

.bead--small .icon {
  width: 12px;
  height: 12px;
}

.bead--gold {
  --dot: var(--finish);
}

.js-dress {
  flex-wrap: nowrap;
  white-space: nowrap;
}

/* ---------- landing and join ---------- */

/* The start plate's bar holds one thing, the sound toggle, at the right. */
.sheet--start .hub-bar {
  justify-content: flex-end;
}

/* Records leads the bar; on a phone the sound chip keeps the far corner. */
.records-chip {
  margin-right: auto;
}

/* Mouse speed: a chip like its neighbour, for a desk only. A finger has no mouse to turn with. */
.mouse-speed {
  display: none;
}

@media (hover: hover) and (pointer: fine) {
  /* It takes the room the sound chip leaves, and the slider takes what the words leave, so the
     pair never spills past the sheet's corner screws. */
  .mouse-speed {
    display: flex;
    flex: 1 1 0;
    min-width: 0;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    padding: 4px 14px;
    font-size: 14px;
    font-weight: 800;
    color: var(--cream);
    white-space: nowrap;
    border: 2px solid var(--seam);
    border-radius: 999px;
    background: var(--plate-lo);
  }

  .mouse-speed__range {
    flex: 1 1 0;
    width: 0;
    min-width: 48px;
    accent-color: var(--trim);
    cursor: pointer;
  }

  /* Fixed width and tabular digits: 90% to 100% must not nudge the slider under the hand. */
  .mouse-speed__value {
    min-width: 4ch;
    text-align: right;
    font-variant-numeric: tabular-nums;
  }
}

/* Host is the one thing to do here. Join is a size down, and practice sits
   apart under a seam, so the three never read as equal choices. */
.sheet--start .stack .btn--secondary {
  min-height: 56px;
}

.practice {
  display: grid;
  gap: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--seam);
}

.solo-row {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: 8px;
}

.picker--solo::after {
  right: 11px;
}

.picker--wide {
  grid-column: 1 / -1;
}

.picker--wide[hidden] {
  display: none;
}

.solo-row .btn {
  grid-column: 1 / -1;
  min-height: 48px;
  font-size: 16px;
}

.field-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}

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

.field-row .btn--chip {
  min-height: 52px;
  border-radius: var(--radius-m);
}

/* A code that came with the link: stamped, not typed. */
.join-code {
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: 10px 12px;
  background: var(--plate-lo);
  border: 2px solid var(--seam);
  border-radius: var(--radius-m);
}

.join-code[hidden] {
  display: none;
}

.join-code__value {
  font-family: var(--font-mono);
  font-size: 44px;
  line-height: 1;
  font-weight: 700;
  letter-spacing: 0.16em;
  /* The tracking trails the last letter; pull it back so the code sits centred. */
  margin-right: -0.16em;
  color: var(--trim);
  text-shadow: 0 3px 0 var(--ink);
}

@media (max-height: 600px) {
  .sheet {
    gap: 8px;
    padding: 14px 14px 14px;
  }

  .logo {
    font-size: 38px;
    display: flex;
    gap: 0 0.28em;
  }

  .logo--small {
    font-size: 28px;
  }

  .brand__key {
    display: none;
  }

  .screen .sheet .btn--big {
    min-height: 52px;
    font-size: 17px;
  }

  .sheet--start .stack .btn--secondary,
  .solo-row .btn {
    min-height: 44px;
  }

  .practice {
    padding-top: 8px;
    gap: 6px;
  }

  .picker__select,
  .field__input {
    min-height: 44px;
  }
}

/* Two pickers share a phone's width, so each gives its words every pixel it can. */
.picker--solo .picker__select {
  padding: 4px 26px 4px 10px;
  font-size: 15px;
}

/* Short screens (a phone on its side): the card scrolls instead of clipping. */
@media (max-height: 520px) {
  .screen--start,
  .screen--join {
    place-items: start center;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }

  .screen--start .sheet,
  .screen--join .sheet {
    max-height: none;
  }
}

/* ---------- TV screen (/tv/CODE) ---------- */

.screen--tv {
  --tv-unit: min(1vw, 1.8vh);
  place-items: stretch;
  padding: calc(var(--safe-top) + 3vh) calc(var(--safe-right) + 3vw) calc(var(--safe-bottom) + 3vh)
    calc(var(--safe-left) + 3vw);
}

.screen--tv[data-phase="lobby"],
.screen--tv[data-phase="results"] {
  background: radial-gradient(120% 90% at 50% 0%, rgba(18, 20, 28, 0.35), rgba(18, 20, 28, 0.8));
}

.tv-lobby,
.tv-race,
.tv-results {
  display: none;
}

.screen--tv[data-phase="lobby"] .tv-lobby,
.screen--tv[data-phase="results"] .tv-results {
  display: grid;
}

.screen--tv[data-phase="race"] .tv-race {
  display: block;
}

.tv-lobby {
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 5vw;
}

.tv-invite {
  display: grid;
  justify-items: center;
  gap: 1vh;
  padding: 2.4vh;
  color: var(--cream);
  background: var(--slab);
  border: 2px solid var(--slab-rim);
  border-radius: var(--radius-l);
  box-shadow: var(--shadow-panel);
}

/* White behind the code, whatever the plate: a camera needs the contrast. */
.tv-invite__qr {
  width: min(56vh, 40vw);
  aspect-ratio: 1;
  padding: 1vh;
  background: #fff;
  border-radius: var(--radius-m);
}

.tv-invite__scan {
  margin: 0;
  font-size: calc(var(--tv-unit) * 4);
  font-weight: 800;
}

.tv-invite__url {
  margin: 0;
  font-size: calc(var(--tv-unit) * 2);
  font-weight: 700;
  opacity: 0.75;
}

.tv-side {
  display: grid;
  gap: 1.6vh;
  align-content: center;
  color: var(--cream);
}

.tv-side .logo {
  font-size: calc(var(--tv-unit) * 8);
}

.tv-code {
  margin: 0;
  font-size: calc(var(--tv-unit) * 3);
  font-weight: 700;
}

.tv-code strong {
  display: block;
  font-size: calc(var(--tv-unit) * 9);
  line-height: 1;
  font-family: var(--font-mono);
  letter-spacing: 0.14em;
  color: var(--trim);
  text-shadow: 0 6px 0 var(--ink);
}

.tv-status {
  margin: 0;
  font-size: calc(var(--tv-unit) * 2.6);
  font-weight: 700;
}

.tv-roster {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.2vh 1vw;
}

.tv-roster .player {
  min-height: calc(var(--tv-unit) * 6);
  font-size: calc(var(--tv-unit) * 2.4);
}

.tv-roster .player__name {
  font-size: inherit;
}

.tv-roster .player__state {
  font-size: calc(var(--tv-unit) * 1.8);
}

.tv-roster .portrait {
  width: calc(var(--tv-unit) * 4.4);
  height: calc(var(--tv-unit) * 4.4);
}

/* In-race standings: an opaque card in the top-right corner, inside the safe area. */
.tv-race {
  justify-self: end;
  align-self: start;
  box-sizing: border-box;
  width: min(30vw, 520px);
  max-width: 100%;
  max-height: 100%;
  overflow: hidden;
  padding: 1.6vh 1.2vw;
  background: var(--ink);
  border: 4px solid var(--trim);
  border-radius: var(--radius-l);
  box-shadow: var(--shadow-panel);
  color: var(--cream);
}

.tv-race__title {
  margin: 0 0 1vh;
  font-family: var(--font-display);
  font-size: calc(var(--tv-unit) * 3);
  color: var(--trim);
}

.tv-race__code {
  margin: 1.2vh 0 0;
  font-size: calc(var(--tv-unit) * 1.6);
  font-weight: 700;
  opacity: 0.85;
}

.tv-race .standings__row {
  background: var(--plate);
}

.tv-race .standings__row--done {
  background: var(--boost-lo);
}

.tv-race .portrait {
  width: calc(var(--tv-unit) * 3.2);
  height: calc(var(--tv-unit) * 3.2);
}

/* TV distance: names big enough to read from the sofa. */
.tv-roster .player__name,
.tv-roster .player__name--long,
.tv-roster .player__name--longer {
  font-size: max(28px, calc(var(--tv-unit) * 2.4));
}

.results--tv .results__row {
  min-height: calc(var(--tv-unit) * 4);
}

.tv-race__code strong {
  color: var(--trim);
  letter-spacing: 0.1em;
}

.tv-results {
  align-content: center;
  justify-items: center;
  gap: 1.4vh;
  color: var(--cream);
}

.tv-results__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: calc(var(--tv-unit) * 5);
  text-shadow: 0 5px 0 var(--ink);
}

.podium {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: flex-end;
  gap: 1.4vw;
}

.podium__step {
  display: grid;
  justify-items: center;
  gap: 0.6vh;
  width: min(16vw, 280px);
  font-weight: 800;
}

.podium__racer {
  display: block;
  width: calc(var(--tv-unit) * 9);
  aspect-ratio: 1;
}

.podium__racer .portrait {
  width: 100%;
  height: 100%;
}

.podium__name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: calc(var(--tv-unit) * 3);
}

.podium__time {
  font-size: calc(var(--tv-unit) * 2.2);
  font-variant-numeric: tabular-nums;
  opacity: 0.85;
}

.podium__block {
  display: grid;
  place-items: center;
  width: 100%;
  font-size: calc(var(--tv-unit) * 4);
  font-family: var(--font-display);
  color: var(--ink);
  border: 3px solid var(--ink);
  border-radius: var(--radius-s) var(--radius-s) 0 0;
  /* A machined block: a bright top edge and polish down its face. */
  background-image: var(--gloss);
  box-shadow: inset 0 3px 0 rgba(255, 255, 255, 0.35);
}

.podium__step--1 .podium__block {
  height: 18vh;
  background-color: var(--finish);
}

.podium__step--2 .podium__block {
  height: 13vh;
  background-color: var(--silver);
}

.podium__step--3 .podium__block {
  height: 9vh;
  background-color: var(--copper);
}

/* Everyone below the podium in two columns, so eight racers fit a 1080p screen. */
.results--tv {
  width: min(70vw, 1100px);
  max-height: 24vh;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  font-size: calc(var(--tv-unit) * 2);
}

.results--tv:empty {
  display: none;
}

/* Waiting for the room: no empty QR card or "----", just the message. */
.screen--tv[data-load="loading"] .tv-invite,
.screen--tv[data-load="loading"] .tv-code,
.screen--tv[data-load="missing"] .tv-invite,
.screen--tv[data-load="missing"] .tv-code,
.screen--tv[data-load="missing"] .tv-roster {
  display: none;
}

.screen--tv[data-load="loading"] .tv-lobby,
.screen--tv[data-load="missing"] .tv-lobby {
  grid-template-columns: 1fr;
  justify-items: center;
  text-align: center;
}

.tv-message {
  position: absolute;
  left: 50%;
  bottom: 6vh;
  transform: translateX(-50%);
  max-width: 80vw;
  margin: 0;
  padding: 1.4vh 2vw;
  font-size: calc(var(--tv-unit) * 2.6);
  font-weight: 800;
  text-align: center;
  color: var(--cream);
  background: var(--plate-lo);
  border: 4px solid var(--hot);
  border-radius: var(--radius-l);
}

.tv-message[hidden] {
  display: none;
}

/* A TV link opened on a phone still works: stack the QR above the roster. */
@media (max-aspect-ratio: 1 / 1) {
  .screen--tv {
    --tv-unit: 1.6vw;
  }

  .tv-lobby {
    grid-template-columns: 1fr;
    justify-items: center;
    align-content: start;
    gap: 2vh;
  }

  .tv-invite__qr {
    width: min(56vw, 36vh);
  }

  .tv-side {
    justify-items: center;
    text-align: center;
  }

  .tv-race {
    width: min(60vw, 260px);
  }

  .podium__step {
    width: 26vw;
  }
}

.podium__name.player__name--long {
  font-size: calc(var(--tv-unit) * 2.5);
}

.podium__name.player__name--longer {
  font-size: calc(var(--tv-unit) * 2);
}

/* ---------- phone lobby ---------- */

.sheet--lobby {
  gap: 10px;
}

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

.qr svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* White behind the code, whatever the plate: a camera needs the contrast. */
.invite__qr {
  flex: none;
  width: clamp(96px, 30vw, 132px);
  aspect-ratio: 1;
  padding: 4px;
  background: #fff;
  border: 2px solid var(--trim);
  border-radius: var(--radius-m);
}

.invite__text {
  display: grid;
  gap: 4px;
  justify-items: start;
  min-width: 0;
}

/* The code, engraved: the same mono face as every number in the game. */
.invite__code {
  font-family: var(--font-mono);
  font-size: clamp(34px, 10.5vw, 46px);
  line-height: 1;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--trim);
  text-shadow: 0 3px 0 var(--ink);
}

/* What happens next, or why Start will not press: full-strength, it is the instruction. */
.lobby-status {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--cream);
}

/* Dress up, at the head of the roster it changes. */
.roster-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--seam);
}

.players {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
  min-height: 0;
  overflow: auto;
  align-content: start;
}

/* Five or more racers fold into two columns so eight still fit a small phone. */
.players:has(> :nth-child(5)) {
  grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
}

/* A racer is a well in the plate. Ready lights its rim in the go colour. */
.player {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 4px 10px;
  border: 2px solid var(--seam);
  border-radius: var(--radius-m);
  background: var(--plate-lo);
  min-width: 0;
}

.player--ready {
  border-color: var(--boost);
}

.player--you {
  background: var(--plate-hi);
}

.player--away {
  opacity: 0.55;
}

.player__dot {
  flex: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--dot);
  border: 2px solid var(--ink);
}

.player__name {
  flex: 1;
  min-width: 0;
  font-weight: 800;
  font-size: 17px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Name and "you" on one line: the name ellipsises, the chip never clips. */
.player__line {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

.player__you {
  flex: none;
  font-family: var(--font-mono);
  font-style: normal;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 16px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--trim);
  color: var(--ink);
}

.player__crown {
  flex: none;
  color: var(--finish);
  display: inline-flex;
}

.player__state {
  flex: none;
  font-size: 13px;
  font-weight: 700;
  color: var(--cream-dim);
}

.player--ready .player__state {
  color: var(--boost);
}

.players:has(> :nth-child(5)) .player__state {
  font-size: 0;
}

.players:has(> :nth-child(5)) .player--ready .player__state::after {
  content: "✓";
  font-size: 18px;
}

.lobby-actions {
  display: flex;
  gap: 10px;
}

.lobby-actions .btn {
  flex: 1 1 0;
  padding-inline: 10px;
}

/* Ready is a latch: pressed, it stays down and lights up in the go colour. */
#ready-btn[aria-pressed="true"] {
  color: var(--boost);
  background: var(--plate-lo);
  border-color: var(--boost);
  box-shadow: 0 1px 0 var(--ink);
  transform: translateY(3px);
}

@media (max-height: 600px) {
  .invite__code {
    font-size: 34px;
  }

  .lobby-status {
    font-size: 14px;
  }

  .roster-bar {
    padding-top: 4px;
  }

  /* Eight racers on the shortest phones: one line each, so all eight show at once. */
  .invite__qr {
    width: 84px;
  }

  .players:has(> :nth-child(5)) .player {
    min-height: 34px;
    padding-block: 2px;
  }

  /* A full room has a fourth row of racers to fit: the plate closes up around it. */
  .sheet--lobby:has(.players > :nth-child(7)) {
    gap: 6px;
    padding-block: 10px;
  }

  .sheet--lobby:has(.players > :nth-child(7)) .invite__code {
    font-size: 28px;
  }

  .sheet--lobby:has(.players > :nth-child(7)) .invite__qr {
    width: 72px;
  }

  .players:has(> :nth-child(7)) {
    gap: 4px;
  }

  .players:has(> :nth-child(7)) .player {
    min-height: 30px;
    padding-block: 0;
  }
}

/* ---------- host settings and roster looks ---------- */

/* Mode is short ("Race"): it gets the narrow column, the map name the rest. */
.lobby-config {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  gap: 8px;
}

.lobby-config .picker__select {
  padding-right: 28px;
  padding-left: 10px;
}

@media (max-width: 360px) {
  .lobby-config .picker__select {
    font-size: 15px;
  }
}

.lobby-config[hidden],
.config-summary[hidden] {
  display: none;
}

/* What the host picked, for everyone else: read-only, so no rim to press. */
.config-summary {
  margin: 0;
  padding: 8px 12px;
  font-weight: 800;
  font-size: 15px;
  color: var(--cream);
  background: var(--plate-lo);
  border: 2px dashed var(--seam);
  border-radius: var(--radius-m);
}

.player__who {
  display: grid;
  flex: 1;
  min-width: 0;
  line-height: 1.1;
}

/* A chibi portrait of each racer's look (portrait.ts): head in their colour, their hat. */
.portrait {
  display: inline-block;
  flex: none;
  width: 36px;
  height: 36px;
}

.portrait svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* Each face sits on a small cream dial, so any head colour reads on a dark plate. */
.portrait {
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--ink);
  background: var(--cream);
}

.player__portrait {
  margin-block: -4px;
}

.player--you .player__portrait {
  box-shadow:
    0 0 0 2px var(--ink),
    0 0 0 4px var(--trim);
}

/* The lobby's own Dress up, at the head of the roster it changes. */
.btn--dress {
  gap: 8px;
}

/* ---------- wardrobe ---------- */

.wardrobe {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  padding: calc(var(--safe-top) + var(--edge)) calc(var(--safe-right) + var(--edge))
    calc(var(--safe-bottom) + var(--edge)) calc(var(--safe-left) + var(--edge));
  background: rgba(18, 20, 28, 0.7);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

.wardrobe[hidden] {
  display: none;
}

.sheet--wardrobe {
  gap: 8px;
}

/* The fitting room: a lit window in the plate with the runner turning inside. */
.wardrobe__stage {
  position: relative;
  flex: 1 1 200px;
  min-height: 120px;
  max-height: 260px;
  border: 2px solid var(--trim);
  border-radius: var(--radius-m);
  overflow: hidden;
}

.wardrobe__preview {
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.wardrobe__sparkle {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(circle at 22% 30%, var(--cream) 0 4px, transparent 5px),
    radial-gradient(circle at 78% 24%, var(--finish) 0 5px, transparent 6px),
    radial-gradient(circle at 70% 70%, var(--cream) 0 3px, transparent 4px),
    radial-gradient(circle at 30% 72%, var(--boost) 0 4px, transparent 5px),
    radial-gradient(circle at 50% 14%, var(--finish) 0 3px, transparent 4px);
  opacity: 0;
}

.wardrobe__sparkle.is-on {
  animation: sparkle var(--dur-pop) var(--ease-out) both;
}

@keyframes sparkle {
  0% {
    opacity: 0;
    transform: scale(0.7);
  }
  35% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: scale(1.25);
  }
}

.wardrobe__tabs {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 4px;
}

.wardrobe__tab {
  min-height: 44px;
  padding: 2px 4px;
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  color: var(--cream);
  background: var(--plate-lo);
  border: 2px solid var(--seam);
  border-radius: var(--radius-m);
  cursor: pointer;
}

/* The chosen part is the one in trim metal, as the chosen thing is on every screen. */
.wardrobe__tab[aria-selected="true"] {
  color: var(--ink);
  background: var(--gloss), var(--trim);
  border-color: var(--part-seam);
}

.wardrobe__options {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  min-height: 0;
  overflow-y: auto;
  align-content: start;
  flex: 0 1 auto;
}

.wardrobe__options[data-part="color"] {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.wardrobe__option {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 2px 8px;
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  line-height: 1.05;
  text-align: left;
  overflow-wrap: anywhere;
  min-width: 0;
  color: var(--cream);
  background: var(--plate-lo);
  border: 2px solid var(--seam);
  border-radius: var(--radius-m);
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out);
}

.wardrobe__option[aria-checked="true"] {
  color: var(--trim-hi);
  background: var(--plate-hi);
  border-color: var(--trim);
}

.wardrobe__option:active {
  transform: scale(0.96);
}

.wardrobe__swatch {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--swatch);
  border: 2px solid var(--ink);
  box-shadow: 0 0 0 1px var(--seam);
}

@media (max-height: 600px) {
  .wardrobe__stage {
    flex-basis: 110px;
  }
}

/* Colour names ("Bubblegum", "Snowdrop") in three narrow columns: a size down, never cut. */
.wardrobe__options[data-part="color"] .wardrobe__option {
  font-size: 12px;
  gap: 4px;
  padding-inline: 6px;
}

/* Long names shrink a size or two before they would ever need an ellipsis (lobby-view.ts). */
.player__name--long {
  font-size: 15px;
}

.player__name--longer {
  font-size: 13px;
}

/* Two roster columns: a smaller portrait leaves the name the rest of the row. */
.players:has(> :nth-child(5)) .player__portrait {
  width: 28px;
  height: 28px;
}

.players:has(> :nth-child(5)) .player {
  gap: 5px;
  padding-inline: 6px;
}

/* ---------- HUD ---------- */

.screen--hud {
  place-items: start center;
  pointer-events: none;
}

/* In a race the HUD's metal is its track's (src/client/themes/looks.ts; ui.ts
   setLook marks the page). Graphite is the stylesheet's own copper, so only the
   other two looks are named; the menus stay graphite whatever was raced last. */
html[data-screen="hud"][data-look="brass"] {
  --trim: #e0957a;
  --trim-hi: #ffd8c6;
  --trim-lo: #a35a45;
}

html[data-screen="hud"][data-look="dusk"] {
  --trim: #b9aee0;
  --trim-hi: #f4efff;
  --trim-lo: #7b6fb0;
}

/* The blush and lilac rooms are bright: milk glass and ink type. */
html[data-screen="hud"]:is([data-look="brass"], [data-look="dusk"]) {
  --glass: rgba(255, 255, 255, 0.72);
  --glass-edge: rgba(255, 255, 255, 0.95);
  --glass-shine: rgba(255, 255, 255, 0.7);
  --glass-shadow: 0 6px 18px rgba(92, 56, 60, 0.2);
  --hud-fg: #33262b;
  --hud-dim: rgba(51, 38, 43, 0.64);
  --boost-ink: #0a8ba3;
  --hot-ink: #c22d1f;
  --cap-hi: #ffffff;
  --cap: #f7efea;
  --cap-lo: #e0d0cb;
  --cap-ink: #33262b;
}

/* The race HUD (DOS-460): two icon rows in the top left corner (place, then
   lap or flags), the clock at the top, and the item box in the top right corner.
   Every size is a multiple of --hud-u (base.css), so a television's HUD is as
   large against its screen as a phone's. Each readout is white type on a dark
   slanted plate with one line of the track's metal under it: the same plate on
   a pale room and a dark one, so nothing has to be re-read from track to track. */
.hud,
.race-banner,
.slam,
.takeover-exit,
.screen--replay {
  --plate: rgba(20, 16, 26, 0.84);
  --plate-ink: #fff;
  --plate-dim: rgba(255, 255, 255, 0.66);
}

.hud {
  position: absolute;
  top: calc(var(--safe-top) + 10px);
  left: calc(var(--safe-left) + 12px);
  right: calc(var(--safe-right) + 12px);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas:
    "rows timer item"
    "map . ."
    "pause . .";
  align-items: start;
  gap: 0 8px;
  color: var(--plate-ink);
}

/* The plate: a parallelogram behind its row, drawn once and never animated. */
.hud__row::before,
.hud__timer::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--plate);
  border-radius: calc(var(--hud-u) * 4);
  box-shadow: 0 calc(var(--hud-u) * 3) calc(var(--hud-u) * 10) rgba(8, 9, 14, 0.28);
  transform: skewX(-12deg);
}

.hud__rows {
  grid-area: rows;
  justify-self: start;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Two rows tall whatever is up, with the lap row the lower one: a split that
     comes in a time trial takes the empty slot over it and nothing below moves. */
  justify-content: flex-end;
  min-height: calc(var(--hud-u) * 80);
  gap: calc(var(--hud-u) * 8);
  /* The icons hang off their plates' left ends. */
  padding-left: calc(var(--hud-u) * 10);
}

.hud__row {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: baseline;
  height: calc(var(--hud-u) * 36);
  padding: 0 calc(var(--hud-u) * 14) 0 calc(var(--hud-u) * 40);
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 900;
  line-height: calc(var(--hud-u) * 36);
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.hud__row[hidden] {
  display: none;
}

/* The sprites: baked in Blender on ARC's rose-gold tray (scripts/art/hud.py). */
.hud__icon {
  position: absolute;
  left: calc(var(--hud-u) * -12);
  top: 50%;
  width: calc(var(--hud-u) * 50);
  height: calc(var(--hud-u) * 50);
  transform: translateY(-50%);
  background: center / contain no-repeat;
  filter: drop-shadow(0 calc(var(--hud-u) * 2) calc(var(--hud-u) * 2) rgba(8, 9, 14, 0.4));
}

.hud__icon--flag {
  background-image: url("/assets/art/hud-flag.c2a70fdc.webp");
}

.hud__icon--track {
  background-image: url("/assets/art/hud-track.021e5555.webp");
}

.hud__count {
  display: flex;
  align-items: baseline;
}

/* "2" large, "/8" small: the number that changes is the one read at a glance. */
.hud__now {
  font-size: calc(var(--hud-u) * 30);
  transform-origin: 50% 70%;
}

.hud__of {
  margin-left: calc(var(--hud-u) * 2);
  font-size: calc(var(--hud-u) * 15);
  color: var(--plate-dim);
}

/* A new place or a new lap: the numeral lands from a little larger. Two names
   for one bump, so ui.ts replays it by flipping the mark. */
.hud__now[data-bump="a"] {
  animation: hud-bump-a var(--dur-ui) var(--ease-out);
}

.hud__now[data-bump="b"] {
  animation: hud-bump-b var(--dur-ui) var(--ease-out);
}

@keyframes hud-bump-a {
  from {
    transform: scale(1.35);
  }
}

@keyframes hud-bump-b {
  from {
    transform: scale(1.35);
  }
}

/* This lap's flags: a line of the track's metal filling along the plate's foot. */
.hud__bar {
  position: absolute;
  left: calc(var(--hud-u) * 6);
  right: calc(var(--hud-u) * 10);
  bottom: 0;
  height: calc(var(--hud-u) * 3);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.16);
  border-radius: 999px;
  transform: skewX(-12deg);
}

/* A sprint's row is the bar alone, across the middle of its plate. */
.hud__row--lap[data-sprint] {
  width: calc(var(--hud-u) * 97);
}

.hud__row--lap[data-sprint] .hud__bar {
  left: calc(var(--hud-u) * 42);
  right: calc(var(--hud-u) * 14);
  bottom: calc(50% - var(--hud-u) * 3);
  height: calc(var(--hud-u) * 6);
}

.hud__fill {
  display: block;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, var(--trim-lo), var(--trim) 60%, var(--trim-hi));
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-ui) var(--ease-out);
}

/* The race clock: mono numerals, upright on the slanted plate. */
.hud__timer {
  grid-area: timer;
  position: relative;
  z-index: 0;
  padding: calc(var(--hud-u) * 6) calc(var(--hud-u) * 14);
  font-family: var(--font-mono);
  font-size: calc(var(--hud-u) * 20);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The item box: the tray, always in its corner while the track has items; the
   item held shows in its glass (touch-controls.ts hangs .hud-item in here). */
.hud__item {
  grid-area: item;
  justify-self: end;
  position: relative;
  width: calc(var(--hud-u) * 76);
  height: calc(var(--hud-u) * 76);
  background: url("/assets/art/hud-item-box.9acb802f.webp") center / contain no-repeat;
  filter: drop-shadow(0 calc(var(--hud-u) * 3) calc(var(--hud-u) * 5) rgba(8, 9, 14, 0.35));
}

.hud__item[hidden] {
  display: none;
}

/* Pause (race-pause.ts): a round plate under the minimap, on the left, clear of the
   call-out stack that hangs under the item box on the right (combo, moves, feed)
   and above the stick's zone. A thumb-sized target, and the HUD's only button. */
.hud__pause {
  grid-area: pause;
  justify-self: start;
  /* Over the touch layer's look pad (touch.css, z-index 5), which covers the screen's right half. */
  position: relative;
  z-index: 6;
  display: grid;
  place-items: center;
  width: calc(var(--hud-u) * 44);
  height: calc(var(--hud-u) * 44);
  margin: calc(var(--hud-u) * 8) 0 0 calc(var(--hud-u) * 6);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--plate);
  color: var(--plate-ink);
  box-shadow: 0 calc(var(--hud-u) * 3) calc(var(--hud-u) * 10) rgba(8, 9, 14, 0.28);
  pointer-events: auto;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 120ms var(--ease-out);
}

.hud__pause svg {
  width: 46%;
  height: 46%;
}

.hud__pause:active {
  transform: scale(0.92);
}

.hud__pause[hidden] {
  display: none;
}

/* The minimap (minimap.ts): one lap from above with a dot a racer, under the
   rows. Two canvases the size of the drawing, the dots over the track. */
.hud__map {
  grid-area: map;
  justify-self: start;
  position: relative;
  width: calc(var(--hud-u) * 150);
  height: calc(var(--hud-u) * 124);
  margin-top: calc(var(--hud-u) * 10);
}

.hud__map canvas {
  position: absolute;
  top: 0;
  left: 0;
}

/* A time trial's split against the best run, in the place row's slot (a trial
   has no place): cyan ahead, red behind. It drops in, holds, and fades (ui.ts
   setSplit); a split that comes while one is up only changes the number. */
.hud__split {
  padding-left: calc(var(--hud-u) * 14);
  font-family: var(--font-mono);
  font-style: normal;
  font-weight: 700;
  font-size: calc(var(--hud-u) * 18);
  opacity: 0;
  transform: translateY(-4px) scale(0.96);
  transform-origin: 0 50%;
  transition:
    opacity var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.hud__split[data-shown] {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-ui) var(--ease-out),
    transform var(--dur-ui) var(--ease-out);
}

.hud__split[data-split="ahead"] {
  color: var(--boost);
}

.hud__split[data-split="behind"] {
  color: color-mix(in srgb, var(--hot), #fff 28%);
}

@media (prefers-reduced-motion: reduce) {
  .hud__split {
    transform: none;
  }

  .hud__now[data-bump] {
    animation: none;
  }
}

.hud-note {
  position: absolute;
  top: calc(var(--safe-top) + 112px);
  left: 50%;
  transform: translateX(-50%);
  max-width: calc(100% - 28px);
  margin: 0;
  padding: 7px 16px;
  font-size: 16px;
  font-weight: 700;
  text-align: center;
  color: var(--hud-fg);
  border-radius: var(--radius-l);
  background: linear-gradient(180deg, var(--glass-shine), transparent 60%), var(--glass);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-shadow);
}

.hud-note[hidden] {
  display: none;
}

/* ---------- The call: FINISH!, NEW RECORD!, CAUGHT! ----------
   One word across the whole screen on the HUD's plate, tilted like a referee's
   call. The band sweeps in from the left and the word slams onto it. The type
   is sized by the word's length (ui.ts), so the longest call still fits a phone. */
.slam {
  position: absolute;
  top: 25%;
  left: 0;
  right: 0;
  z-index: 0;
  display: grid;
  place-items: center;
  height: 1.56em;
  font-family: var(--font-display);
  font-size: min(calc(108vw / var(--slam-chars, 7)), 16vh, 220px);
  font-style: italic;
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
  color: var(--plate-ink);
  transform: rotate(-4deg);
}

.slam[hidden] {
  display: none;
}

.slam::before {
  content: "";
  position: absolute;
  inset: 0 -12%;
  z-index: -1;
  background: var(--plate);
  box-shadow:
    0 0.07em 0 var(--trim),
    0 0.2em 0.5em rgba(8, 9, 14, 0.3);
  animation: slam-band 260ms var(--ease-out) both;
}

.slam__word {
  animation: slam-word 300ms var(--ease-out) 60ms both;
}

/* A record is gold, with dark type; a run that ended early is red. */
.slam[data-tone="gold"] {
  --plate: #ffcf4a;
  --trim: #fff;
  color: #2a1c05;
}

.slam[data-tone="hot"] {
  --plate: var(--hot);
  --trim: #fff;
}

@keyframes slam-band {
  from {
    transform: translateX(-100%);
  }
}

@keyframes slam-word {
  from {
    transform: scale(1.7);
    opacity: 0;
  }
}

/* ---------- Call-outs: the combo counter, the PERFECT / NICE pop and the status words ----------
   One stack in the top right corner, under the item box (--callout-top, base.css),
   so nothing is drawn over the road ahead of the runner. Each has its own slot:
   the stack never reflows when a line comes or goes. */

/* Status feed (status-feed.ts): one word per line, the stack's last slot, growing down.
   Each word sits on its own dark, slanted plate in the word's colour, like the combo's
   "Chain" tag, so it reads the same over a pale room, a dark one and a sculpture passing behind. */
.status-feed {
  position: absolute;
  right: var(--callout-right);
  top: calc(var(--callout-top) + 88px);
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  /* Hung from the right edge, so the longest word ("Perfect Jump") is never cut. */
  align-items: flex-end;
  gap: 3px;
  pointer-events: none;
}

.status-feed__line {
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
  padding: 4px 6px 4px 8px;
  border-radius: 4px;
  color: color-mix(in srgb, var(--feed-colour, #fff), #fff 55%);
  background: color-mix(in srgb, var(--feed-colour, #fff), #1c1020 80%);
  transform: skewX(-12deg);
}

/* Punch text (DOS-309, DOS-442): slanted display type cast in the look's polished
   metal, tinted by --pop-colour (FEED_LOOK): a bright top, a hard horizon and a
   dark extruded edge, so it reads on a pale room and a dark one alike. Scaled by
   --punch (0..1, status-feed.ts punchOf): Nice 0.2, Perfect 0.5, a combo 0.6 at
   its third link up to 1 at its ninth; a better move is bigger, slams in from
   further and runs hotter. */
.move-pop,
.combo {
  --pop-hi: color-mix(in srgb, var(--pop-colour, #fff), #fff calc(58% + var(--punch, 0) * 30%));
  --pop-lo: color-mix(in srgb, var(--pop-colour, #fff), #2a1430 34%);
  --pop-edge: color-mix(in srgb, var(--pop-colour, #fff), #1c1020 76%);
  position: absolute;
  right: var(--callout-right);
  /* It grows out of the corner it hangs from. */
  transform-origin: 100% 50%;
  pointer-events: none;
  z-index: 6;
}

.move-pop__word,
.combo__count {
  font-family: var(--font-display);
  font-weight: 900;
  font-style: italic;
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Room for the slant: clipped to the glyphs, the fill stops at the box. */
  padding: 0.08em 0.2em 0.1em 0.08em;
  color: transparent;
  background: linear-gradient(
    180deg,
    #fff 0%,
    var(--pop-hi) 34%,
    var(--pop-colour, #fff) 50%,
    var(--pop-lo) 54%,
    var(--pop-colour, #fff) 74%,
    var(--pop-hi) 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  filter: drop-shadow(1px 0 0 var(--pop-edge)) drop-shadow(-1px 0 0 var(--pop-edge))
    drop-shadow(0 -1px 0 var(--pop-edge)) drop-shadow(0 3px 0 var(--pop-edge))
    drop-shadow(0 5px 6px rgba(28, 16, 32, 0.35));
}

/* The combo counter: the stack's first slot, a big number and a small "Chain" tag. */
.combo {
  top: var(--callout-top);
  display: none;
  align-items: center;
  gap: 2px;
}

.combo--hit-a,
.combo--hit-b,
.combo--drop {
  display: flex;
}

.combo__count {
  font-size: calc(30px + 16px * var(--punch, 0));
}

.combo__label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 4px 4px 3px 7px;
  border-radius: 4px;
  color: #fff;
  background: var(--pop-edge);
  transform: skewX(-12deg);
}

.combo--hit-a {
  animation: punch-in var(--dur-pop) var(--ease-spring) both;
}

.combo--hit-b {
  animation: punch-in-again var(--dur-pop) var(--ease-spring) both;
}

/* The chain broke: the metal goes dull, tips over and drops away. */
.combo--drop {
  --pop-colour: #a39cae;
  animation: combo-drop var(--dur-pop) var(--ease-out) forwards;
}

/* In from the screen's edge, oversized, then a short shake as it seats. */
@keyframes punch-in {
  0% {
    opacity: 0;
    transform: translateX(30%) scale(calc(1.25 + 0.5 * var(--punch, 0)));
  }
  24% {
    opacity: 1;
    transform: scale(0.92);
  }
  40% {
    transform: translateX(calc(-5px * var(--punch, 0))) scale(1.06) rotate(-3deg);
  }
  58% {
    transform: translateX(calc(3px * var(--punch, 0))) scale(1.02) rotate(1.5deg);
  }
  100% {
    opacity: 1;
    transform: none;
  }
}

@keyframes punch-in-again {
  0% {
    opacity: 0;
    transform: translateX(30%) scale(calc(1.25 + 0.5 * var(--punch, 0)));
  }
  24% {
    opacity: 1;
    transform: scale(0.92);
  }
  40% {
    transform: translateX(calc(-5px * var(--punch, 0))) scale(1.06) rotate(-3deg);
  }
  58% {
    transform: translateX(calc(3px * var(--punch, 0))) scale(1.02) rotate(1.5deg);
  }
  100% {
    opacity: 1;
    transform: none;
  }
}

/* Out the way it came: back toward the edge. */
@keyframes pop-away {
  to {
    opacity: 0;
    transform: translateX(14px);
  }
}

/* The same exit under another name: it restarts with punch-in-again, so a
   pop that lands while the last one is still up gets its full time. */
@keyframes pop-away-again {
  to {
    opacity: 0;
    transform: translateX(14px);
  }
}

@keyframes combo-drop {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translateY(16px) rotate(8deg) scale(0.9);
  }
}

@media (prefers-reduced-motion: reduce) {
  .combo--hit-a,
  .combo--hit-b,
  .combo--drop {
    animation: none;
  }
}

.controls-hint {
  position: absolute;
  bottom: calc(var(--safe-bottom) + 12px);
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 16px;
  color: var(--hud-fg);
  border-radius: 999px;
  font-size: 14px;
  white-space: nowrap;
  transition: opacity var(--dur-pop) var(--ease-out);
  background: linear-gradient(180deg, var(--glass-shine), transparent 60%), var(--glass);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-shadow);
}

.controls-hint--faded {
  opacity: 0;
}

/* ---------- countdown and closing banner (shared by HUD, spectators and TV) ---------- */

.countdown[hidden],
.closing[hidden] {
  display: none;
}

/*
 * The map title card and the 3·2·1 sit in one column in the upper third, just
 * under the HUD's top row, so neither covers the starting pack or its name tags.
 */
.race-banner {
  --banner-top: 86px;
  position: fixed;
  z-index: 20;
  top: calc(var(--safe-top) + var(--banner-top));
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  pointer-events: none;
}

@media (max-width: 520px) {
  .race-banner {
    --banner-top: 120px;
  }
}

/* 3, 2, 1: a white numeral on the HUD's dark slanted plate, large enough to be
   the only thing read. It slams in from larger and stops dead, with no bounce. */
.countdown {
  position: relative;
  z-index: 0;
  display: grid;
  place-items: center;
  min-width: 1.5em;
  height: 1.3em;
  padding: 0 0.3em 0 0.24em;
  font-family: var(--font-display);
  font-size: clamp(64px, min(34vw, 22vh), 240px);
  font-style: italic;
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.04em;
  white-space: nowrap;
  color: var(--plate-ink);
  transform-origin: 50% 55%;
}

.countdown::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--plate);
  border-radius: 0.06em;
  box-shadow:
    0 0.05em 0 var(--trim),
    0 0.12em 0.3em rgba(8, 9, 14, 0.3);
  transform: skewX(-12deg);
}

.countdown--pop {
  animation: count-slam 220ms var(--ease-out) both;
}

@keyframes count-slam {
  from {
    transform: scale(1.45);
    opacity: 0;
  }
}

/* GO! is up for 450 ms (race-director.ts): it slams in, holds, and is gone
   before it is taken down, so it never blinks off. */
@keyframes go-slam {
  0% {
    transform: scale(1.6);
    opacity: 0;
  }

  36%,
  74% {
    transform: none;
    opacity: 1;
  }

  100% {
    transform: scale(1.06);
    opacity: 0;
  }
}

.closing {
  position: fixed;
  z-index: 15;
  top: calc(var(--safe-top) + 112px);
  left: 50%;
  transform: translateX(-50%);
  max-width: calc(100% - 28px);
  padding: 7px 18px;
  font-size: 16px;
  font-weight: 700;
  text-align: center;
  color: #fff;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 60%), var(--hot);
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 999px;
  box-shadow: var(--glass-shadow);
  pointer-events: none;
}

html[data-screen="hud"] .hud-note:not([hidden]) ~ .closing,
html[data-screen="hud"] .closing {
  top: calc(var(--safe-top) + 160px);
}

html[data-screen="watch"] .closing {
  top: calc(var(--safe-top) + 100px);
}

html[data-screen="tv"] .closing {
  top: auto;
  bottom: 5vh;
  font-size: calc(min(1vw, 1.8vh) * 3);
}

html:not([data-screen="hud"]):not([data-screen="watch"]):not([data-screen="tv"]) .closing,
html:not([data-screen="hud"]):not([data-screen="watch"]):not([data-screen="tv"]) .countdown {
  display: none;
}

/* ---------- spectating on a phone ---------- */

.screen--watch {
  grid-template-rows: none;
  place-items: start stretch;
  align-content: start;
  gap: 10px;
}

.watch-card {
  display: grid;
  gap: 2px;
  justify-self: center;
  max-width: 420px;
  padding: 10px 16px;
  text-align: center;
  background: var(--slab);
  border: 1px solid var(--slab-rim);
  border-radius: var(--radius-m);
  box-shadow: var(--slab-lit), var(--shadow-toy);
}

.watch-card__title {
  font-family: var(--font-display);
  font-size: 17px;
}

.watch-leave {
  position: absolute;
  left: calc(var(--safe-left) + 14px);
  bottom: calc(var(--safe-bottom) + 14px);
  pointer-events: auto;
}

.standings {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}

.standings--mini {
  justify-self: end;
  width: min(220px, 60vw);
}

.standings__row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 2px 10px;
  color: var(--cream);
  font-weight: 800;
  background: rgba(18, 20, 28, 0.7);
  border-radius: 999px;
}

.standings__row--done {
  background: var(--boost-lo);
}

.standings__place {
  width: 1.4em;
  text-align: center;
}

.standings__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.standings__time {
  font-variant-numeric: tabular-nums;
  font-size: 0.85em;
}

.tv-race .standings__row {
  min-height: calc(min(1vw, 1.8vh) * 5);
  font-size: calc(min(1vw, 1.8vh) * 2.4);
}

.tv-race .player__dot {
  width: 1em;
  height: 1em;
}

/* A phone that is watching this round has no runner, so no thumb stick or buttons.
   Specific enough to beat both "show" rules in touch.css, which loads after this file. */
html[data-spectating="true"][data-screen] .touch,
html[data-spectating="true"][data-screen][data-touch] .touch {
  display: none;
}

/* ---------- map title card over the countdown ---------- */

.title-card {
  position: relative;
  z-index: 0;
  max-width: calc(100% - 40px);
  padding: 0.34em 1.1em 0.4em;
  font-size: clamp(17px, 2.6vmin, 30px);
  text-align: center;
  pointer-events: none;
  animation: plate-in var(--dur-ui) var(--ease-out) both;
}

/* The track's name on the same plate as every HUD readout. */
.title-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--plate);
  border-radius: 0.2em;
  box-shadow:
    0 0.12em 0 var(--trim),
    0 0.3em 0.7em rgba(8, 9, 14, 0.28);
  transform: skewX(-12deg);
}

.title-card[hidden] {
  display: none;
}

.title-card__name {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 900;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--plate-ink);
}

/* The plate drops into place from just above. */
@keyframes plate-in {
  from {
    opacity: 0;
    transform: translateY(-10px) scale(0.96);
  }
}

html:not([data-screen="hud"]):not([data-screen="watch"]):not([data-screen="tv"]) .title-card {
  display: none;
}

/*
 * Desktop look hint (mouse-look.ts appends it, touch.css places it): replayed
 * from the moment the race screen appears, countdown included, then faded.
 * Raised above the countdown banner so nothing ever covers it.
 */
html[data-screen="hud"] .look-hint.is-showing {
  z-index: 21;
  animation-name: look-hint-replay;
}

html[data-screen="hud"] .look-hint.is-showing.is-locked {
  animation-name: look-hint-fade-locked;
}

/* Held at full strength through the 3·2·1; the fade restarts from GO. */
html[data-screen="hud"][data-countdown="true"] .look-hint {
  animation: none;
  opacity: 1;
}

@keyframes look-hint-replay {
  0%,
  80% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/* ---------- announcer pops ("Perfect!", "Shortcut!", "Honey Rush!", "… takes the lead!") ---------- */

.announcer {
  z-index: 19;
  top: calc(var(--safe-top) + 96px);
}

@media (max-width: 520px) {
  .announcer {
    top: calc(var(--safe-top) + 104px);
  }
}

.announcer__line {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: calc(100vw - 32px);
  padding: 7px 16px 7px 12px;
  font-size: clamp(14px, 3.9vw, 19px);
  font-weight: 700;
  line-height: 1.2;
  color: var(--hud-fg);
  border-radius: var(--radius-l);
  background: linear-gradient(180deg, var(--glass-shine), transparent 60%), var(--glass);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-shadow);
}

/* The line's tone is a bead of colour ahead of the words. */
.announcer__line::before {
  content: "";
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fff, var(--tone, var(--finish)) 55%);
}

.announcer__line[hidden] {
  display: none;
}

.announcer__line[data-tone="coral"] {
  --tone: var(--hot);
}

.announcer__line[data-tone="mint"] {
  --tone: var(--boost);
}

.announcer__line.is-popping {
  animation: announcer-pop 1300ms linear both;
}

@keyframes announcer-pop {
  /* In from just above over 230 ms, a hold, and out faster than it came. */
  0% {
    opacity: 0;
    transform: translateY(-8px) scale(0.96);
    animation-timing-function: var(--ease-out);
  }
  18% {
    opacity: 1;
    transform: none;
  }
  86% {
    opacity: 1;
    transform: none;
    animation-timing-function: var(--ease-out);
  }
  100% {
    opacity: 0;
    transform: translateY(-6px);
  }
}

/* Reduce Motion: the line stays up just as long and only fades (see base.css). */
@media (prefers-reduced-motion: reduce) {
  .announcer__line.is-popping {
    animation: announcer-fade 1300ms ease-out both;
    animation-duration: 1300ms !important;
  }
}

@keyframes announcer-fade {
  0%,
  100% {
    opacity: 0;
  }
  18%,
  78% {
    opacity: 1;
  }
}

/*
 * Race screen, 3·2·1: the HUD (a 00:00 timer, an empty bar) steps aside, the
 * track's name takes the top and the numeral stands under it, above the
 * starting pack. The HUD comes back with GO!.
 */
html[data-screen="hud"] .race-banner:not(.announcer) {
  top: calc(var(--safe-top) + 14px);
  left: var(--safe-left);
  right: var(--safe-right);
  flex-direction: column;
  gap: clamp(12px, 2.4vh, 28px);
  padding: 0 10px;
}

html[data-screen="hud"][data-countdown="true"] .hud {
  visibility: hidden;
  opacity: 0;
}

.hud {
  transition: opacity var(--dur-ui) var(--ease-out);
}

/* A landscape phone has no height for both: the name sits beside the numeral. */
@media (orientation: landscape) and (max-height: 500px) {
  html[data-screen="hud"] .race-banner:not(.announcer) {
    flex-direction: row;
    justify-content: center;
    gap: 28px;
  }
}

/* The GPU dropped its context: the picture is frozen until it comes back. */
.gpu-note {
  position: fixed;
  z-index: 25;
  left: 50%;
  bottom: calc(var(--safe-bottom) + 40%);
  transform: translateX(-50%);
  margin: 0;
  padding: 8px 16px;
  font-size: 15px;
  font-weight: 700;
  color: var(--hud-fg);
  border-radius: 999px;
  pointer-events: none;
  background: linear-gradient(180deg, var(--glass-shine), transparent 60%), var(--glass);
  border: 1px solid var(--glass-edge);
  box-shadow: var(--glass-shadow);
}

.gpu-note[hidden] {
  display: none;
}

/* GO!: the HUD is back, so the word drops below its rows and the item box.
   The plate turns the go colour, the same cyan as a dash pad, with dark type. */
html[data-screen="hud"][data-countdown="go"] .race-banner:not(.announcer) {
  top: calc(var(--safe-top) + 26px + var(--hud-u) * 80);
}

html[data-countdown="go"] .countdown {
  --plate: var(--boost);
  --trim: #fff;
  font-size: clamp(60px, min(27vw, 20vh), 220px);
  color: #0a1a22;
}

html[data-countdown="go"] .countdown--pop {
  animation: go-slam 450ms var(--ease-out) both;
}

/* Reduce Motion cuts every animation to its last frame (base.css), and GO!'s last frame is gone. */
@media (prefers-reduced-motion: reduce) {
  html[data-countdown="go"] .countdown--pop {
    animation: none;
  }
}

/* On-screen phone controls (src/client/touch-controls.ts).
   Shown only on the HUD screen of a touch device: a coarse pointer, or once a
   touch has been seen (html[data-touch], set by touch-controls.ts). */

.touch {
  position: absolute;
  inset: 0;
  display: none;
  pointer-events: none;
  z-index: 5;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

html[data-screen="hud"][data-touch] .touch {
  display: block;
}

@media (pointer: coarse) {
  html[data-screen="hud"] .touch {
    display: block;
  }
  #controls-hint {
    display: none;
  }
}

html[data-touch] #controls-hint {
  display: none;
}

/* Left thumb area: the stick appears wherever the thumb lands in here. */
.touch__zone {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 58%;
  height: 62%;
  pointer-events: auto;
  touch-action: none;
}

.touch__base {
  position: absolute;
  /* Resting spot (a hint of where to put the thumb) until a touch moves it. */
  left: calc(84px + env(safe-area-inset-left));
  top: calc(100% - 112px - env(safe-area-inset-bottom));
  width: 112px;
  height: 112px;
  margin: -56px 0 0 -56px;
  border-radius: 50%;
  /* A dish of clear glass: thicker toward the rim, with a hairline that shows on any floor. */
  background: radial-gradient(
    circle at 50% 50%,
    color-mix(in srgb, var(--glass) 18%, transparent) 0%,
    color-mix(in srgb, var(--glass) 34%, transparent) 62%,
    color-mix(in srgb, var(--glass) 60%, transparent) 100%
  );
  border: 1px solid var(--glass-edge);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--hud-fg) 20%, transparent);
  opacity: 0.8;
  transition: opacity var(--dur-fast) ease;
  pointer-events: none;
}

.touch__base.is-active {
  opacity: 1;
  transition: none;
}

.touch__knob {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 54px;
  height: 54px;
  margin: -27px 0 0 -27px;
  border-radius: 50%;
  /* Opaque, many-stop radial shading, a polished ball of the trim metal: smooth
     on every DPR, no translucency for the canvas to band through. */
  background: radial-gradient(
    circle at 36% 28%,
    #fff 0%,
    var(--trim-hi) 16%,
    color-mix(in srgb, var(--trim-hi), var(--trim)) 38%,
    var(--trim) 60%,
    color-mix(in srgb, var(--trim), var(--trim-lo)) 82%,
    var(--trim-lo) 100%
  );
  box-shadow:
    0 6px 12px rgba(12, 13, 17, 0.28),
    inset 0 -3px 6px rgba(12, 13, 17, 0.16);
}

.touch__base.is-active .touch__knob {
  box-shadow:
    0 6px 12px rgba(12, 13, 17, 0.28),
    inset 0 -3px 6px rgba(12, 13, 17, 0.16),
    0 0 0 3px color-mix(in srgb, var(--boost) 80%, transparent);
}

.touch__btn {
  position: absolute;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 0;
  color: var(--hud-fg);
  font-weight: 800;
  font-size: 16px;
  letter-spacing: 0.02em;
  box-shadow: var(--glass-shadow);
  pointer-events: auto;
  touch-action: none;
  /* A game control answers at once: 60 ms. */
  transition: transform 60ms var(--ease-out);
}

.touch__btn--jump {
  /* Jump keeps >= 10 px of clear space from Action's gauge ring and its Perfect
     ring (14.6 px to the gauge ring, 10 px to the Perfect ring at its widest). */
  right: calc(12px + env(safe-area-inset-right));
  bottom: calc(28px + env(safe-area-inset-bottom));
  width: 96px;
  height: 96px;
  /* Jump is the biggest thing under the thumb: a lacquer cap (::before) set in a
     bezel of turned trim metal, which a conic sweep lights as a lathe would. */
  isolation: isolate;
  background: conic-gradient(
    from 140deg,
    var(--trim-hi),
    var(--trim) 12%,
    var(--trim-lo) 25%,
    var(--trim) 38%,
    var(--trim-hi) 50%,
    var(--trim) 62%,
    var(--trim-lo) 75%,
    var(--trim) 88%,
    var(--trim-hi)
  );
  box-shadow:
    0 10px 20px -4px rgba(12, 13, 17, 0.36),
    inset 0 0 0 1px rgba(255, 255, 255, 0.4);
  color: var(--cap-ink);
  font-family: var(--font-display);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.touch__btn--jump::before {
  content: "";
  position: absolute;
  inset: 7px;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 16%, var(--cap-hi), var(--cap) 56%, var(--cap-lo));
  box-shadow:
    0 0 0 1px rgba(12, 13, 17, 0.22),
    inset 0 1px 0 rgba(255, 255, 255, 0.5),
    inset 0 -5px 9px rgba(12, 13, 17, 0.14);
}

/* Pressed, the cap sits down into its bezel. */
.touch__btn--jump.is-pressed::before {
  box-shadow:
    0 0 0 1px rgba(12, 13, 17, 0.22),
    inset 0 4px 9px rgba(12, 13, 17, 0.3);
}

/* Action: context icon over a label, both in fixed boxes so a context change
   never shifts layout. The icon shown is picked by data-action (action-button.ts). */
.touch__btn--action {
  right: calc(128px + env(safe-area-inset-right));
  bottom: calc(52px + env(safe-area-inset-bottom));
  width: 80px;
  height: 80px;
  /* Action is a glass dial: its gauge ring and icon carry the colour. */
  color: var(--boost-ink);
  background: linear-gradient(180deg, var(--glass-shine), transparent 60%), var(--glass);
  box-shadow:
    inset 0 0 0 1px var(--glass-edge),
    var(--glass-shadow);
  grid-template-rows: 30px 18px;
  align-content: center;
  justify-items: center;
  gap: 1px;
  font-size: 14px;
}

.touch__icons {
  position: relative;
  width: 30px;
  height: 30px;
}

.touch__icon {
  position: absolute;
  inset: 0;
  width: 30px;
  height: 30px;
  display: none;
}

.touch__label {
  height: 18px;
  line-height: 18px;
}

.touch__btn--action[data-action="dash"] .touch__icon--dash,
.touch__btn--action[data-action="grab"] .touch__icon--grab,
.touch__btn--action[data-action="parry"] .touch__icon--parry {
  display: block;
}

.touch__btn--action[data-action="parry"] {
  color: var(--ink);
  background: radial-gradient(circle at 50% 16%, #fff3c4, var(--finish) 60%);
}

/* Parry is a timing window: the shield pulses so it reads as "now!". */
.touch__btn--action[data-action="parry"] .touch__icon--parry {
  animation: parry-pulse 520ms ease-in-out infinite;
}

@keyframes parry-pulse {
  50% {
    transform: scale(1.22);
  }
}

@media (prefers-reduced-motion: reduce) {
  .touch__btn--action[data-action="parry"] .touch__icon--parry {
    animation: none;
  }
}

.touch__btn.is-pressed {
  transform: scale(0.95);
  box-shadow: 0 2px 6px rgba(12, 13, 17, 0.3);
}

/* Small phones (320 px wide): keep the buttons thumb-sized but leave room for the stick. */
@media (max-width: 360px) {
  .touch__btn--jump {
    width: 88px;
    height: 88px;
    right: calc(8px + env(safe-area-inset-right));
  }
  .touch__btn--action {
    width: 76px;
    height: 76px;
    right: calc(116px + env(safe-area-inset-right));
  }
}

/* Right-half dead zone: touches that miss a button do nothing (no camera look,
   no page pan or zoom, DOS-301). First child of .touch, so the stick zone and
   buttons stack above it. */
.touch__look {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 50%;
  pointer-events: auto;
  touch-action: none;
}

/* Desktop mouse-look hint (src/client/mouse-look.ts): race HUD on a mouse only. */
.look-hint {
  position: absolute;
  /* Clear of the keys' hint below it and of the gauge above. */
  bottom: calc(env(safe-area-inset-bottom) + 56px);
  left: 50%;
  transform: translateX(-50%);
  display: none;
  padding: 5px 14px;
  background: var(--glass);
  border: 1px solid var(--glass-edge);
  color: var(--hud-fg);
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  pointer-events: none;
  animation: look-hint-fade 9s ease forwards;
}

.look-hint.is-locked {
  animation-name: look-hint-fade-locked;
}

html[data-screen="hud"] .look-hint {
  display: block;
}

html[data-touch] .look-hint {
  display: none;
}

@media (pointer: coarse) {
  .look-hint {
    display: none !important;
  }
}

@keyframes look-hint-fade {
  0%,
  80% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/* Same curve under another name, so entering mouse-look replays the hint. */
@keyframes look-hint-fade-locked {
  0%,
  80% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/* ---------- Ribbon dash gauge (src/client/gauge-hud.ts) ---------- */

/* Phones: a stitched ribbon ring hugging the Action button. One stroke-dashoffset
   write per gauge change; the ring is outside the grid flow, so no layout. */
.gauge-ring {
  position: absolute;
  inset: -10px;
  width: calc(100% + 20px);
  height: calc(100% + 20px);
  pointer-events: none;
  overflow: visible;
}

.gauge-ring__edge,
.gauge-ring__track,
.gauge-ring__fill {
  fill: none;
  stroke-linecap: round;
}

/* A pale outline under the track keeps the ring readable over any sky. */
.gauge-ring__edge {
  stroke: var(--glass);
  stroke-width: 11;
}

/* Dark track: the empty part of the gauge. */
.gauge-ring__track {
  stroke: color-mix(in srgb, var(--hud-fg) 20%, transparent);
  stroke-width: 7;
}

/* Bright fill over the track; colour follows the level band (data-gauge). */
.gauge-ring__fill {
  stroke: #22b8d4;
  stroke-width: 7;
  transition: stroke 160ms ease;
}

.gauge-ring__cap-dot {
  fill: #fff;
  stroke: color-mix(in srgb, var(--hud-fg) 60%, transparent);
  stroke-width: 1.5;
}

.touch__btn--action[data-gauge="full"] .gauge-ring__fill {
  stroke: var(--boost);
  filter: drop-shadow(0 0 3px rgba(47, 224, 255, 0.9));
}

.touch__btn--action[data-gauge="full"] .gauge-ring__cap-dot {
  fill: var(--finish);
}

.touch__btn--action[data-gauge="low"] .gauge-ring__fill {
  stroke: var(--ramp);
}

.touch__btn--action[data-gauge="empty"] .gauge-ring__fill,
.touch__btn--action[data-gauge="empty"] .gauge-ring__cap-dot {
  display: none;
}

/* Out of breath: red ribbon and a gentle shake (the ring, so Action's pressed
   transform is untouched). */
.touch__btn--action.is-breathless .gauge-ring__fill {
  stroke: var(--hot);
}

.touch__btn--action.is-breathless .gauge-ring__track {
  stroke: var(--hot-lo);
}

.touch__btn--action.is-breathless .gauge-ring {
  animation: gauge-shake 420ms ease-in-out infinite;
}

@keyframes gauge-shake {
  0%,
  100% {
    transform: translateX(0) rotate(0deg);
  }
  25% {
    transform: translateX(1.5px) rotate(-3deg);
  }
  75% {
    transform: translateX(-1.5px) rotate(3deg);
  }
}

/* Perfect landing dash: the gauge turns gold while its word is up (gauge-hud.ts).
   No ring comes out of it (Do, 14 Nov). */
.touch__btn--action.is-perfect .gauge-ring__fill {
  stroke: var(--finish);
  transition-duration: 0s;
}

/* Desktop: a small ribbon bar at the bottom centre (mouse/keyboard HUD only). */
.gauge-bar {
  position: absolute;
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom) + 100px);
  width: 180px;
  height: 10px;
  margin-left: -90px;
  display: none;
  border: 1px solid var(--glass-edge);
  border-radius: 999px;
  background: var(--glass);
  overflow: hidden;
  pointer-events: none;
}

.gauge-bar__fill {
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, #22b8d4, var(--boost));
  transform-origin: left center;
}

.gauge-bar.is-breathless {
  animation: gauge-bar-shake 420ms ease-in-out infinite;
}

.gauge-bar.is-breathless .gauge-bar__fill {
  background: var(--hot);
}

@keyframes gauge-bar-shake {
  25% {
    transform: translateX(2px);
  }
  75% {
    transform: translateX(-2px);
  }
}

html[data-screen="hud"] .gauge-bar {
  display: block;
}

html[data-touch] .gauge-bar {
  display: none;
}

/* ---------- "Perfect!" / "Nice" pop (landing dash) ---------- */

/* The combo counter's punch text (hud.css), scaled by --punch from gauge-hud.ts:
   a Nice is modest, a Perfect strong. The call-out stack's second slot, under
   the combo: it punches in from the edge, holds, then slips back out. */
.move-pop {
  top: calc(var(--callout-top) + 56px);
  display: none;
  font-size: calc(15px + 12px * var(--punch, 0));
  /* Its two animations share transform, so the browser runs them on the main
     thread and gives it no layer: the word was drawn again at every size on the
     way in. With a layer and the shadows on it, the five shadows were worked out
     again on every frame it moved instead: 21 missed refreshes a Perfect at
     240 Hz (DOS-461). So the word and its shadows are a child, drawn once into
     this layer, and only the finished picture moves. */
  will-change: transform, opacity;
}

.move-pop__word {
  display: block;
}

.move-pop--perfect,
.move-pop--nice {
  display: block;
  animation:
    punch-in var(--dur-pop) var(--ease-spring) both,
    pop-away var(--dur-ui) var(--ease-out) 760ms forwards;
}

/* Other names for the same punch and float: gauge-hud.ts flips this class on
   every pop while one is up, so each pop replays from the start, whatever
   its grade. */
.move-pop--again {
  animation-name: punch-in-again, pop-away-again;
}

/* ---------- Item bubble (src/client/item-bubble.ts) ---------- */

/* Directly above Action, inside the right thumb's arc and clear of the gauge
   ring (ring top = Action top + 9 px; 10 px gap above that). */
.touch__btn--item {
  /* 64 px, centred over Action (centre right 168 px), 10 px above its gauge ring (top 142 px). */
  right: calc(136px + env(safe-area-inset-right));
  bottom: calc(152px + env(safe-area-inset-bottom));
  width: 64px;
  height: 64px;
  /* Dark glass in a polished ring: the bubble over a runner's head, to press. */
  border: 3px solid transparent;
  background:
    radial-gradient(circle at 50% 8%, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0) 52%)
    padding-box,
    linear-gradient(rgba(18, 20, 28, 0.9), rgba(18, 20, 28, 0.9)) padding-box,
    conic-gradient(from 215deg, #fff, #8d909b 18%, #f3f3f6 38%, #62656f 60%, #dcdde2 80%, #fff)
    border-box;
  color: var(--cream);
  text-shadow: none;
  box-shadow: var(--glass-shadow);
}

.touch__btn--item[hidden],
.hud-item[hidden] {
  display: none;
}

/* One icon shows through the window; --item-icon is its edge (the HUD's box draws it larger). */
.item-window {
  position: relative;
  display: block;
  width: var(--item-icon, 30px);
  height: var(--item-icon, 30px);
  overflow: hidden;
}

.item-strip {
  display: flex;
  flex-direction: column;
}

.item-icon {
  flex: none;
  width: var(--item-icon, 30px);
  height: var(--item-icon, 30px);
}

/* The strip is three icons tall: a third of itself up per icon. */
[data-item="paperCrane"] .item-strip {
  transform: translateY(calc(-100% / 3));
}
[data-item="bubbleBonnet"] .item-strip {
  transform: translateY(calc(-200% / 3));
}

/* The 0.8 s roulette: the strip spins through every icon. */
[data-rolling] .item-strip {
  animation: item-roll 150ms steps(3) infinite;
}

@keyframes item-roll {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-100%);
  }
}

/* An item arriving springs in and glows while the roulette spins, then a thin
   ring bursts off it when it lands, so a first-timer notices it. Both are one
   gold outline on its own layer (::before): a soft halo while rolling, a bare
   stroke for the burst. Each is drawn once; only opacity and transform animate. */
.touch__btn--item::before,
.hud-item::before {
  content: "";
  position: absolute;
  inset: -3px;
  border: 3px solid rgba(242, 201, 76, 0.95);
  border-radius: inherit;
  opacity: 0;
  pointer-events: none;
}

.touch__btn--item[data-rolling]::before,
.hud-item[data-rolling]::before {
  box-shadow: 0 0 18px 6px rgba(242, 201, 76, 0.85);
  animation: item-glow 400ms ease-in-out infinite;
}

.touch__btn--item.is-arriving,
.hud-item.is-arriving .item-window {
  animation: item-pop var(--dur-ui) var(--ease-spring) both;
}

.touch__btn--item.is-landed::before,
.hud-item.is-landed::before {
  animation: item-landed var(--dur-pop) var(--ease-out);
}

/* --ease-spring carries it past full size and back. */
@keyframes item-pop {
  from {
    opacity: 0;
    transform: scale(0.8);
  }
}

@keyframes item-glow {
  50% {
    opacity: 1;
  }
}

@keyframes item-landed {
  from {
    opacity: 1;
    transform: scale(1);
  }
  40% {
    opacity: 1;
    animation-timing-function: linear;
  }
  to {
    opacity: 0;
    transform: scale(1.5);
  }
}

@media (max-width: 360px) {
  /* Still 64 px: centred over the 76 px Action (centre right 154 px), above its ring (138 px). */
  .touch__btn--item {
    width: 64px;
    height: 64px;
    right: calc(122px + env(safe-area-inset-right));
    bottom: calc(148px + env(safe-area-inset-bottom));
  }
}

/* The item held, in the glass of the HUD's item box (hud.css .hud__item): the
   icon at the centre and, for a keyboard, its key on the tray's corner. It
   fills the box, so its gold outline (::before) rings the whole tray. */
.hud-item {
  --item-icon: calc(var(--hud-u) * 40);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: 17%;
  color: var(--cream);
}

.hud-item kbd {
  position: absolute;
  right: calc(var(--hud-u) * -4);
  bottom: calc(var(--hud-u) * -4);
  font-size: calc(var(--hud-u) * 11);
}

html[data-touch] .hud-item kbd {
  display: none;
}

@media (pointer: coarse) {
  .gauge-bar,
  .hud-item kbd {
    display: none !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  .touch__btn--action.is-breathless .gauge-ring,
  .gauge-bar.is-breathless,
  [data-rolling] .item-strip,
  .touch__btn--item[data-rolling]::before,
  .hud-item[data-rolling]::before {
    animation: none;
  }
  .touch__btn--item[data-rolling] {
    box-shadow: 0 0 14px 4px rgba(242, 201, 76, 0.8);
  }
  .touch__btn--item.is-arriving,
  .hud-item.is-arriving .item-window {
    animation: item-pop-fade var(--dur-ui) var(--ease-out) both;
    animation-duration: var(--dur-ui) !important;
  }
  /* !important: base.css cuts every animation to 0.01 ms, and this text is gone when its animation ends. */
  .move-pop--perfect,
  .move-pop--nice {
    animation: move-pop-fade 900ms var(--ease-out) forwards;
    animation-duration: 900ms !important;
  }
  .move-pop--again {
    animation-name: move-pop-fade-again;
  }
}

@keyframes item-pop-fade {
  from {
    opacity: 0;
  }
}

@keyframes move-pop-fade {
  0%,
  100% {
    opacity: 0;
  }
  20%,
  70% {
    opacity: 1;
  }
}

@keyframes move-pop-fade-again {
  0%,
  100% {
    opacity: 0;
  }
  20%,
  70% {
    opacity: 1;
  }
}

/* The bubble says what it is: a small "Item" tag under it (R2-18). */
.touch__btn--item::after {
  content: "Item";
  position: absolute;
  bottom: -9px;
  left: 50%;
  transform: translateX(-50%);
  padding: 0 6px;
  font-size: 11px;
  font-weight: 800;
  line-height: 16px;
  color: var(--cream);
  background: var(--ink);
  border-radius: 999px;
  pointer-events: none;
}

/* ---------- Rage heart and Fairy Fury (src/client/rage-heart.ts) ---------- */

/* A pink heart that fills from the bottom with --rage (0..1): a clip-path,
   so a write repaints without layout. Phones: left of the Action ring (dash
   right, rage left); desktop: left of the ribbon bar. */
.rage-heart {
  position: absolute;
  width: 26px;
  height: 26px;
  pointer-events: none;
  display: none;
  filter: drop-shadow(0 2px 0 rgba(18, 20, 28, 0.45));
}

.rage-heart__svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.rage-heart__svg path {
  fill: var(--hot);
  stroke: var(--cream);
  stroke-width: 1.6;
  stroke-linejoin: round;
}

.rage-heart__track path {
  fill: rgba(18, 20, 28, 0.6);
}

.rage-heart__fill {
  position: absolute;
  inset: 0;
  clip-path: inset(calc((1 - var(--rage, 0)) * 100%) 0 0 0);
}

/* Phone: outside the ring's left edge, vertically centred on Action. */
.rage-heart--touch {
  right: calc(100% + 12px);
  top: 50%;
  margin-top: -13px;
}

html[data-touch] .rage-heart--touch {
  display: block;
}

/* Narrow phones: Action sits further left, so the heart tucks under the
   ring's lower-left (just outside it) instead of crowding the stick's rest spot. */
@media (max-width: 360px) {
  .rage-heart--touch {
    right: auto;
    left: -21px;
    top: 70px;
    margin-top: 0;
  }
}

/* Desktop: just left of the 180 px ribbon bar. */
.rage-heart--desk {
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom) + 93px);
  margin-left: -124px;
}

html[data-screen="hud"]:not([data-touch]) .rage-heart--desk {
  display: block;
}

@media (pointer: coarse) {
  .rage-heart--desk {
    display: none !important;
  }
}

/* Fairy Fury: the heart glows, beats and cycles the rainbow while it burns.
   The glow is drawn once; the beat is a transform, and the rainbow is six
   stepped fills on a 26 px heart, so no filter animates. */
.rage-heart.is-fury {
  animation: fury-heart 600ms ease-in-out infinite;
  filter: drop-shadow(0 0 6px #f6ecd9) drop-shadow(0 0 10px #ff8a3c);
}

.rage-heart.is-fury .rage-heart__fill path {
  animation: fury-rainbow 600ms step-end infinite;
}

@keyframes fury-heart {
  from,
  to {
    transform: scale(1.1);
  }
  50% {
    transform: scale(1.25);
  }
}

@keyframes fury-rainbow {
  0% {
    fill: #ff4b3a;
  }
  17% {
    fill: #ff8a3c;
  }
  33% {
    fill: #f2c94c;
  }
  50% {
    fill: #5cf0d0;
  }
  67% {
    fill: #2fe0ff;
  }
  83% {
    fill: #f6ecd9;
  }
}

@media (prefers-reduced-motion: reduce) {
  .rage-heart.is-fury,
  .rage-heart.is-fury .rage-heart__fill path {
    animation: none;
  }
  .rage-heart.is-fury {
    transform: scale(1.15);
  }
}

/* ---------- Items: the rush and flight screen (items-rush.ts) ---------- */

.items-rush {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  /* Out of the page while nothing plays; the fade-out still runs first. */
  visibility: hidden;
  transition:
    opacity var(--dur-ui) var(--ease-out),
    visibility 0s linear var(--dur-ui);
  overflow: hidden;
  --rush-tint: 47, 224, 255;
}

.items-rush--on {
  visibility: visible;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.items-rush[data-kind="flight"] {
  --rush-tint: 246, 236, 217;
}

/* A glow in the boost colour pressing in from the edges. */
.items-rush__glow {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse at 50% 55%,
    transparent 52%,
    rgba(var(--rush-tint), 0.28) 74%,
    rgba(var(--rush-tint), 0.55) 100%
  );
}

/* Speed lines streaming out of the middle, clear where the runner is. */
.items-rush__lines {
  position: absolute;
  inset: -25%;
  background: repeating-conic-gradient(
    from 0deg at 50% 52%,
    rgba(255, 255, 255, 0) 0deg 4deg,
    rgba(255, 255, 255, 0.85) 4.4deg 5deg,
    rgba(255, 255, 255, 0) 5.4deg 11deg,
    rgba(var(--rush-tint), 0.7) 11.3deg 11.8deg,
    rgba(255, 255, 255, 0) 12.1deg 17deg
  );
  mask-image: radial-gradient(ellipse at 50% 52%, transparent 30%, #000 62%);
  -webkit-mask-image: radial-gradient(ellipse at 50% 52%, transparent 30%, #000 62%);
}

/* The lines stream only while a rush or flight shows; hidden, nothing ticks. */
.items-rush--on .items-rush__lines {
  animation: items-rush-stream 0.24s steps(3) infinite;
}

@keyframes items-rush-stream {
  0% {
    transform: rotate(0deg) scale(1);
  }
  100% {
    transform: rotate(6deg) scale(1.06);
  }
}

@media (prefers-reduced-motion: reduce) {
  .items-rush--on .items-rush__lines {
    animation: none;
    opacity: 0.5;
  }
}

/* ---------- finish and results ---------- */

.sheet--finish {
  gap: 8px;
}

/* Your time, engraved in the finish colour: the biggest thing on the plate. */
.finish-time {
  margin: 0;
  font-family: var(--font-mono);
  font-size: clamp(34px, 10.5vw, 52px);
  line-height: 1;
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--finish);
  text-shadow: 0 4px 0 var(--ink);
}

.finish-time[hidden] {
  display: none;
}

.finish-note {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--cream-dim);
}

.results {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Not auto: a long name would widen the column past the plate on a small phone. */
  grid-template-columns: minmax(0, 1fr);
  gap: 5px;
  min-height: 0;
  overflow: auto;
  align-content: start;
}

.results__row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 38px;
  padding: 2px 12px;
  border: 2px solid var(--seam);
  border-radius: var(--radius-m);
  background: var(--plate-lo);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Your row is the raised one with a trim rim; gold is kept for the winner. */
.results__row--you {
  background: var(--plate-hi);
  border-color: var(--trim);
}

.results__row--dnf {
  opacity: 0.75;
}

.results__place {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-width: 2.6em;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  color: var(--trim);
}

/* The winner's row is the one to see: solid gold, a crown and a bigger portrait. */
.results__row--winner {
  min-height: 52px;
  color: var(--ink);
  background-color: var(--finish);
  background-image: var(--gloss);
  border-color: var(--part-seam);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.7),
    var(--shadow-toy);
}

.results__row--winner .results__place {
  color: var(--ink);
}

.results__row--winner .player__you {
  color: var(--cream);
  background: var(--ink);
}

.results__row--winner .player__portrait {
  width: 40px;
  height: 40px;
}

.results__crown {
  display: inline-flex;
  margin-right: 6px;
  vertical-align: middle;
}

/* On the gold row the bead is the dark part and the crown the gold. */
.results__row--winner .bead {
  --dot: var(--ink);
  color: var(--finish);
}

.results__row--winner .bead .icon {
  filter: none;
}

.results__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 800;
}

.results__row--winner .results__name {
  font-size: 1.15em;
}

.results__time {
  flex: none;
  font-family: var(--font-mono);
  font-size: 13px;
}

.results__row--dnf .results__time {
  font-weight: 700;
  color: var(--hot);
}

.finish-actions {
  display: flex;
  gap: 10px;
}

/* Sized so "Retry" and "Main Menu" each stay on one line at 390 px. */
.finish-actions .btn {
  flex: 1 1 auto;
  padding-inline: 10px;
  font-size: 16px;
}

.finish-actions .btn--ghost {
  min-height: 64px;
}

@media (max-height: 640px) {
  .results__row {
    min-height: 32px;
  }

  .finish-time {
    font-size: 32px;
  }

  .finish-actions .btn {
    min-height: 52px;
    font-size: 15px;
  }

  .panel__title {
    font-size: 20px;
  }
}

/* ---------- error ---------- */

/* The one plate rimmed in the hazard colour. */
.sheet--error {
  border-color: var(--hot);
}

.error-message {
  margin: 0;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.35;
  padding: 12px 14px;
  color: var(--cream);
  background: var(--plate-lo);
  border: 2px solid var(--seam);
  border-left: 8px solid var(--hot);
  border-radius: var(--radius-m);
  word-break: break-word;
}

/* ---------- podium ceremony (phone results and the TV) ---------- */

.podium[hidden] {
  display: none;
}

/* The phone podium reuses the TV steps at phone scale. */
.podium--phone {
  --tv-unit: min(2.6vw, 1.5vh);
  justify-content: center;
  gap: 6px;
  min-height: 0;
}

.podium--phone .podium__step {
  width: min(30%, 120px);
  min-width: 0;
}

/* The phone podium is the results' centrepiece: big portraits over the steps. */
.podium--phone .podium__racer {
  width: clamp(48px, 16vw, 76px);
}

.podium--phone .podium__step--1 .podium__racer {
  width: clamp(58px, 20vw, 92px);
}

.results .player__portrait {
  width: 28px;
  height: 28px;
}

.podium--phone .podium__name {
  font-size: 16px;
}

.podium--phone .podium__time {
  font-family: var(--font-mono);
  font-size: 11px;
  text-align: center;
}

.podium--phone .podium__block {
  font-size: 17px;
  border-width: 2px;
}

.podium--phone .podium__step--1 .podium__block {
  height: clamp(44px, 9vh, 80px);
}

.podium--phone .podium__step--2 .podium__block {
  height: clamp(32px, 6.5vh, 58px);
}

.podium--phone .podium__step--3 .podium__block {
  height: clamp(24px, 4.5vh, 40px);
}

.podium__step--you .podium__name {
  padding: 0 6px;
  border-radius: 999px;
  background: var(--trim);
  color: var(--ink);
}

/* Each racer cheers with their own celebration (look.emote 0..3). */
.podium__racer--emote-0 {
  animation: cheer-twirl 1.6s ease-in-out infinite;
}

.podium__racer--emote-1 {
  animation: cheer-hop 0.9s ease-in-out infinite;
}

.podium__racer--emote-2 {
  animation: cheer-pulse 1.1s ease-in-out infinite;
}

.podium__racer--emote-3 {
  animation: cheer-bounce 0.7s var(--ease-spring) infinite;
}

@keyframes cheer-twirl {
  50% {
    transform: rotateY(180deg) translateY(-4px);
  }
}

@keyframes cheer-hop {
  50% {
    transform: translateY(-22%) rotate(-8deg);
  }
}

@keyframes cheer-pulse {
  50% {
    transform: scale(1.14);
  }
}

@keyframes cheer-bounce {
  50% {
    transform: translateY(-14%) scaleY(1.08);
  }
}

/* Metal filings and cyan sparks over the results, phone and TV alike. */
.confetti {
  position: fixed;
  inset: 0;
  z-index: 30;
  overflow: hidden;
  pointer-events: none;
}

.confetti__piece {
  position: absolute;
  top: -4vh;
  left: var(--x);
  width: 9px;
  height: 14px;
  border-radius: 2px;
  opacity: 0;
  animation: confetti-fall var(--fall) ease-in var(--delay) both;
}

@keyframes confetti-fall {
  from {
    opacity: 1;
    transform: translate(0, 0) rotate(0deg);
  }

  to {
    opacity: 0.9;
    transform: translate(var(--drift), 110vh) rotate(var(--spin));
  }
}

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

/* Short phones: a lower podium so Rematch and the list still fit. */
@media (max-height: 640px) {
  .podium--phone .podium__time {
    display: none;
  }

  .podium--phone .podium__racer,
  .podium--phone .podium__step--1 .podium__racer {
    width: clamp(40px, 13vw, 52px);
  }
}

/* ---------- fun awards (race-awards.ts), one per racer ---------- */

.results__who {
  display: grid;
  flex: 1;
  min-width: 0;
  line-height: 1.1;
}

.award {
  justify-self: start;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 800;
  color: var(--trim-hi);
}

/* On the winner's gold row the award is engraved in ink. */
.results__row--winner .award {
  color: var(--ink);
}

/* The small "Never out of breath" badge under a finisher's award. */
.award--badge {
  font-size: 11px;
  color: var(--boost);
}

.podium .award {
  justify-self: center;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--plate-lo);
  color: var(--cream);
  border: 1px solid var(--seam);
  font-size: calc(var(--tv-unit) * 1.8);
}

.podium--phone .award {
  font-size: 11px;
  padding: 0 5px;
}

/* ---------- the race menu (race-pause.ts) ---------- */

.sheet--pause {
  width: min(340px, 100%);
  gap: 10px;
  text-align: center;
}

.pause-note,
.pause-record {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--cream-dim);
}

.pause-note[hidden],
.pause-record:empty {
  display: none;
}

.pause-record {
  font-variant-numeric: tabular-nums;
}

/* ---------- world records (records-view.ts) ---------- */

.sheet--records .hub-bar {
  justify-content: flex-start;
}

.records {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
  min-height: 0;
  overflow: auto;
}

/* A row with a record is a button: it plays the run (records-view.ts). */
.records__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  grid-template-areas:
    "track time play"
    "who time play";
  align-items: center;
  gap: 0 12px;
  width: 100%;
  min-height: 56px;
  padding: 8px 14px;
  border: 2px solid var(--seam);
  border-radius: var(--radius-m);
  background: var(--plate-lo);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 120ms var(--ease-out);
}

.records__row:disabled {
  cursor: default;
}

.records__row:active:not(:disabled) {
  transform: scale(0.98);
  border-color: var(--trim);
}

.records__play {
  grid-area: play;
  width: 22px;
  height: 22px;
  color: var(--trim);
}

.records__track {
  grid-area: track;
  font-weight: 800;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.records__who {
  grid-area: who;
  font-size: 13px;
  font-weight: 700;
  color: var(--cream-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.records__time {
  grid-area: time;
  font-family: var(--font-mono);
  font-size: 18px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--finish);
}

.records__row--none .records__time {
  color: var(--cream-dim);
}

/* ---------- replay (replay-screen.ts) ----------
   The transport under a replayed run. It is the HUD's dark plate, so it reads
   on any track's room; the picture above it stays clear for the run, and a
   drag across it turns the camera. Nothing here moves on a key or a drag: only
   a pressed button gives, and the bar's thumb swells under the finger. */

.screen--replay {
  grid-template-rows: auto minmax(0, 1fr) auto;
  place-items: stretch;
  color: var(--plate-ink);
}

.replay-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
}

.replay-top > * {
  pointer-events: auto;
}

.replay-tag {
  position: relative;
  z-index: 0;
  margin-left: 8px;
  padding: 7px 18px;
  font-family: var(--font-display);
  font-size: 15px;
  font-style: italic;
  font-weight: 900;
  line-height: 1.2;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.replay-tag::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--plate);
  border-radius: 4px;
  box-shadow: 0 3px 10px rgba(8, 9, 14, 0.28);
  transform: skewX(-12deg);
}

/* The ways out: the HUD's dark plate, so they read on a pale room as on a dark one. */
.replay-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 4px 16px;
  color: var(--plate-ink);
  font-family: var(--font-display);
  font-size: 13px;
  font-style: italic;
  font-weight: 900;
  line-height: 1.2;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  white-space: nowrap;
  background: var(--plate);
  border: 0;
  border-radius: 8px;
  box-shadow: 0 3px 10px rgba(8, 9, 14, 0.28);
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
}

.replay-chip[hidden] {
  display: none;
}

.replay-chip:active {
  transform: scale(0.96);
}

.replay-chip:focus-visible {
  outline: 3px solid var(--boost);
  outline-offset: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .replay-chip:hover {
    background: var(--plate-hi);
  }
}

.replay-deck {
  grid-row: 3;
  justify-self: center;
  display: grid;
  gap: 8px;
  width: min(100%, 520px);
  padding: 12px 14px 14px;
  background: var(--plate);
  border-radius: var(--radius-l);
  box-shadow: 0 6px 20px rgba(8, 9, 14, 0.3);
  pointer-events: auto;
}

/* What was pressed on this frame, and the run's clock. */
.replay-read {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
}

.replay-pad {
  display: flex;
  align-items: center;
  gap: 6px;
}

.replay-pad__stick {
  position: relative;
  flex: none;
  width: 30px;
  height: 30px;
  border: 2px solid rgba(255, 255, 255, 0.28);
  border-radius: 50%;
}

.replay-pad__nub {
  position: absolute;
  inset: 8px;
  background: #fff;
  border-radius: 50%;
}

.replay-pad__key {
  padding: 4px 7px;
  font-family: var(--font-display);
  font-size: 10px;
  font-style: italic;
  font-weight: 900;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--plate-dim);
  border: 1.5px solid rgba(255, 255, 255, 0.24);
  border-radius: 999px;
}

.replay-pad__key[data-on] {
  color: #14101a;
  background: #fff;
  border-color: #fff;
}

/* Dash is the cyan it is on the road and under the thumb. */
#replay-dash[data-on] {
  background: var(--boost);
  border-color: var(--boost);
}

.replay-clock {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.replay-clock__of {
  margin-left: 3px;
  font-size: 10px;
  font-weight: 500;
  color: var(--plate-dim);
}

/* On the narrowest phones the bar alone says how far in the run is. */
@media (max-width: 359px) {
  .replay-clock__of {
    display: none;
  }
}

/* The bar: tall enough for a thumb, the track a thin line through it. */
.replay-bar {
  position: relative;
  height: 36px;
  margin-inline: 8px;
  cursor: pointer;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* The thumb hangs half over either end: a press on that half is a press on the bar. */
.replay-bar::before {
  content: "";
  position: absolute;
  inset: 0 -10px;
}

.replay-bar:focus-visible {
  outline: 3px solid var(--boost);
  outline-offset: 2px;
  border-radius: var(--radius-s);
}

.replay-bar__track {
  position: absolute;
  top: 50%;
  right: 0;
  left: 0;
  height: 6px;
  margin-top: -3px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 3px;
}

.replay-bar__fill {
  display: block;
  height: 100%;
  background: #fff;
  transform: scaleX(0);
  transform-origin: left center;
}

.replay-bar__marks,
.replay-bar__run {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* A track has dozens of flags: each is a faint notch, a ruler along the bar.
   A fall is what a replay is opened to find, so it stands proud in the hazard's red. */
.replay-bar__mark {
  position: absolute;
  top: 50%;
  width: 2px;
  height: 10px;
  margin: -5px 0 0 -1px;
  background: rgba(255, 255, 255, 0.32);
  border-radius: 1px;
}

.replay-bar__mark--fall {
  width: 4px;
  height: 18px;
  margin: -9px 0 0 -2px;
  background: var(--hot);
  border-radius: 2px;
}

.replay-bar__thumb {
  position: absolute;
  top: 50%;
  left: -8px;
  width: 16px;
  height: 16px;
  margin-top: -8px;
  background: #fff;
  border-radius: 50%;
  box-shadow: 0 1px 4px rgba(8, 9, 14, 0.5);
  transition: transform var(--dur-fast) var(--ease-out);
}

.replay-bar[data-held] .replay-bar__thumb {
  transform: scale(1.4);
}

.replay-keys {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.replay-key {
  display: grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 48px;
  padding: 0;
  color: #fff;
  font: inherit;
  background: none;
  border: 0;
  border-radius: 12px;
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
}

.replay-key svg {
  width: 24px;
  height: 24px;
  fill: currentColor;
}

.replay-key--flip svg {
  transform: scaleX(-1);
}

.replay-key:active {
  transform: scale(0.92);
}

.replay-key:focus-visible {
  outline: 3px solid var(--boost);
  outline-offset: 2px;
}

.replay-key--play {
  width: 56px;
  height: 56px;
  color: #14101a;
  background: #fff;
  border-radius: 50%;
}

.replay-key--play svg {
  width: 28px;
  height: 28px;
}

.replay-key--speed {
  width: 48px;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.03em;
}

@media (hover: hover) and (pointer: fine) {
  .replay-key:not(.replay-key--play):hover {
    background: rgba(255, 255, 255, 0.12);
  }
}

.replay-take {
  min-height: 52px;
  font-family: var(--font-display);
}

.replay-hint {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px 14px;
  margin: 2px 0 0;
  font-size: 12px;
  font-weight: 700;
  color: var(--plate-dim);
}

.replay-hint span {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

@media (pointer: coarse) {
  .replay-hint {
    display: none;
  }
}

/* A wide screen, or a phone on its side: the deck is one low bar under the run,
   the bar the full width of it, so the runner above stays in the picture. */
@media (min-width: 1000px), (orientation: landscape) and (max-height: 500px) {
  .replay-deck {
    /* The sides are as wide as each other, so the transport sits in the middle, and never narrower than what is in them. */
    grid-template-columns: minmax(max-content, 1fr) auto minmax(max-content, 1fr);
    align-items: center;
    gap: 2px 12px;
    width: min(100%, 1000px);
    padding: 6px 14px 10px;
  }

  .replay-bar {
    grid-row: 1;
    grid-column: 1 / -1;
  }

  .replay-read {
    grid-row: 2;
    grid-column: 1;
    justify-content: flex-start;
    gap: 14px;
  }

  .replay-keys {
    grid-row: 2;
    grid-column: 2;
    gap: 4px;
  }

  .replay-take {
    grid-row: 2;
    grid-column: 3;
    justify-self: end;
    min-width: 168px;
    min-height: 44px;
  }

  .replay-hint {
    grid-row: 3;
    grid-column: 1 / -1;
    margin-top: 4px;
  }
}

/* On its side a phone is narrow between its notch and its home bar: the bar alone
   says how far in the run is, and everything sits closer. */
@media (orientation: landscape) and (max-height: 500px) {
  .replay-deck {
    column-gap: 8px;
    padding-inline: 12px;
  }

  .replay-clock__of {
    display: none;
  }

  .replay-read {
    gap: 10px;
  }

  .replay-keys {
    gap: 0;
  }

  .replay-take {
    min-width: 0;
    padding-inline: 14px;
    font-size: 16px;
  }
}

/* A takeover's way back, under the clock: clear of both thumbs. */
.takeover-exit {
  position: absolute;
  top: calc(var(--safe-top) + 14px + var(--hud-u) * 40);
  right: 0;
  left: 0;
  width: max-content;
  min-height: 36px;
  margin-inline: auto;
  pointer-events: auto;
}

/* The chip is short so it stays out of the run; its reach is a thumb's all the same. */
.takeover-exit::after {
  content: "";
  position: absolute;
  inset: -6px -8px;
}

@media (pointer: coarse) {
  .takeover-exit kbd {
    display: none;
  }
}

/* Dev Mode: the track editor. A desktop tool laid around the 3D view, which stays clickable. */

.screen--dev {
  grid-template: "bar bar bar" auto "palette . inspector" minmax(0, 1fr) "foot foot foot" auto / 236px minmax(
      0,
      1fr
    ) 300px;
  place-items: stretch;
  gap: 10px;
  padding: 10px;
  font-size: 13px;
  line-height: 1.3;
}

.dev-bar,
.dev-panel,
.dev-foot {
  pointer-events: auto;
  background: var(--plate);
  border: 2px solid var(--ink);
  border-radius: var(--radius-m);
  box-shadow: var(--shadow-panel);
}

.dev-bar {
  grid-area: bar;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  flex-wrap: wrap;
}

.dev-panel {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  overflow-y: auto;
  align-self: start;
  max-height: 100%;
}

.dev-palette {
  grid-area: palette;
  gap: 6px;
}

.dev-inspector {
  grid-area: inspector;
}

.dev-foot {
  grid-area: foot;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 10px;
}

.dev-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: 13px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.dev-hint {
  margin: 0;
  font-size: 12px;
  opacity: 0.75;
}

.dev-btn,
.dev-tool,
.dev-run,
.dev-problem {
  font: inherit;
  font-weight: 700;
  color: var(--cream);
  background: var(--plate-hi);
  border: 2px solid var(--ink);
  border-radius: var(--radius-s);
  padding: 5px 10px;
  cursor: pointer;
  transition:
    transform 120ms ease-out,
    background-color 120ms ease-out;
}

.dev-btn:active,
.dev-tool:active,
.dev-run:active {
  transform: scale(0.97);
}

.dev-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.dev-btn:focus-visible,
.dev-tool:focus-visible,
.dev-run:focus-visible,
.dev-problem:focus-visible,
.dev-input:focus-visible,
.dev-select:focus-visible {
  outline: 3px solid var(--boost);
  outline-offset: 1px;
}

@media (hover: hover) {
  .dev-btn:not(:disabled):hover,
  .dev-run:hover,
  .dev-tool:not([aria-pressed="true"]):hover {
    background: var(--plate-lo);
  }
}

.dev-btn--play {
  color: var(--ink);
  background: var(--boost);
  padding: 6px 16px;
}

.dev-btn--danger {
  color: var(--hot);
  border-color: var(--hot);
}

.dev-tools {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px;
}

.dev-tool {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  padding: 2px 7px;
  font-size: 12px;
  line-height: 1.2;
  text-align: left;
}

/* The armed tool: what a click on the view does now. */
.dev-tool[aria-pressed="true"] {
  color: var(--ink);
  background: var(--trim);
  box-shadow: inset 0 0 0 1px var(--ink);
}

.dev-tool--object {
  border-left: 7px solid var(--tool, var(--ink));
}

.dev-tool kbd {
  font-size: 10px;
}

.dev-swatch {
  flex: none;
  width: 14px;
  height: 14px;
  border: 2px solid var(--ink);
  border-radius: 4px;
}

.dev-input,
.dev-select {
  font: inherit;
  font-weight: 700;
  color: var(--cream);
  background: var(--plate-hi);
  border: 2px solid var(--ink);
  border-radius: var(--radius-s);
  padding: 4px 8px;
}

.dev-input {
  width: 180px;
}

.dev-field {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 700;
}

.dev-field[hidden] {
  display: none;
}

/* The one thing in the bar that gives way: it takes the room that is left and shortens, so the bar never
   grows a second row (and shoves the view down) when its words change. */
.dev-saved {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  opacity: 0.7;
}

.dev-saved--bad {
  color: var(--hot);
  opacity: 1;
  font-weight: 800;
}

.dev-ready {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 800;
  cursor: pointer;
}

.dev-ready:has(input:disabled) {
  opacity: 0.45;
  cursor: default;
}

.dev-num {
  display: grid;
  grid-template: "label label" auto "range box" auto / minmax(0, 1fr) 68px;
  gap: 2px 8px;
  align-items: center;
}

.dev-num span {
  grid-area: label;
  font-weight: 700;
}

.dev-num input[type="number"] {
  font: inherit;
  font-weight: 700;
  width: 100%;
  color: var(--cream);
  background: var(--plate-lo);
  border: 2px solid var(--ink);
  border-radius: var(--radius-s);
  padding: 3px 6px;
}

.dev-num input[type="range"] {
  width: 100%;
  accent-color: var(--trim);
}

.dev-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-weight: 700;
}

.dev-row--check {
  justify-content: flex-start;
}

.dev-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.dev-flags {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 8px;
}

.dev-keys {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 3px 10px;
  margin: 0;
  font-size: 12px;
}

.dev-keys dt {
  font-weight: 800;
  white-space: nowrap;
}

.dev-keys dd {
  margin: 0;
  opacity: 0.75;
}

.dev-foot__label {
  font-weight: 800;
}

/* One thing to run before the track may be ready: hollow until it has been run. */
.dev-run {
  padding: 3px 9px;
  font-size: 12px;
  border-color: var(--run, var(--ink));
  border-style: dashed;
}

.dev-run--done {
  border-style: solid;
  background: color-mix(in srgb, var(--run, var(--boost)) 30%, var(--plate-hi));
}

.dev-problems {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

.dev-ok,
.dev-bad {
  margin: 0;
  font-weight: 800;
}

.dev-ok {
  color: var(--boost);
}

.dev-bad {
  color: var(--hot);
}

.dev-ok span,
.dev-bad span {
  margin-left: 8px;
  color: var(--cream);
  font-weight: 600;
  opacity: 0.7;
}

.dev-problem {
  color: var(--hot);
  border-color: var(--hot);
  text-align: left;
}

.dev-small {
  display: none;
}

.dev-return {
  position: absolute;
  top: calc(var(--safe-top) + 64px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  font: inherit;
  font-weight: 800;
  color: var(--ink);
  background: var(--trim);
  border: 2px solid var(--ink);
  border-radius: var(--radius-s);
  padding: 6px 14px;
  cursor: pointer;
}

.dev-return[hidden] {
  display: none;
}

/* A phone or a narrow window: say so instead of a cramped editor. */
@media (pointer: coarse), (max-width: 899px) {
  .screen--dev {
    display: grid;
    grid-template: none;
    place-items: center;
    background: var(--ink-shade);
    pointer-events: auto;
  }

  .dev-bar,
  .dev-panel,
  .dev-foot {
    display: none;
  }

  .dev-small {
    display: grid;
    gap: 12px;
    justify-items: center;
    max-width: 320px;
    padding: 20px;
    text-align: center;
    background: var(--plate);
    border: 2px solid var(--trim);
    border-radius: var(--radius-l);
  }

  .dev-small p {
    margin: 0;
  }
}
