/*
 * style.css — everything the player reads or taps.
 *
 * The palette is the arena's own: deep navy ground, cyan rim light, and the
 * six rider colours arriving through a --c custom property so a chip, a
 * banner or a button can take a player's colour without a class per colour.
 * No web fonts (hub rule): a heavy system sans with wide tracking carries the
 * arcade look instead.
 */

:root {
  --ink: #eaf2ff;
  --ink-dim: #9fb0d8;
  --ground: #0a1030;
  --glass: rgba(12, 18, 52, 0.62);
  --line: rgba(130, 170, 255, 0.28);
  --rim: #46e6ff;
  --accent: #6c8cff;
  --c: var(--rim);
  --font:
    system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--ground);
  color: var(--ink);
  font: 15px/1.4 var(--font);
  -webkit-text-size-adjust: 100%;
  /*
   * This is a game, not a document. On a tablet a thumb resting on a steer
   * button is a long press and a drag that starts on a paddock label is a
   * text selection, so iPad Safari highlights words and offers Copy / Look Up
   * mid-round. Nothing selects and nothing opens a callout, except a real
   * text field (below). `manipulation` drops double-tap-to-zoom without
   * taking pinch away; the arena itself is touch-action: none. js/touch-guard.js
   * is the second half: CSS alone is not honoured everywhere (hub CLAUDE.md
   * §2, "a game is not a document").
   */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
input,
textarea,
[contenteditable="true"] {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

#arena {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
  outline: none;
}

/* The overlay layer never eats input itself; each control opts back in. */
#ui {
  position: fixed;
  inset: 0;
  pointer-events: none;
  padding: env(safe-area-inset-top, 0) env(safe-area-inset-right, 0)
    env(safe-area-inset-bottom, 0) env(safe-area-inset-left, 0);
}
#ui > * {
  pointer-events: auto;
}
[hidden] {
  display: none !important;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
button:focus-visible {
  outline: 2px solid var(--rim);
  outline-offset: 3px;
}

/* ---------- The paddock (title / menu) ---------- */

.panel {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(440px, calc(100% - 32px));
  max-height: calc(100% - 32px);
  overflow: auto;
  padding: 26px 26px 22px;
  border-radius: 22px;
  background: var(--glass);
  border: 1px solid var(--line);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.3),
    0 20px 60px rgba(0, 0, 0, 0.5),
    0 0 60px rgba(70, 230, 255, 0.12);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  text-align: center;
}

.kicker {
  margin: 0 0 4px;
  color: var(--rim);
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

/*
 * The title is codex's logo, which already carries its own glow, so the
 * heading adds none: the old text-shadow stack was doing the job this
 * artwork does better. The text stays in the markup for screen readers and
 * for the case where the image does not load, and is pushed out of sight
 * rather than hidden, because display:none takes it from the accessibility
 * tree too.
 */
/*
 * Scoped to #title, not to every h1 on the page. It used to be a bare `h1`,
 * which reaches every panel — and js/net/lobby.js builds its heading as an
 * h1 with text in it, so "Together" and "Find a game" were being rendered at
 * font-size 0. A heading with no artwork behind it takes the rule below
 * instead.
 */
#title {
  margin: 2px 0 10px;
  font-size: 0;
  line-height: 0;
}
#title img {
  display: block;
  width: 100%;
  height: auto;
  /* The art is trimmed to its own glow, which reaches well past the fox, so
   * a little negative margin stops the panel looking over-padded. */
  margin: -4% -2% -6%;
}

/*
 * Every other panel's heading — the sound menu's, and the two the lobby
 * writes. The paddock spends its heading on codex's logo; these are words,
 * so they carry the rim light the logo carries in its own artwork.
 */
.panel h1,
.panel h2 {
  margin: 0 0 14px;
  font-size: 26px;
  font-weight: 900;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-shadow:
    0 0 14px rgba(70, 230, 255, 0.5),
    0 0 40px rgba(70, 230, 255, 0.25);
}

.row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 10px 0;
  text-align: left;
}
/*
 * One gutter for every row, so every control in the menu starts at the same x
 * (hub CLAUDE.md §2). 84px since the game learned bokmål (2026-10-10): the
 * longest single words are now EFFEKTER, SPILLERE and LANGUAGE, about 74px at
 * this size and tracking, and a word cannot wrap — it would run out of its
 * gutter and push its control out of line with the rest. Two-word captions
 * ("Spill til", "Win at") wrap to two short lines inside it as before.
 */
.label {
  flex: 0 0 84px;
  color: var(--ink-dim);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/*
 * Segmented rows: a set of pills where exactly one is pressed.
 *
 * `flex: 1 1 0` and pointedly not `1 1 auto` (hub §2, "a group of equals looks
 * equal"). Auto sizes each pill to its own text, which turned the Riders row
 * into a small "Solo" beside a large "Two, one keyboard" — two options of
 * equal standing drawn at two different sizes, which tells the player the
 * small one matters less. From a zero basis they simply split the row.
 */
.seg {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  flex: 1;
}
.seg button {
  flex: 1 1 0;
  min-width: 0;
  min-height: 40px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(20, 30, 70, 0.6);
  color: var(--ink);
  transition:
    background 0.15s,
    box-shadow 0.15s,
    transform 0.08s;
}
.seg button:hover {
  background: rgba(40, 60, 120, 0.7);
}
.seg button[aria-pressed="true"] {
  background: linear-gradient(135deg, #3aa0ff, #7838e8);
  border-color: transparent;
  color: #08102a;
  font-weight: 700;
  box-shadow: 0 0 18px rgba(90, 150, 255, 0.55);
}
.seg button:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

/*
 * The language row: the same segmented pills, each carrying its flag beside
 * its own name (hub §2, "Two languages"). The flags are the hub's two SVGs and
 * keep their own colours; a thin rim keeps the white of each off the glass.
 * Padding comes in a little so "English" and its flag fit a half-column on a
 * sideways phone without the pill growing a second line.
 */
.seg.lang button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 6px 8px;
  font-size: 14px;
}
.seg.lang .flag {
  flex: 0 0 auto;
  width: 22px;
  height: 15px;
  border-radius: 3px;
  box-shadow: 0 0 0 1px rgba(234, 242, 255, 0.35);
}
.seg.lang button span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * The arena slider. A bare range input is an OS control and belongs to no
 * game (hub rule), so the track becomes a thin rim-light and the thumb a
 * glowing orb in the same gradient the pressed pills use. Both vendor
 * pseudo-elements are styled; there is no shared selector for them.
 */
.slider {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 40px;
}
.slider input[type="range"] {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 28px;
  margin: 0;
  background: transparent;
  cursor: pointer;
  touch-action: pan-y;
}
.slider input[type="range"]:focus-visible {
  outline: 2px solid var(--rim);
  outline-offset: 4px;
  border-radius: 999px;
}
.slider input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(
    90deg,
    rgba(70, 230, 255, 0.35),
    rgba(176, 124, 255, 0.55)
  );
  box-shadow: 0 0 8px rgba(70, 230, 255, 0.35);
}
.slider input[type="range"]::-moz-range-track {
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(
    90deg,
    rgba(70, 230, 255, 0.35),
    rgba(176, 124, 255, 0.55)
  );
  box-shadow: 0 0 8px rgba(70, 230, 255, 0.35);
}
.slider input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 24px;
  height: 24px;
  margin-top: -10px;
  border-radius: 50%;
  border: 2px solid #06102a;
  background: linear-gradient(135deg, #46e6ff, #3aa0ff 55%, #ff5fb4);
  box-shadow: 0 0 14px rgba(70, 230, 255, 0.7);
}
.slider input[type="range"]::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid #06102a;
  background: linear-gradient(135deg, #46e6ff, #3aa0ff 55%, #ff5fb4);
  box-shadow: 0 0 14px rgba(70, 230, 255, 0.7);
}
.slider output {
  flex: 0 0 64px;
  text-align: right;
  font-weight: 700;
  color: var(--rim);
  font-variant-numeric: tabular-nums;
}

.primary {
  display: block;
  width: 100%;
  margin: 20px 0 12px;
  min-height: 54px;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(135deg, #46e6ff, #3aa0ff 55%, #ff5fb4);
  color: #06102a;
  font-size: 20px;
  font-weight: 900;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  box-shadow:
    0 0 24px rgba(70, 230, 255, 0.5),
    0 8px 24px rgba(0, 0, 0, 0.4);
  transition:
    transform 0.08s,
    box-shadow 0.15s;
}
.primary:hover {
  box-shadow:
    0 0 36px rgba(70, 230, 255, 0.75),
    0 8px 24px rgba(0, 0, 0, 0.4);
}
.primary:active {
  transform: scale(0.98);
}

/*
 * The paddock's two doors. Equal width, equal height, equal type, because a
 * match against the rivals and a match against a friend on another screen are
 * the same size of decision — the old pairing put a full-brightness Blaze!
 * over a thin ghost pill and said otherwise.
 *
 * They wrap their labels rather than shrink their type: "Play together" is
 * twice the word count of "Blaze!" and takes two lines on a narrow phone,
 * which the shared min-height absorbs so both pills stay exactly as tall as
 * each other whatever the viewport does to the text.
 */
.play {
  display: flex;
  gap: 10px;
  margin: 20px 0 12px;
}
.play .primary {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  margin: 0;
  padding: 8px 12px;
  font-size: 17px;
  line-height: 1.15;
  letter-spacing: 0.08em;
}
/*
 * Same pill, different door. Blaze! keeps the cool half of the palette
 * (Bolt's blue running into Mochi through the rim cyan); together takes the
 * warm half — Plum, Mochi, Tango — so the pair reads as two choices rather
 * than one button drawn twice.
 */
.primary.together {
  background: linear-gradient(135deg, #7838e8, #ff5fb4 55%, #ffa03c);
  box-shadow:
    0 0 24px rgba(255, 95, 180, 0.5),
    0 8px 24px rgba(0, 0, 0, 0.4);
}
.primary.together:hover {
  box-shadow:
    0 0 36px rgba(255, 95, 180, 0.75),
    0 8px 24px rgba(0, 0, 0, 0.4);
}

.ghost {
  min-height: 38px;
  padding: 6px 16px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink-dim);
}
.ghost:hover {
  color: var(--ink);
  background: rgba(40, 60, 120, 0.4);
}
/*
 * The update pill. Shown only while a new service worker sits waiting, and
 * driven entirely by js/update.js; the markup is in #menu so it cannot be
 * reached during a match.
 *
 * It is deliberately NOT the .primary gradient. Blaze! is what the player came
 * to press, and a second full-brightness pill directly above it would make the
 * paddock ask two questions at once. So it takes Mochi's magenta — a rider
 * colour, from the same palette as everything else here — on the panel's own
 * dark glass, and breathes once every few seconds instead of shouting.
 */
#update {
  display: block;
  width: 100%;
  margin: 0 0 16px;
  min-height: 52px;
  padding: 8px 18px;
  border-radius: 18px;
  border: 1px solid rgba(255, 95, 180, 0.5);
  background: linear-gradient(
    180deg,
    rgba(88, 20, 62, 0.5),
    rgba(22, 14, 48, 0.6)
  );
  color: var(--ink);
  text-align: center;
  animation: breathe 3s ease-in-out infinite;
}
#update:hover {
  border-color: #ff5fb4;
  background: linear-gradient(
    180deg,
    rgba(118, 26, 82, 0.6),
    rgba(30, 18, 60, 0.7)
  );
}
#update:disabled {
  /* The tap has been accepted and the page is about to reload itself. */
  opacity: 0.65;
  cursor: progress;
  animation: none;
}
.update-title {
  display: block;
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #ff9fd0;
}
.update-sub {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--ink-dim);
}
@keyframes breathe {
  0%,
  100% {
    box-shadow: 0 0 0 rgba(255, 95, 180, 0);
  }
  50% {
    box-shadow: 0 0 24px rgba(255, 95, 180, 0.45);
  }
}
/* Noticed, not nagging: a steady glow for anyone who has asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  #update {
    animation: none;
    box-shadow: 0 0 18px rgba(255, 95, 180, 0.35);
  }
}

/*
 * THE CORNER CLUSTER: menu, mute and fullscreen, as icons, top right.
 *
 * A pill of round icon buttons on the arena's own dark glass — fishtank's
 * shape, because the problem is the same one. None of the three belongs to a
 * round and none is a setting of the game, so none wants a row in the menu;
 * and in a corner, at this size, a symbol survives where a word would have to
 * be shrunk until nobody reads it anyway.
 *
 * 42px targets. The hub asks for touch targets that stay comfortable and a
 * tablet is the common case, so these are deliberately larger than the glyph
 * inside them needs.
 *
 * FIXED TO THE PAGE, AND IT NEVER GOES ANYWHERE. It used to be shown with the
 * paddock and taken away for a match, because the Pause pill wanted this corner
 * and two things cannot have one — which meant a player who had started riding
 * could not fill the screen or turn the music down without ending the round
 * first. That pill does not exist any more: pausing is the first plate here, so
 * the thing that was fighting for the corner is now in it. Reading in from the
 * corner it is fullscreen, mute, menu.
 *
 * z-index 30 puts it over every panel (none of which has one), over the pause
 * scrim, over the scoreboard and over the crash bar at 10. Everything it sits
 * above is something it outlives, so nothing any of them does may cover it.
 * `fixed` rather than `absolute` for the same reason: it answers to the
 * viewport, not to #ui, and it carries its own safe-area insets because a fixed
 * box does not inherit the padding #ui puts up for a notch.
 */
#corner-tools {
  position: fixed;
  top: calc(12px + env(safe-area-inset-top, 0px));
  right: calc(12px + env(safe-area-inset-right, 0px));
  z-index: 30;
  display: flex;
  gap: 4px;
  padding: 4px;
  border-radius: 999px;
  background: rgba(8, 12, 36, 0.62);
  border: 1px solid rgba(130, 170, 255, 0.24);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.icon-button {
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-dim);
  transition:
    background 0.15s,
    color 0.15s;
}
.icon-button:hover {
  color: var(--ink);
  background: rgba(70, 230, 255, 0.16);
}
.icon-button:active {
  transform: scale(0.94);
}
/* Stroked, not filled, so one `currentColor` drives the whole glyph and the
 * hover state is a single property. */
.icon-button svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The fullscreen glyph has two states in one file: corners pushing out, and
 * the same four pulled in. aria-pressed picks, so the picture and the label
 * js/screen.js writes can never disagree. */
.icon-button .icon-on {
  display: none;
}
.icon-button[aria-pressed="true"] .icon-off {
  display: none;
}
.icon-button[aria-pressed="true"] .icon-on {
  display: inline;
}
/* Pressed means ACTIVE here, which is true of fullscreen and pointedly not of
 * mute — a silenced arena is a thing that has been switched off, so it is
 * scoped to the one plate it describes rather than left on .icon-button
 * generally. */
#screen-toggle[aria-pressed="true"] {
  color: var(--rim);
}

/* The mute plate is the same trick with the same one write behind it: one svg
 * carrying the speaker, its waves and a slash, with aria-pressed choosing which
 * of the last two shows. js/ui.js sets that attribute and the accessible label
 * together, so what is drawn and what is announced cannot drift — which an
 * emoji swapped into textContent could manage neither way round, since no
 * screen reader announces 🔇 as "muted".
 *
 * Tango, the rider palette's amber, for the muted state. Not the rim light the
 * fullscreen plate takes when it is on: this one is lit because something is
 * OFF, and a player scanning the corner should be able to tell those apart
 * without reading either. */
.icon-button .icon-slash {
  display: none;
}
#mute-toggle[aria-pressed="true"] .icon-waves {
  display: none;
}
#mute-toggle[aria-pressed="true"] .icon-slash {
  display: inline;
}
#mute-toggle[aria-pressed="true"] {
  color: #ffa03c;
}

/*
 * THE IN-MATCH PAUSE PILL IS GONE, and with it the one thing that ever wanted
 * this corner at the same time as the cluster. It was a word pill parked
 * inboard of the icons, existing only while a round was on, opening the panel
 * the menu plate opens now. A menu is what it always raised, so it is the menu
 * plate: one control, in the corner, on every screen, instead of two that
 * looked unrelated and did the same job in different places.
 */

/*
 * The scrim behind a genuinely stopped match. Deep enough that the panel is
 * easy to read over a bright arena, light enough that the player can still
 * see the round they are about to go back to — a pause that hides the game
 * makes the way back feel like starting over. Shown by js/ui.js only for a
 * local match; a net game is still being ridden and is left bright.
 */
#pause-scrim {
  position: fixed;
  inset: 0;
  background: rgba(4, 6, 20, 0.5);
}

/* The panel's own note: the sentence that says what this moment is — that the
 * arena is holding still, that a net round is not, or what the two levels under
 * it actually cover. .hint's bottom margin is written for the paddock, where it
 * sits above the sliders; here it sits under the heading and wants less room. */
#pause-note {
  margin: 0 0 12px;
}

/* The way out of the panel when it is a mixer rather than a pause, and the last
 * thing in the column when it shows at all. A ghost pill of its own rather than
 * a .menu-tools cluster of one: the pair above it is the match's, this is the
 * panel's, and standing it apart is what says so. The min-width stops a
 * two-word label drawing a pill narrower than the ones it is standing under. */
#menu-back {
  margin-top: 14px;
  min-width: 150px;
}

/*
 * THE WAY BACK TO THE ARCADE — one button in every cabinet (hub §2,
 * 2026-10-11). #quit on the paddock and #pause-quit at the foot of the
 * in-match menu are the same object: last in the column and its full width,
 * a little more room above it than between the rows it follows, the ghost
 * pill's outline in the panel's own ink rather than a rider gradient —
 * leaving is never the recommended action — and the shared back arrow,
 * stroked like the corner plates, before sentence-case words.
 */
.exit {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 42px;
  font-size: 14px;
}
.exit svg {
  width: 18px;
  height: 18px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* On the paddock it sits in what is left of the tools row, whose equal-pill
 * rule caps a lone survivor at 190px; the way out is not a pill in a cluster
 * but the column's last row, so it takes the whole width. */
.menu-tools > .exit {
  max-width: none;
  font-size: 14px;
  padding: 6px 16px;
}
#menu .menu-tools {
  margin-top: 20px;
}
#pause-quit {
  margin-top: 20px;
}

/* Which build is serving this session, answered by the service worker. */
#build {
  margin: 14px 0 0;
  color: var(--ink-dim);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.55;
}

/* A struck-through .ghost[aria-pressed="false"] used to live here: the old
 * single SOUND ON / SOUND OFF word pill, which the two volume sliders replaced
 * long before the mute plate arrived. Nothing has carried that attribute on a
 * .ghost since. Silence is a plate in the corner now (#mute-toggle above), and
 * it says so with a slash rather than by crossing out its own label. */

/*
 * What is left of the tools row: the way out of the arcade, alone now that
 * sound and fullscreen have become icons in #corner-tools. It stays a .ghost
 * pill rather than joining a .row's label + slider/seg shape because it is
 * not gameplay, and the rules below still size a cluster even when the
 * cluster is one — the max-width is what stops a lone survivor stretching
 * across the panel.
 */
.menu-tools {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
  margin: 14px 0 0;
}
/*
 * One size for the whole cluster, same rule as the .seg pills and the two
 * doors: a toolbar of ragged widths reads as three unrelated things rather
 * than one set (fishtank's three header buttons are identical squares for
 * exactly this reason). The max-width is what stops a lone survivor — the
 * paddock hides both Fullscreen and Back to the den when the browser or the
 * host cannot do them, leaving Sound on its own — from stretching the width
 * of the panel. Type drops a step because three equal thirds is a narrower
 * pill than these labels were written for.
 */
.menu-tools > * {
  flex: 1 1 0;
  min-width: 0;
  max-width: 190px;
  font-size: 13px;
  padding: 6px 10px;
}
.hint {
  margin: 4px 0 14px;
  color: var(--ink-dim);
  font-size: 13px;
}
kbd {
  display: inline-block;
  min-width: 22px;
  padding: 1px 6px;
  margin: 0 1px;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: rgba(20, 30, 70, 0.7);
  color: var(--ink);
  font: 12px/1.5 var(--font);
}

/* ---------- Scoreboard ---------- */

#hud {
  position: absolute;
  top: 12px;
  left: 12px;
  max-width: 46vw;
}
#scores {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 6px;
  border-radius: 999px;
  background: rgba(8, 12, 36, 0.55);
  border: 1px solid rgba(130, 170, 255, 0.16);
  font-size: 13px;
  transition: opacity 0.3s;
}
.chip .dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--c);
  box-shadow: 0 0 8px var(--c);
  flex: none;
}
.chip .name {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chip .pts {
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--c);
}
.chip.out {
  opacity: 0.4;
}
.chip.out .dot {
  box-shadow: none;
}
#target-label {
  margin: 6px 0 0 8px;
  color: var(--ink-dim);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* ---------- Banners ---------- */

/* The round's big words ("Steer to aim", "Round 2") step aside while the menu
 * is open. They used to paint straight through it — a pause taken during the
 * aiming countdown put the banner across the panel's heading. They are still
 * there, unchanged, the moment the menu closes. */
#pause:not([hidden]) ~ #banner {
  visibility: hidden;
}
#banner {
  position: absolute;
  left: 50%;
  top: 22%;
  transform: translateX(-50%);
  width: min(560px, calc(100% - 32px));
  text-align: center;
  pointer-events: none;
}
/*
 * The two things you can do with a finished match, at one size — the same
 * rule as the paddock's two doors. Equal size is not equal weight: Rematch
 * keeps the gradient and the way out stays a ghost pill, so the hierarchy is
 * carried by colour where it belongs rather than by one button being bigger.
 * A guest's banner has no Rematch at all (js/ui.js hides it), and max-width
 * is what stops the survivor stretching across the whole banner.
 */
#banner-actions {
  pointer-events: auto;
  display: flex;
  gap: 10px;
  justify-content: center;
  align-items: stretch;
  flex-wrap: wrap;
  margin-top: 8px;
}
#banner-actions > * {
  flex: 1 1 0;
  min-width: 0;
  max-width: 220px;
}
#banner-actions .primary {
  width: auto;
  margin: 0;
  padding: 0 16px;
  font-size: 17px;
}
#banner-sub {
  margin: 0;
  color: var(--ink-dim);
  font-size: 13px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
#banner-text {
  margin: 2px 0 0;
  font-size: clamp(28px, 7vw, 48px);
  font-weight: 900;
  letter-spacing: 0.04em;
  color: #fff;
  text-shadow:
    0 0 14px var(--c),
    0 0 40px var(--c);
  animation: pop 0.35s cubic-bezier(0.2, 1.4, 0.4, 1);
}
@keyframes pop {
  from {
    transform: scale(0.7);
    opacity: 0;
  }
}

/* ---------- Spectator caption ---------- */

#spectate {
  position: absolute;
  left: 50%;
  bottom: 28px;
  transform: translateX(-50%);
  margin: 0;
  padding: 8px 16px;
  border-radius: 999px;
  background: rgba(8, 12, 36, 0.6);
  border: 1px solid rgba(130, 170, 255, 0.2);
  color: var(--ink);
  font-size: 14px;
  white-space: nowrap;
  pointer-events: none;
  text-shadow: 0 0 10px var(--c);
}
#spectate-text {
  font-weight: 800;
  color: var(--c);
}
#spectate .how {
  margin-left: 10px;
  color: var(--ink-dim);
  font-size: 12px;
}
/*
 * Skip ahead rides in the caption, the one thing on screen while you watch the
 * bots. The caption lets pointers through to the arena; the button takes them
 * back. z-index because #touch comes later in the page and its circles carry
 * an invisible hit margin wide enough to reach under the caption on a phone.
 */
#spectate {
  z-index: 1;
}
#skip {
  pointer-events: auto;
  margin-left: 12px;
  min-height: 32px;
  padding: 4px 14px;
  color: var(--ink);
  font-size: 13px;
}
#skip kbd {
  margin-left: 6px;
}
@media (max-width: 600px) {
  #spectate {
    bottom: 124px; /* above the touch buttons */
    font-size: 13px;
  }
}

/* ---------- Touch steering ---------- */

#touch {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  justify-content: space-between;
  padding: 0 18px 18px;
  pointer-events: none;
}
#touch button {
  pointer-events: auto;
  width: 104px;
  height: 104px;
  border-radius: 50%;
  border: 2px solid rgba(190, 220, 255, 0.5);
  background: rgba(20, 30, 80, 0.45);
  color: rgba(234, 242, 255, 0.85);
  font-size: 34px;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition:
    background 0.1s,
    box-shadow 0.1s;
}
#touch button.held {
  background: rgba(70, 230, 255, 0.35);
  border-color: var(--rim);
  box-shadow: 0 0 24px rgba(70, 230, 255, 0.6);
}

/* ---------- Crash bar ---------- */

/*
 * The opening sting (js/intro.js), a canvas the script creates and removes.
 * Over everything, the corner plates at 30 included, because it is a curtain:
 * for its three seconds nothing under it is meant to be reachable, and a tap
 * anywhere on it is the skip.
 */
#intro {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 40;
  touch-action: none;
  cursor: pointer;
}

#crash {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 10px 14px;
  background: #4b1030;
  color: #ffd6e6;
  font-size: 13px;
  z-index: 10;
}

@media (max-width: 600px) {
  .panel {
    padding: 20px 18px 16px;
  }
  .label {
    flex-basis: 76px;
  }
  .chip {
    font-size: 12px;
    padding: 3px 8px 3px 5px;
  }
  #touch button {
    width: 88px;
    height: 88px;
  }
}

/* ============ Landscape phones: the paddock turns sideways ============
 *
 * A phone on its side is 300-430px tall and a browser toolbar can take fifty
 * of those. Every panel here is one column, and the column that fits a
 * portrait phone with room to spare runs well past the fold sideways — so the
 * paddock scrolled, and what scrolls off the bottom of a column is whatever
 * was last in it, which here is the way out of the game.
 *
 * What a sideways phone has instead is width, so the paddock becomes a grid
 * that spends it: the logo and the two doors down the left, everything that
 * is a setting down the right, and the things that read as one line — the
 * hint, the tools, the build — full width underneath. The grid is laid over
 * the existing markup BY AREA, so nothing moves in index.html and portrait is
 * untouched. The sound and pause panels need no areas at all: they are short
 * enough that standing their two slider rows side by side is the whole fix.
 *
 * Five settings on the right since Turning arrived (2026-09-19), and the
 * paddock is exactly as tall as it was with four: the doors moved up a row to
 * sit beside Win at, the update pill sits beside Turning, and the kicker —
 * the one line the left column could spare — goes, for the reason the
 * lobby's h1 went. Adding a row would have been 44px this layout does not
 * have; the hub's rule is that this menu must not scroll.
 *
 * This is maxgear's shape, copied deliberately rather than reinvented (hub
 * CLAUDE.md §2), including its 500px cut: a tablet is taller than that
 * whichever way up it is held, and keeps the stacked layout.
 *
 * .panel keeps its overflow: auto behind all of this. That is a safety net
 * for a viewport shorter than anything measured here, not the plan.
 */
@media (orientation: landscape) and (max-height: 500px) {
  /*
   * Narrower than the screen on purpose. 760 was "as wide as a landscape
   * phone will allow", which is not the same as "as wide as this wants to
   * be": a panel running to both edges reads as a page rather than as a
   * thing sitting over the arena, and it drags every row of controls out to
   * a length no one needs. 640 is enough for the two-column layouts below —
   * two 292px columns after padding — and leaves the game visible down both
   * sides, which is most of what makes this a game and not a form.
   */
  .panel {
    width: min(640px, calc(100% - 24px));
    max-height: calc(100% - 12px);
    padding: 14px 18px 12px;
    border-radius: 18px;
  }

  /* Controls lose a few pixels of height, and no more than a few: this fires
   * on a phone, where a finger is still the pointer (hub §2, touch targets
   * stay comfortable). 38 is the floor, not an invitation to go lower. */
  .seg button,
  .slider,
  .ghost {
    min-height: 38px;
  }
  .primary {
    min-height: 46px;
  }
  .row {
    margin: 0;
  }

  /*
   * THE CORNER STAYS IN THE CORNER SIDEWAYS TOO — 38px plates and a little
   * closer to the edge, and that is the whole of what changes about it. maxgear
   * used to fold its cluster into the sideways grid as a cell of its own, which
   * fitted the layout and broke the rule; a set that moves when the phone turns
   * is not a corner. So it holds its place, and whatever it lands on gives way
   * instead — see --corner-bite below.
   */
  #corner-tools {
    top: calc(8px + env(safe-area-inset-top, 0px));
    right: calc(8px + env(safe-area-inset-right, 0px));
  }
  .icon-button {
    width: 38px;
    height: 38px;
  }

  /*
   * HOW FAR THE CLUSTER REACHES INTO A PANEL, derived once and used by everyone
   * it lands on — here, and by js/net/lobby.js, which draws its own <style> and
   * has the same corner over the same panel geometry.
   *
   * The cluster is 130px wide sideways (three 38s, 4 between each pair, 4 of
   * padding a side) and sits 8 from the edge, so its left edge is 138 in from
   * the right. Every panel is `min(640px, 100% - 24px)` centred in #ui, so its
   * content ends 18 (its own padding) inside its right edge. Call #ui's width U
   * and the panel's W: the overlap is (W - U) / 2 + 120, which for any U from
   * 664 up is 440px - U/2 — 108px at the bottom of that range, 0 at 880 and
   * negative past it, hence the clamp. Below 664 the panel is U - 24 wide and
   * the answer is a flat 108. One expression covers both.
   *
   * U AND NOT 100vw. #ui is inset by the safe areas and the cluster sits inside
   * them too, so on a notched phone held sideways the viewport is 90-odd pixels
   * wider than the box any of this is measured in — and the overlap gets BIGGER
   * as that box gets narrower, not smaller. A bare 50vw would under-taper by
   * about 47px on exactly the phones most people own.
   *
   * It was a flat 66px behind a `max-width: 800px` when the cluster was two
   * plates. Three made both numbers wrong, and a formula cannot go stale the
   * way a pair of constants did.
   *
   * It is only ever the TOP of a panel that is at risk: the cluster is 46px
   * tall here and nothing else about it changed.
   */
  :root {
    /* U itself: the box every panel here is centred in. */
    --ui-width: calc(
      100vw - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px)
    );
    --corner-bite: clamp(0px, calc(440px - var(--ui-width) / 2), 108px);
  }

  #menu {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "logo   riders"
      "logo   rivals"
      "logo   arena"
      "play   turning"
      "tools  target"
      "hint   hint";
    align-content: center;
    align-content: safe center;
    gap: 6px 20px;
  }
  #title {
    grid-area: logo;
    margin: 0;
    position: relative;
    align-self: stretch;
    min-height: 0;
  }
  /* The art is trimmed to its own glow and portrait pulls it in with negative
   * margins; here it is height that is scarce, so the art takes NO height of
   * its own: it is lifted out of flow and fitted (object-fit: contain) into
   * whatever the three rows beside it add up to. An in-flow image capped at
   * max-height: 100% of a grid area sized by its own content is circular —
   * when the start screen briefly carried a language row (2026-10-11) and
   * the logo had two rows beside it instead of three, it inflated both and
   * the paddock scrolled by 12px. Fitted, it can never size its neighbours.
   * --logo-bite is the corner's share of the cell on the narrowest phones
   * (see the max-width: 820px block below). */
  #title img {
    position: absolute;
    top: 0;
    left: 0;
    width: calc(100% - var(--logo-bite, 0px));
    height: 100%;
    max-width: none;
    margin: 0;
    object-fit: contain;
  }
  /* The logo beside it already says the name, and its row is the one the
   * column gave up when Turning arrived. Portrait keeps it. */
  #menu .kicker {
    display: none;
  }
  /*
   * THE WAY OUT JOINED THE DOORS' COLUMN on 2026-10-11, and the update pill
   * gave up its own row to make room. The hub's one Back to arcade button is
   * the last thing in the menu column; sideways, the column that holds the
   * menu's doors is the logo's, so it stands right under Play and
   * Multiplayer, its full width, in the row beside Win at. As a full-width
   * row of its own it was 50px this screen did not have — the paddock already
   * scrolled by a few pixels before it — and the way out is precisely the
   * thing that runs off the bottom when a column is too tall.
   *
   * The update pill, which used to own the cell above Play, now stands in the
   * hint's row when it shows, and the keyboard hint yields to it: the pill is
   * rare, transient and the more important of the two, and the hint is a line
   * of keys a phone does not have. One row either way, so an update waiting
   * cannot push the way out off the screen either.
   */
  #update {
    grid-area: hint;
    justify-self: center;
    width: min(100%, 340px);
    margin: 0;
    min-height: 38px;
    padding: 4px 18px;
  }
  #menu:has(#update:not([hidden])) .hint {
    display: none;
  }
  /*
   * AND THE ONE ROW THE CORNER WOULD SIT ON GIVES UP THE WIDTH, rather than the
   * panel giving up height it does not have. Sideways the paddock is already as
   * tall as it can be — the note above this block explains why a sixth row
   * would not fit — so buying the cluster its room with a padding-top, the way
   * maxgear did, would push the way out below the fold, which is the thing this
   * whole block exists to prevent. What the cluster covers is small and known:
   * 46px down, and --corner-bite in from the right, and the only control that
   * high on the right is the top row of the settings column. So that row alone
   * ends short, and it ends exactly as short as it has to. The pills stay equal
   * to each other, which is what the hub's rule asks of a group; they are
   * simply a little narrower than the four rows below them.
   */
  #row-riders {
    grid-area: riders;
    padding-right: var(--corner-bite);
  }
  /*
   * AND UNDER 820px THE COLUMNS SWAP INSTEAD OF THE ROW GIVING WAY.
   *
   * What the taper leaves that row is U/2 - 148 whatever the width (the panel
   * stops growing at 640 exactly as the bite stops shrinking, so the two
   * cancel): 184px on a 664px phone, which is a 76px label and two 44px pills
   * with nothing to spare, and 136px on a 568px one, which is two pills of 21
   * and not a control any more. Three plates cost 42px more than two and this
   * is where that lands.
   *
   * The cut was 660 until 2026-10-11, and that sum was done for the English
   * of the day: the 84px gutter bokmål needs and "To spillere" / "Two
   * players" each wanting about 80px of pill make the row's real floor about
   * 262px, which U/2 - 148 reaches at 820. Between 660 and 820 the riders row
   * was crushed to two overlapping slivers (seen at 667, an iPhone 8 on its
   * side). 844 — most current phones — still keeps the logo on the left.
   *
   * So on the narrowest phones the settings move to the left and the logo takes
   * the corner, because art can be covered by three plates and a row of buttons
   * cannot. Same rows in the same order at the same height — only which side
   * they are on changes — and the taper goes with the corner onto the logo,
   * which is fitted into its cell anyway and simply centres in what is left.
   */
  @media (max-width: 820px) {
    #menu {
      grid-template-areas:
        "riders  logo"
        "rivals  logo"
        "arena   logo"
        "turning play"
        "target  tools"
        "hint    hint";
    }
    #row-riders {
      padding-right: 0;
    }
    #title {
      --logo-bite: var(--corner-bite);
    }
  }
  /* MULTIPLAYER at the portrait size and tracking ran a few pixels past its
   * pill in a half-column; the doors give up a little letter-spacing. */
  .play .primary {
    font-size: 15px;
    letter-spacing: 0.04em;
  }
  #row-rivals {
    grid-area: rivals;
  }
  #row-arena {
    grid-area: arena;
  }
  #row-turn {
    grid-area: turning;
  }
  #row-target {
    grid-area: target;
  }
  .play {
    grid-area: play;
    margin: 0;
  }
  #menu .hint {
    grid-area: hint;
    align-self: center;
    margin: 0;
  }
  #menu .menu-tools {
    grid-area: tools;
    align-self: end;
    margin: 4px 0 0;
  }
  /* The hint runs the full width rather than sharing a row with something: at
   * half a panel it wraps to three lines. Across the whole panel it is one. */
  /* The build line leaves the grid for the panel's bottom padding, where it
   * sits under the hint without costing a row and its gap. It is a number for
   * whoever is debugging a release, not a control. */
  #build {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 1px;
    margin: 0;
    font-size: 9px;
    line-height: 1;
    text-align: center;
  }

  /*
   * The menu panel is short enough that it needs no areas: put the two slider
   * rows beside each other and let everything else — heading, note, buttons —
   * keep the full width it was written for. It was two selectors here until
   * v1.9, when #sound and #pause became one panel; the rules did not change,
   * because the two were already being given the same layout for the same
   * reason, which was most of the argument for merging them.
   *
   * Both faces fit standing up sideways with room over: the pause is a kicker,
   * a heading, a note, one slider row, Keep riding and the two ways out — about
   * 230px of the 300 a sideways phone has — and the mixer is shorter still.
   */
  #pause {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-content: center;
    align-content: safe center;
    gap: 8px 20px;
  }
  #pause > :not(.row) {
    grid-column: 1 / -1;
  }
  #pause h2 {
    margin: 0;
  }
  /* The kicker goes sideways, as the start screen's does: the heading and
   * the note under it already say what this moment is, and the language row
   * (2026-10-10) needed the 24px. */
  #pause .kicker {
    display: none;
  }
  /* The language row takes the full width under the two sliders by default —
   * on the narrowest sideways phones a half-column cannot hold a caption and
   * two flagged pills. See just below for where there is room. */
  #row-lang {
    grid-column: 1 / -1;
  }
  #pause .hint {
    margin: 0;
  }
  #pause .primary {
    margin: 0;
  }
  /* The two ways out of a match, and the way out of the mixer: the grid's own
   * row gap is already holding them apart, so their portrait margins are height
   * this does not have. */
  #pause .menu-tools {
    margin: 0;
  }
  #menu-back {
    margin-top: 0;
  }
  /* A little apart from the row above it, as standing up; the grid gap is
   * already most of that. */
  #pause-quit {
    margin-top: 4px;
  }
  /* WHERE THERE IS WIDTH, the language row and Resume share a line: the row
   * in the left column, the big pill in the right. That is the line the row
   * would otherwise have cost, which keeps the pause face as tall as it was
   * before languages arrived. 700px is where a half-column (292px) still fits
   * the 84px caption and two flagged pills of about 95px each. Outside a match
   * Resume is hidden and the row simply sits on the left. */
  @media (min-width: 700px) {
    #row-lang {
      grid-column: 1;
    }
    #pause #resume {
      grid-column: 2;
    }
  }

  /* The steering buttons are the one thing that must NOT shrink sideways —
   * they are the whole control scheme on a touch device — but a 104px circle
   * on a 320px-tall screen owns a third of it, so they come in a little and
   * sit tighter to the corners. */
  #touch {
    padding: 0 12px 10px;
  }
  #touch button {
    width: 76px;
    height: 76px;
    font-size: 28px;
  }
  #spectate {
    bottom: 12px;
  }
}


/* ============ Touch devices: bigger targets, no live blur ============
 *
 * Two things a finger needs that a mouse does not, found 2026-10-02.
 *
 * TARGETS. The owner: "the buttons need to be bigger on touch devices, they
 * are easily missed." The steering circles grow, and each also gets an
 * invisible margin (::before) that counts as the button, so a thumb that
 * lands near it still steers — the circle is what you see, the margin is
 * what you hit. Corner plates go to 48px (44px is the usual floor for a
 * fingertip), and the menu pills grow too, except on a sideways phone where
 * the extra height would make the paddock scroll (hub §2).
 *
 * BLUR. backdrop-filter re-blurs whatever is behind the element every frame,
 * and behind these is a canvas that redraws every frame — on an old tablet
 * that is a full-screen blur per panel per frame. The glass goes a little
 * more opaque instead, so the panel still reads as the same panel.
 */
@media (pointer: coarse) {
  .panel {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(12, 18, 52, 0.86);
  }
  #corner-tools {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(8, 12, 36, 0.84);
  }
  .icon-button {
    width: 48px;
    height: 48px;
  }
  #touch button {
    position: relative;
    width: 120px;
    height: 120px;
    font-size: 40px;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(20, 30, 80, 0.62);
  }
  #touch button::before {
    content: "";
    position: absolute;
    inset: -28px;
    border-radius: 50%;
  }
  /* Keys a thumb has not got: the caption keeps its width for the button. */
  #spectate .how,
  #skip kbd {
    display: none;
  }
  #skip {
    min-height: 44px;
  }
}
@media (pointer: coarse) and (max-width: 600px) {
  #touch button {
    width: 100px;
    height: 100px;
  }
}
@media (pointer: coarse) and (min-height: 501px) {
  .seg button {
    min-height: 46px;
  }
}
@media (pointer: coarse) and (orientation: landscape) and (max-height: 500px) {
  .icon-button {
    width: 44px;
    height: 44px;
  }
  /* Sideways the circles grow only a little (76 -> 84px) to leave the arena
   * its height; the hit margin does the rest of the finding. */
  #touch button {
    width: 84px;
    height: 84px;
    font-size: 30px;
  }
  #touch button::before {
    inset: -22px;
  }
}
