/* Bladevolley — mobile-landscape-first. The canvas is the game; DOM is topbar, HUD, overlays and
   speech bubbles.

   The whole DOM layer is design 1a "Sticker Book" (Claude Design project 50aa7b68, section #1a):
   cream card stock over the painted arena, 3px ink outlines, hard offset shadows, halftone dot
   fill, tilted sticker labels, Bowlby One SC display type. The start screen is 2a "Sticker
   lockup" — the painted cover with the title stack pushed to the right, over the empty sky the
   art leaves there.

   Every colour below is the mockup's own value. The canvas has its own palette (render.js,
   venues.js) and does not read these; the one crossing is --you, which main.js's popScore reads
   for the score glow. */

:root {
  --ink: #1b2733;
  --card: #fdf3e3; /* cream card stock */
  --panel: #ffffff; /* the white panel inside a card */
  --paper: #efe9dc; /* the page behind the stage */
  --accent: #e8724f; /* orange — primary buttons, numbered steps, slider fill */
  --you: #17a398; /* teal — the player's side, everywhere */
  --boss: #a844b0; /* purple — "the boss" sticker */
  --gold: #ffe066; /* the tilted callout stickers */
  --muted: #5d6a76;
  --muted-2: #7a8794;
  --dim: #9aa4ad; /* locked */
  /* The halftone the cream stock is printed with. One declaration, used by every card. */
  --halftone: radial-gradient(#1b273314 1.1px, transparent 1.2px);

  /* Bowlby One SC is a small-caps display face, so everything set in it is uppercased anyway;
     the fallbacks are the heaviest thing likely to be installed, because a page that loses the
     webfont must still read as a poster and not as body copy. */
  --font-display: "Bowlby One SC", "Arial Black", "Helvetica Neue", system-ui, sans-serif;
  --font-ui: "Nunito", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* The keyboard/controller focus ring, as two numbers rather than one rule, because two places
     need them: the rule that DRAWS the ring (bottom of this file) and every scroll container that
     has to reserve room for it. --ring-reach is derived, never retyped, so widening the ring can
     never leave a list clipping it again. */
  --ring-w: 3px;
  --ring-gap: 3px; /* outline-offset — the clear air between the control and its ring */
  --ring-reach: calc(var(--ring-w) + var(--ring-gap)); /* how far outside its box the ring lands */
}

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

html,
body {
  height: 100%;
  /* The document itself never scrolls, but Chrome-on-Android's pull-to-refresh does not need it
     to: a downward swipe starting anywhere with touch-action:auto (the topbar) reloads the page,
     which mid-match means losing the match. */
  overscroll-behavior: none;
}

body {
  font-family: var(--font-ui);
  color: var(--ink);
  background: var(--paper);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  height: 100dvh;
}

/* env() is only readable from CSS, so the safe-area insets are parked here as padding on a
   zero-size box and read back with getComputedStyle (main.js measureViewport). The HIT button's
   hit test and its picture are both placed from that ONE reading — see input.js controlGeometry. */
#inset-probe {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  visibility: hidden;
  pointer-events: none;
  padding-top: env(safe-area-inset-top, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left: env(safe-area-inset-left, 0px);
}

.hidden {
  display: none !important;
}

/* ---------- topbar ---------- */

.topbar {
  display: flex;
  align-items: center;
  gap: 8px;
  /* At 375 px (a phone held upright) the three buttons are wider than the bar and the last one —
     the way out — was clipped off the right edge with nothing to scroll. */
  flex-wrap: wrap;
  row-gap: 5px;
  justify-content: flex-end;
  padding: 6px 10px;
  padding-left: max(10px, env(safe-area-inset-left));
  padding-right: max(10px, env(safe-area-inset-right));
  background: var(--card);
  background-image: var(--halftone);
  background-size: 7px 7px;
  border-bottom: 3px solid var(--ink);
  flex: 0 0 auto;
}

.brand {
  font-family: var(--font-display);
  font-size: 15px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.flex-spacer {
  flex: 1;
}

/* ---------- the shared kit: buttons, stickers, pills ---------- */

.ghost-btn {
  font: inherit;
  font-size: 13px;
  font-weight: 900;
  color: var(--ink);
  background: var(--panel);
  border: 3px solid var(--ink);
  border-radius: 10px;
  padding: 6px 13px;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

.ghost-btn:hover {
  background: #fffaf0;
}

.ghost-btn:active {
  translate: 0 2px;
}

.big-btn {
  font-family: var(--font-display);
  font-size: clamp(17px, 2.4vw, 22px);
  color: #fff;
  background: var(--accent);
  border: 3px solid var(--ink);
  border-radius: 14px;
  box-shadow: 0 5px 0 var(--ink);
  padding: 11px 34px;
  cursor: pointer;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.big-btn:active {
  translate: 0 3px;
  box-shadow: 0 2px 0 var(--ink);
}

/* A sticker is a rotated, ink-outlined chip with a hard shadow — the mockup's way of shouting one
   short fact. .tilt-l / .tilt-r pick which way it leans; nothing else about them differs. */
/* A primary button that cannot be pressed has to LOOK like it: the versus sheet ships Start
   disabled until both seats have a device, and a full-accent button there invites the one click the
   screen exists to explain. The hard shadow goes with it — the button stops standing off the page. */
.big-btn:disabled {
  background: #d9d3c6;
  color: #8b8578;
  box-shadow: 0 3px 0 #b9b3a6;
  cursor: default;
}

.big-btn:disabled:active {
  translate: none;
}

.sticker {
  display: inline-block;
  background: var(--gold);
  border: 3px solid var(--ink);
  border-radius: 8px;
  box-shadow: 3px 3px 0 var(--ink);
  padding: 4px 11px;
  font-size: 12px;
  font-weight: 900;
  color: var(--ink);
  white-space: nowrap;
}

.sticker.tilt-l {
  transform: rotate(-3deg);
}

.sticker.tilt-r {
  transform: rotate(3deg);
}

.sticker.boss {
  background: var(--boss);
  color: #fff;
}

/* A pill is the quiet version: no rotation, no shadow, 2px outline. Status, venue, a binding. */
.pill {
  display: inline-block;
  border: 2px solid var(--ink);
  border-radius: 20px;
  padding: 2px 9px;
  font-size: 11px;
  font-weight: 900;
  white-space: nowrap;
  background: #f1efe6;
  color: var(--ink);
}

.pill.win {
  background: var(--you);
  color: #fff;
}

.pill.next {
  background: var(--gold);
}

.pill.gold {
  background: var(--gold);
}

/* ---------- stage ---------- */

.stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  user-select: none;
  -webkit-user-select: none;
}

/* `touch-action: none` LIVES ON THE CANVAS, NOT ON THE STAGE, and that is the fix for the bug that
   made this game unplayable on a phone. The overlays are children of the stage, so declaring it
   there suppressed touch panning for every one of them: the match-over card could not be scrolled
   to its Rematch button, the roster could not be scrolled to opponents 4-6, and the options sheet
   could not be scrolled to the music slider. On desktop the wheel still worked, which is why it
   went unnoticed. The canvas fills the stage and sits under every overlay, so the game's own
   gestures are unaffected — and no overlay has a `none` ancestor any more, which does not depend on
   how a browser resolves the touch-action intersection rule. */
#canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
}

/* The in-stage pause button, which is the whole of the topbar's job during a match on a phone.
   Top-LEFT: the score capsule is centred and the right half of the stage belongs to the aim
   thumb. 44 px is the smallest target worth shipping; the insets keep it off a rounded corner. */
#pause-fab {
  position: absolute;
  top: calc(8px + env(safe-area-inset-top, 0px));
  left: calc(8px + env(safe-area-inset-left, 0px));
  z-index: 5;
  display: none;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  font-size: 17px;
  line-height: 1;
  color: var(--ink);
  background: #fdf3e3d9;
  border: 3px solid var(--ink);
  border-radius: 50%;
  box-shadow: 0 3px 0 var(--ink);
  cursor: pointer;
}

#pause-fab:active {
  translate: 0 2px;
  box-shadow: 0 1px 0 var(--ink);
}

/* A fold-out is the phone's answer to a column it has no room for: the panels it owns are hidden
   until it is pressed. Both the button and the folding are coarse-pointer-only — where there is
   room for every column, all of them are simply open and the button does not exist. */
.fold-btn {
  display: none;
  align-self: flex-start;
  flex: 0 0 auto;
}

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

#hud {
  position: absolute;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  text-align: center;
  pointer-events: none;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

/* One capsule, three cells: your teal block, the cream touch counter, the opponent's own colour.
   overflow:hidden is what makes the outer radius cut the coloured cells. */
#score-line {
  display: flex;
  align-items: stretch;
  border: 3px solid var(--ink);
  border-radius: 16px;
  box-shadow: 0 5px 0 var(--ink);
  overflow: hidden;
  font-family: var(--font-display);
}

.score-side {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 16px;
  color: #fff;
  font-size: clamp(14px, 2vw, 18px);
  text-transform: uppercase;
}

.score-side.you {
  background: var(--you);
}

/* --opp is the opponent's roster colour, set by main.js when the match opens. A translucent black
   chip rather than a darkened tint: it has to sit legibly on all six jersey colours, and
   color-mix would need a fallback for the one browser that lacks it anyway. */
.score-side.opp {
  background: var(--opp, var(--boss));
}

.score-num {
  background: rgba(0, 0, 0, 0.3);
  border-radius: 6px;
  padding: 1px 10px;
  font-size: clamp(17px, 2.4vw, 21px);
  /* popScore (main.js) scales this — inline boxes ignore transform */
  display: inline-block;
}

.score-touches {
  background: var(--card);
  border-left: 3px solid var(--ink);
  border-right: 3px solid var(--ink);
  padding: 5px 15px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
}

.touches-label {
  font-family: var(--font-ui);
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.8px;
  color: var(--muted);
  text-transform: uppercase;
}

.score-name.serving::after {
  content: "";
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-left: 7px;
  border-radius: 50%;
  background: var(--gold);
  border: 2px solid var(--ink);
  vertical-align: 1px;
}

#touch-pips {
  display: flex;
  gap: 5px;
  justify-content: center;
}

.pip {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 2px solid var(--ink);
  background: #fff;
}

/* The newly consumed touch pops in. The class is minted by renderHud exactly once per touch —
   the row is only rebuilt when its state moves — so this plays once per pip, never on refresh. */
.pip.ping {
  animation: pip-ping 0.38s cubic-bezier(0.2, 1.4, 0.4, 1);
}

@keyframes pip-ping {
  from {
    transform: scale(2.2);
  }
  to {
    transform: scale(1);
  }
}

/* Who is about to serve, as a tilted sticker under the capsule (1a). Shown only while the sim is
   in its serve phase, so it disappears the instant the rally opens. */
#serve-flag {
  font-size: 11px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

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

/* ---------- speech bubbles ---------- */

#bubble-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 4;
}

/* Style exploration 3b: Shantell Sans on the hand-drawn warm balloon. Two registers —
   .say is the cream wobbly-ink balloon, .shout the gold slab, both with an outlined SVG
   tail (built in main.js) pointing down-right at the mouth anchor. Deliberately NOT restyled
   into the sticker kit: these are the characters' voices inside the picture, not chrome around
   it, and the 1a mockup does not cover them. */

.bubble {
  /* How long the balloon holds before it fades, and how long the fade takes. main.js overrides
     both per bubble — the duration is derived from the LINE'S LENGTH (banter.js), because a
     four-word taunt and a twelve-word one cannot need the same time on screen. These values are
     the fallback for a bubble built without them, and they are the shortest line's timing. */
  --hold: 2820ms;
  --fade: 300ms;
  position: absolute;
  /* The whole balloon sits up-LEFT of the mouth anchor, so the tail — attached unrotated to
     the balloon's lower right edge, pointing right-down like the design's — lands on the
     anchor. Aiming is done by placing the balloon, never by rotating the tail: a tail
     rotated about its base swings off the border and reads as detached. */
  transform: translate(calc(-100% - 30px), calc(-100% - 14px));
  max-width: 230px;
  font-family: "Shantell Sans", "Comic Sans MS", cursive;
  text-align: center;
}

.bubble .pop {
  animation: fadeout var(--fade) ease-in var(--hold) forwards;
}

.bubble.say .pop {
  animation: wobble-in 0.4s ease-out, fadeout var(--fade) ease-in var(--hold) forwards;
  transform-origin: 100% 60%;
}

.bubble.shout .pop {
  animation: slam-in 0.18s ease-out, fadeout var(--fade) ease-in var(--hold) forwards;
  transform-origin: 90% 55%;
}

.bubble.shout .jig {
  animation: jitter 0.22s linear infinite;
}

.bubble .balloon {
  position: relative;
}

.bubble.say .balloon {
  background: #fffdef;
  border: 2.5px solid #6e571c;
  border-radius: 255px 18px 225px 18px / 18px 225px 18px 255px;
  padding: 10px 12px;
  transform: rotate(-1.5deg);
  font-weight: 500;
  font-size: 14.5px;
  line-height: 1.3;
  color: #2e2610;
}

.bubble.shout .balloon {
  background: #dbab47;
  border: 2.5px solid #4a3a10;
  border-radius: 225px 25px 255px 20px / 20px 255px 18px 225px;
  padding: 12px 14px;
  transform: rotate(-2.5deg);
  font-weight: 700;
  font-size: 19px;
  line-height: 1.15;
  letter-spacing: 0.5px;
  color: #2a1d04;
  text-transform: uppercase;
}

.bubble .tail {
  position: absolute;
  overflow: visible;
  /* The base (the polygon's vertical left edge) sits ~7px inside the balloon, so the fill
     masks the right border where the wedge leaves — the design's own attachment trick. */
}

.bubble.say .tail {
  width: 34px;
  height: 30px;
  right: -26px;
  bottom: -10px;
}

.bubble.shout .tail {
  width: 40px;
  height: 36px;
  right: -28px;
  bottom: -12px;
}

@keyframes wobble-in {
  0% {
    transform: scale(0) rotate(-12deg);
    opacity: 0;
  }
  60% {
    transform: scale(1.08) rotate(2.5deg);
    opacity: 1;
  }
  80% {
    transform: scale(0.97) rotate(-1deg);
  }
  100% {
    transform: scale(1) rotate(0deg);
  }
}

@keyframes slam-in {
  0% {
    transform: scale(2.4) rotate(-6deg);
    opacity: 0;
  }
  100% {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}

@keyframes jitter {
  0%,
  100% {
    transform: translate(0, 0) rotate(0deg);
  }
  25% {
    transform: translate(1.4px, -1px) rotate(0.5deg);
  }
  50% {
    transform: translate(-1.4px, 1px) rotate(-0.5deg);
  }
  75% {
    transform: translate(1px, 1.4px) rotate(0.3deg);
  }
}

@keyframes fadeout {
  to {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bubble.say .pop,
  .bubble.shout .pop {
    animation: fadeout var(--fade) ease-in var(--hold) forwards;
  }

  .bubble.shout .jig {
    animation: none;
  }
}

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

/* The bottom/right padding is not decoration: every card casts an 8px hard shadow down-right and
   a full-height sheet would otherwise clip its own. */
.overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(27, 39, 51, 0.42);
  z-index: 10;
  padding: 10px 20px 20px 10px;
}

.overlay-card {
  background-color: var(--card);
  background-image: var(--halftone);
  background-size: 7px 7px;
  border: 3px solid var(--ink);
  border-radius: 22px;
  box-shadow: 8px 8px 0 var(--ink);
  padding: 20px 24px;
  max-width: 460px;
  width: 100%;
  max-height: 100%;
  overflow-y: auto;
  text-align: center;
  position: relative;
}

.overlay-card.wide {
  max-width: 620px;
}

/* The roster, help and options screens are full-bleed sheets in the mockup (inset 22–26px), not
   centred cards: they carry a header row, a body that has to breathe, and a footer.

   height:auto, not 100%: the mockup is 960×540 and a desktop stage is ~750 px tall, so a sheet
   pinned to full height opens a dead band of cream between the content and the footer that the
   design never had. Auto keeps the mockup's proportions at any height and still scrolls when the
   content genuinely outgrows the stage. */
.overlay-card.sheet {
  max-width: 960px;
  height: auto;
  text-align: left;
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 11px;
  /* The sheet itself never scrolls — its BODY does (the roster grid, the open tab panel, the
     options columns, each `flex: 1 1 auto; min-height: 0`). Scrolling the whole sheet instead
     pushed its own footer button off the bottom of a landscape phone, which is the one control
     that gets you out of the screen. */
  overflow: hidden;
}

.overlay-card h1 {
  font-family: var(--font-display);
  font-size: 30px;
  margin-bottom: 6px;
  text-transform: uppercase;
}

.overlay-card h2 {
  font-family: var(--font-display);
  font-size: clamp(19px, 2.8vw, 26px);
  text-transform: uppercase;
  letter-spacing: 0.3px;
}

.overlay-card h3 {
  font-family: var(--font-display);
  font-size: 15px;
  text-transform: uppercase;
}

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  flex: 0 0 auto;
}

.sheet-foot {
  margin-top: auto;
  padding-top: 4px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  flex: 0 0 auto;
}

.sheet-foot .fine {
  margin: 0 auto 0 0;
  text-align: left;
}

/* The white panel that sits on the cream stock — every list, column and group in 1a is one. */
.panel {
  background: var(--panel);
  border: 3px solid var(--ink);
  border-radius: 16px;
  padding: 13px 16px;
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}

.tagline {
  color: #3a4753;
  font-size: clamp(13px, 1.6vw, 15px);
  font-weight: 800;
  line-height: 1.4;
  text-wrap: pretty;
}

.controls-hint {
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
}

.fine {
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  text-align: center;
  text-wrap: pretty;
}

.btn-row {
  display: flex;
  gap: 10px;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 14px;
}

/* ---------- start screen (design 2a) ---------- */

/* Not a card: the cover art is the screen. The lockup goes right, where the painting leaves sky. */
#title-overlay {
  background: none;
  padding: 0;
  align-items: stretch;
  justify-content: flex-end;
  overflow: hidden;
}

#cover-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* 38% horizontally, per the mockup: the spiker is left of centre and everything right of him is
     sky, so this keeps him on screen when a wide window crops the sides.
     12% vertically is NOT the mockup's `center`, because the mockup's frame was 960×540 — exactly
     the raster's own aspect — and never cropped vertically at all. A landscape phone's stage is
     ~2.5:1, which crops 130 px off the height, and centred that took the top off his head and cut
     the raised paddle in half. Biased up it costs sand instead, and costs nothing on desktop,
     where the fit is height-driven and this value is unused. */
  object-position: 38% 12%;
}

.cover-scrim {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 62%;
  background: linear-gradient(90deg, #0b3d6600 0%, #0b3d6644 45%, #0b3d66aa 100%);
}

.cover-stack {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: clamp(10px, 1.8vh, 18px);
  text-align: right;
  padding: 14px clamp(16px, 4vw, 44px) 14px 0;
  max-width: min(56%, 460px);
}

/* badge + wordmark lean together, as one applied sticker */
.cover-title {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  transform: rotate(-3deg);
}

.cover-badge {
  background: var(--accent);
  color: #fff;
  font-family: var(--font-display);
  letter-spacing: 2px;
  font-size: clamp(9px, 1.2vw, 13px);
  padding: 3px 11px;
}

#title-wordmark {
  margin: 8px 0 0;
  font-family: var(--font-display);
  /* Sized against BOTH axes: on a landscape phone the stage is ~325 px tall, and a wordmark
     scaled on width alone pushed the badge off the top and the controls hint off the bottom. */
  font-size: clamp(28px, min(7vw, 12vh), 76px);
  line-height: 0.92;
  color: var(--card);
  text-transform: uppercase;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  /* paint-order puts the fill back on top of its own stroke, so the 9px ink outline grows
     outward instead of eating half the letterform. */
  -webkit-text-stroke: clamp(4px, 0.95vw, 9px) var(--ink);
  paint-order: stroke fill;
  text-shadow: 8px 9px 0 var(--you);
}

/* The over-title — a small line over BLADE/VOLLEY, the way a licensed name sits over a game's.
   (No example of one in this comment: this file ships in the public deploy and the edition that
   has an over-title is named after a real person.)
   Empty in the shipped markup and filled by main.js only in an edition whose brand carries one,
   so `:empty` below is what keeps the public stack identical to what it has always been.
   Everything here is an OVERRIDE of the h1 it lives in: it inherits a 76px display face, a 9px
   stroke and a big offset shadow, and all three have to come down together or it reads as a
   second wordmark rather than as one line of the same lockup. It scales on the same two axes as
   the wordmark (min(vw, vh)) because the 2a stack's tight case is a landscape phone's HEIGHT. */
#title-overline {
  font-size: clamp(11px, min(2.5vw, 4.3vh), 27px);
  letter-spacing: 1px;
  line-height: 1;
  /* Pulls the big lines up into its own leading: the over-title costs its cap height, not a full
     line box, in a stack that already fits a landscape phone with about 11 px to spare. */
  margin-bottom: -0.08em;
  -webkit-text-stroke: clamp(1.5px, 0.32vw, 3px) var(--ink);
  text-shadow: 3px 3px 0 var(--you);
}

#title-overline:empty {
  display: none;
}

.cover-card {
  background: var(--card);
  border: 3px solid var(--ink);
  border-radius: 14px;
  box-shadow: 5px 5px 0 var(--ink);
  padding: 10px 15px;
  text-align: right;
}

.cover-row {
  display: flex;
  gap: 9px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* This one line sits on bare painting rather than on a card, and the painting is bright sky —
   a plain drop shadow disappeared into it. The pill is the cheapest thing that holds contrast
   wherever the crop puts it. */
#title-controls {
  color: #eef4f9;
  background: #0b3d66b3;
  border-radius: 20px;
  padding: 4px 12px;
  text-align: right;
}

#title-controls:empty {
  display: none;
}

/* ---------- match over (1a) ---------- */

.over-card {
  text-align: right;
  /* The portrait disc hangs off the top-left corner, so this card cannot be a scroll container:
     `overflow-y: auto` computes overflow-x to auto too and clips the disc against the card edge.
     It is safe here and only here — the card's content is a headline, two numbers, one line of
     taunt and a button row, which fits the shortest stage the game supports. */
  overflow: visible;
  margin-top: 26px; /* the room the disc hangs into */
}

#over-title {
  color: var(--opp, var(--boss));
}

#over-title.you-won {
  color: var(--you);
}

.over-score {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 22px;
  margin-top: 16px;
}

.over-col-label {
  display: block;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 1px;
  color: var(--muted);
  text-transform: uppercase;
}

.over-col-num {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(44px, 9vw, 74px);
  line-height: 0.9;
}

.over-col-num.you {
  color: var(--you);
}

.over-col-num.opp {
  color: var(--opp, var(--boss));
}

.over-dot {
  font-family: var(--font-display);
  font-size: 30px;
  color: #c6bfae;
}

/* the taunt, in the mockup's white bordered box rather than as loose italic text */
.over-line {
  margin-top: 18px;
  background: var(--panel);
  border: 3px solid var(--ink);
  border-radius: 14px;
  padding: 12px 16px;
  font-size: clamp(14px, 1.9vw, 17px);
  font-style: italic;
  font-weight: 700;
  color: #3a4753;
  text-align: left;
  text-wrap: pretty;
  min-height: 1.2em;
}

.over-line:empty {
  display: none;
}

/* ---------- the portrait disc (roster cards and the match-over card) ---------- */

/* The opponent's own head inside his team colour. The ring IS the old coloured chip — same
   colour, same job — and the tint behind the cutout is that colour too, because a head asset is
   transparent outside its silhouette and needs something to sit on. */
.r-face {
  --face: 58px;
  position: relative;
  flex: none;
  display: block;
  width: var(--face);
  height: var(--face);
  border-radius: 50%;
  overflow: hidden;
  background: var(--chip); /* fallback: a browser without color-mix gets the solid chip behind the head */
  background: color-mix(in srgb, var(--chip) 20%, #fff);
  border: 3px solid var(--ink);
}

/* One art-space frame (render.js HEAD_PORTRAIT_FRAME) for all six heads, arriving as four ratios
   from portraitCrop(): the asset is drawn --pf-w by --pf-h discs and slid --pf-x discs left and
   --pf-y up, which puts the frame on the disc. Every percentage below resolves against the disc,
   which is square, so both axes stay on one scale and the portrait cannot stretch. */
.r-head {
  position: absolute;
  left: calc(var(--pf-x) * -100%);
  top: calc(var(--pf-y) * -100%);
  width: calc(var(--pf-w) * 100%);
  height: calc(var(--pf-h) * 100%);
  /* an SVG that fails to load must not draw a broken-image glyph over the ring */
  color: transparent;
}

/* Locked: the head flattened to a silhouette. The hairline still says who it is — a mohawk is a
   tease, a grey dot is not — while the face itself stays behind the lock. */
.r-face.dim {
  background: #c9cdd1;
  border-color: var(--dim);
}

.r-face.dim .r-head {
  filter: grayscale(1) brightness(0);
  /* 0.8, and the card's own .locked opacity takes it to ~0.44 over the grey disc: enough that the
     hairline still reads at 58 px, far enough from black that it reads as withheld. */
  opacity: 0.8;
}

.over-face {
  position: absolute;
  top: -26px;
  left: 30px;
  --face: 78px;
  box-shadow: 4px 4px 0 var(--ink);
}

/* ---------- roster (1a) ---------- */

/* 250px against the sheet's 960 max width lands on the mockup's three columns, and steps down to
   two and then one as the stage narrows. */
#roster-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 10px;
  flex: 1 1 auto;
  align-content: start;
  min-height: 0;
  overflow-y: auto;
}

.roster-card {
  font: inherit;
  position: relative;
  background: var(--panel);
  border: 3px solid var(--ink);
  border-radius: 16px;
  box-shadow: 4px 4px 0 var(--ink);
  padding: 11px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  text-align: center;
}

.roster-card:not(.locked):hover {
  background: #fffaf0;
}

.roster-card:not(.locked):active {
  translate: 0 2px;
  box-shadow: 0 2px 0 var(--ink);
}

.roster-card.boss {
  background: #f6ecff;
}

.roster-card.locked {
  background: #efece3;
  border: 3px dashed var(--dim);
  box-shadow: none;
  opacity: 0.85;
  cursor: default;
}

/* the "the boss" tab straddles the card's top edge, so the card owns the vertical room for it */
.roster-card.boss .r-face {
  margin-top: 6px;
}

.r-boss {
  position: absolute;
  top: -14px;
  left: 50%;
  transform: translateX(-50%) rotate(-3deg);
  font-size: 11px;
  padding: 2px 10px;
  box-shadow: 2px 2px 0 var(--ink);
}

.r-name {
  font-family: var(--font-display);
  font-size: 15px;
  line-height: 1.2;
  text-transform: uppercase;
}

.roster-card.locked .r-name {
  color: var(--muted-2);
}

.r-tag {
  font-size: 12px;
  line-height: 1.3;
  font-weight: 700;
  color: #4a5763;
  text-wrap: pretty;
}

.roster-card.locked .r-tag {
  color: #8b959e;
}

.r-pills {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: auto;
  padding-top: 4px;
}

/* ---------- how it works: tabs (1a) ---------- */

.tabs {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.tab {
  font: inherit;
  font-size: 13px;
  font-weight: 900;
  color: var(--ink);
  background: var(--panel);
  border: 3px solid var(--ink);
  border-radius: 10px;
  padding: 6px 14px;
  cursor: pointer;
}

.tab.is-on {
  background: var(--accent);
  color: #fff;
  box-shadow: 0 3px 0 var(--ink);
}

.tab-panel {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Where there is room to spend, a floor holds the sheet still while you flick between tabs: the
   three panels are different heights, and a card that resizes under the cursor moves "Got it" out
   from under it. 228 px is the tallest of the three at desktop width — measured, not guessed.
   Deliberately behind a min-height query: on a landscape phone that floor is bigger than the
   whole card, and it is the reason the panel would not shrink to fit. */
@media (min-height: 560px) {
  .tab-panel {
    min-height: 228px;
  }
}

.help-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 12px;
}

/* a numbered rule: the orange (or teal) disc, then the sentence */
.step {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}

.step + .step {
  margin-top: 12px;
}

.step-n {
  flex: 0 0 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--accent);
  border: 3px solid var(--ink);
  color: #fff;
  font-family: var(--font-display);
  font-size: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.step-n.teal {
  background: var(--you);
}

.step p {
  font-size: 14px;
  line-height: 1.4;
  font-weight: 700;
  color: #3a4753;
  text-wrap: pretty;
}

.panel ul {
  padding-left: 18px;
  font-size: 13.5px;
  line-height: 1.45;
  font-weight: 600;
}

.panel li + li {
  margin-top: 4px;
}

/* the gold rock-paper-spike strip */
.rps {
  background: var(--gold);
  border: 3px solid var(--ink);
  border-radius: 16px;
  padding: 12px 16px;
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}

.rps-title {
  font-family: var(--font-display);
  font-size: 14px;
  white-space: nowrap;
  text-transform: uppercase;
}

.rps-chain {
  display: flex;
  gap: 9px;
  align-items: center;
  font-size: 14px;
  font-weight: 900;
  flex-wrap: wrap;
}

.rps-chain b {
  background: var(--panel);
  border: 2px solid var(--ink);
  border-radius: 8px;
  padding: 4px 10px;
  font-weight: 900;
}

/* ---------- versus setup (1a) ---------- */

/* A slimmed roster card: the same white panel, ink outline and hard offset shadow, minus everything
   that is about PROGRESS (tagline, venue pill, status pill, the boss tab) because none of it applies
   in versus. Two columns inside a ~280px panel; the whole grid scrolls inside its own panel rather
   than growing the sheet, which is what keeps three columns of picks inside a short viewport. */
.pick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 6px;
  align-content: start;
  min-height: 0;
  overflow-y: auto;
}

.pick-card {
  font: inherit;
  background: var(--panel);
  border: 3px solid var(--ink);
  border-radius: 12px;
  box-shadow: 3px 3px 0 var(--ink);
  padding: 7px 5px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
}

.pick-card:hover {
  background: #fffaf0;
}

.pick-card:active {
  translate: 0 2px;
  box-shadow: 0 1px 0 var(--ink);
}

/* the chosen one wears its own jersey colour, so the two columns read as two teams at a glance */
.pick-card.is-on {
  background: #fffaf0;
  border-color: var(--chip);
  box-shadow: 3px 3px 0 var(--chip);
}

/* Held by the other player. Same treatment as a locked roster card — the renderer caches rigs on
   their appearance, so two seats on one character would be two indistinguishable figures. */
.pick-card.taken {
  background: #efece3;
  border: 3px dashed var(--dim);
  box-shadow: none;
  opacity: 0.55;
  cursor: default;
}

.pick-card .r-face {
  width: 44px;
  height: 44px;
}

.pick-card .r-name {
  font-size: 11px;
  font-weight: 900;
  line-height: 1.15;
}

/* The venue's own picture, framed like the portrait disc beside it — same 3px ink line, same trick
   of showing the --chip colour underneath so a card with no image yet is the flat swatch this
   replaced rather than a white hole. 12/5 is the baked thumbnail's own aspect, given as a RATIO
   and not a height so the picture is shown whole at every card width the sheet can produce: the
   venue panel is ~126px wide beside two character columns and ~300px when a narrow window folds
   the three panels into one, and a fixed height would have cropped away half the mountains there.
   `object-fit: cover` only ever has work to do in the short-viewport layout below. */
.v-shot {
  display: block;
  align-self: stretch;
  aspect-ratio: 12 / 5;
  border: 3px solid var(--ink);
  border-radius: 8px;
  overflow: hidden;
  background: var(--chip);
}

/* The chosen venue's picture is ringed in its own floor colour, matching the card's border and
   shadow. It is not decoration: the pick-card's "chosen" signal is the --chip, and two of the three
   floor colours (sand, arena wood) are pale enough that a chip-coloured outline on a cream card
   reads as WASHED OUT beside its black-bordered neighbours — the opposite of chosen. Against the
   picture the same colour is unmissable. Before the pictures the card wore a 22px band of that
   colour, which is what used to carry this. */
.venue-grid .pick-card.is-on .v-shot {
  border-color: var(--chip);
}

.v-shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* a thumbnail that fails to load must not draw a broken-image glyph inside the frame */
  color: transparent;
}

#versus-pads {
  margin-top: 6px;
}

/* A short viewport is a FITTING problem, so it is answered with height rules (lessons.md: fitting is
   a height question, affordance is a pointer question).
   Two things happen here. The three panels are forced onto ONE row — auto-fit wraps them to two
   rows on a 640px-wide landscape phone, and two rows of panels cannot fit 130px of sheet body. And
   the row is then given the container's full height (`grid-template-rows: 100%`) so each grid can
   FLEX into whatever its panel has left, rather than being capped at a guessed pixel value: a fixed
   max-height either wastes space on a tall screen or overflows the panel on a short one, and it was
   the panel overflowing that pushed Start off the bottom. Same shape as #roster-grid inside its own
   sheet. The grids themselves still scroll — six characters will not fit 60px and are not meant to —
   but nothing above them does, which is the difference between "scroll this list" and "lost". */
@media (max-height: 520px) {
  #versus-overlay .opt-cols {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: 100%;
    gap: 8px;
  }

  #versus-overlay .panel {
    padding: 6px 7px;
    min-height: 0;
  }

  #versus-overlay .panel-head h3 {
    font-size: 11px;
  }

  /* by id, to match the specificity of the block above — a `.pick-grid` rule here would lose */
  #versus-p1-grid,
  #versus-p2-grid,
  #versus-venue-grid {
    flex: 1 1 auto;
    min-height: 0;
    grid-template-columns: repeat(auto-fit, minmax(56px, 1fr));
    gap: 4px;
  }

  .pick-card {
    padding: 3px 2px;
    border-width: 2px;
    border-radius: 8px;
    gap: 2px;
  }

  .pick-card .r-face {
    width: 22px;
    height: 22px;
  }

  .pick-card .r-name {
    font-size: 9px;
  }

  #versus-overlay .opt-row {
    flex: 0 0 auto;
    font-size: 10px;
    padding: 1px 0;
  }

  #versus-overlay .opt-row .ghost-btn {
    min-width: 0;
    font-size: 10px;
    padding: 2px 7px;
  }

  /* The venue grid takes its natural height instead of flexing: it is ONE row of three and looks
     broken cut in half, whereas the character grids are six and are meant to be scrolled. */
  #versus-venue-grid {
    flex: 0 0 auto;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  /* The one place the picture is cropped rather than fitted. A ~70px card at its 12/5 ratio wants
     29px of height, and this layout exists because the sheet has none to give — the pads list
     under it would pay for every one of them. 18px of centre band still reads as sea / lake /
     stands, which is what the card is for, and it is 8px more picture than the swatch it replaced
     had. Ratio released, or it would win over the height. */
  .venue-grid .v-shot {
    aspect-ratio: auto;
    height: 18px;
    border-width: 2px;
    border-radius: 5px;
  }

  #versus-pads {
    flex: 1 1 auto;
    min-height: 0;
    margin-top: 2px;
    overflow-y: auto;
    font-size: 9px;
  }
}

/* Landscape phone. The pad list is the first thing to go: squeezed into the few pixels left over it
   is a sliver that reads as a rendering fault, and the status pill and the footer hint are already
   saying the one thing it is there to say. */
@media (max-height: 430px) {
  #versus-pads {
    display: none;
  }
}

/* ---------- options (1a) ---------- */

.opt-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 14px;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.opt-cols .panel {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.opt-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 4px 0;
  font-size: 13px;
  font-weight: 900;
  color: #3a4753;
}

#opt-rows .opt-row {
  border-bottom: 2px dashed #e0dbcd;
}

#opt-rows .opt-row:last-child {
  border-bottom: 0;
}

/* the value on a controller row reads as a pill, not a button, until you reach for it */
.opt-row .ghost-btn {
  font-size: 12px;
  font-weight: 900;
  padding: 3px 12px;
  border-width: 2px;
  border-radius: 8px;
  background: #f1efe6;
  min-width: 128px;
  text-align: center;
}

.binding.armed {
  border-color: var(--accent);
  color: var(--accent);
  animation: armed-pulse 1s ease-in-out infinite;
}

@keyframes armed-pulse {
  50% {
    opacity: 0.55;
  }
}

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

/* The sound on/off switch: two labelled halves in one ink-outlined capsule, the live half filled.
   One <button>, so the pad's focus walker (main.js focusList) reaches it like any other control. */
.switch {
  font: inherit;
  display: flex;
  border: 3px solid var(--ink);
  border-radius: 20px;
  overflow: hidden;
  cursor: pointer;
  padding: 0;
  background: #f1efe6;
}

.switch span {
  padding: 3px 14px;
  font-size: 12px;
  font-weight: 900;
  color: var(--muted-2);
}

.switch .on {
  background: var(--you);
  color: #fff;
}

.switch.off .on {
  background: #f1efe6;
  color: var(--muted-2);
}

.switch.off .off {
  background: var(--accent);
  color: #fff;
}

/* The slider, drawn as the mockup's ink-outlined track with an orange fill and a white knob.
   --fill is set by main.js on every render and on input; without it the fill would freeze at its
   initial width while the knob moved, which reads as a broken control. */
.vol {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  height: 14px;
  min-width: 110px;
  border: 3px solid var(--ink);
  border-radius: 20px;
  background-color: #f1efe6;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-size: var(--fill, 50%) 100%;
  cursor: pointer;
}

.vol::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--ink);
}

.vol::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--ink);
  box-sizing: border-box;
}

#opt-audio-rows {
  display: flex;
  flex-direction: column;
}

/* The two option panels are grid siblings and stretch to the taller one (Controller, seven rows).
   Spreading the six sliders down that height is what the mockup shows, and it beats leaving a
   block of empty card under the last one — but only where the height is there to spend. On a
   landscape phone the columns already scroll, and spacing the rows out just means fewer of them
   are on screen at once. */
@media (min-height: 560px) {
  #opt-audio-rows {
    flex: 1;
    justify-content: space-around;
  }
}

#opt-audio-rows .opt-row span {
  width: 72px;
  flex: none;
  text-align: left;
}

/* every pad the browser exposes; the ▶ row is the one the game is reading */
.pad-list {
  list-style: none;
  padding: 0;
  margin: 2px 0 4px;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--muted-2);
  text-align: left;
}

.pad-list li.live {
  color: var(--you);
  font-weight: 900;
}

/* one focus ring for keyboard tabbing and the controller's programmatic .focus() alike */
.overlay button:focus-visible,
.overlay button:focus {
  outline: var(--ring-w) solid var(--accent);
  outline-offset: var(--ring-gap);
}

/* Room for that ring inside the lists that CLIP.

   The ring is painted --ring-reach outside the button, and these grids are scroll containers:
   `overflow-y: auto` computes overflow-x to auto as well, so they clip at their own padding box
   on all four sides. With no padding, a card sits flush against that box — the first row, the last
   row and both edge columns then lost the outer half of their ring, and a frame sliced down one
   side reads as a rendering fault rather than as focus. (The same edge ate the 3px chip-coloured
   shadow the CHOSEN pick-card wears, which is what made it visible with a mouse: press and hold on
   an edge card and the cut is right there under the cursor.)

   Padding alone would buy the room out of the cards' own width, which the versus panels — three
   columns inside one sheet, two cards across each — cannot spare. So the padding is handed
   straight back as a negative margin of the same size: the border box grows --ring-reach on every
   side into the gap/padding its parent already leaves empty, the content box lands exactly where
   it did before, and only the CLIP moves. Nothing on screen shifts by a pixel.

   scroll-padding covers the other half of the problem: once the list is long enough to scroll,
   focus arriving on a half-visible card makes the browser scroll it flush with the scrollport,
   which is flush with the cut. The same reach as an inset means it scrolls the ring into view too,
   not just the card. */
#roster-grid,
.pick-grid {
  padding: var(--ring-reach);
  margin: calc(-1 * var(--ring-reach));
  scroll-padding: var(--ring-reach);
}

/* ---------- rotate nudge ---------- */

/* Bottom, not top: at top:10px it sat exactly where #hud does and covered the score. It is only
   ever the TITLE/ROSTER screens' nudge now — a match that gets rotated pauses, and says why on the
   pause card, where the player is looking for a way out anyway. */
#rotate-nudge {
  display: none;
  position: absolute;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%) rotate(-2deg);
  z-index: 20;
  pointer-events: none;
  font-size: 12px;
  max-width: calc(100% - 24px);
  white-space: normal;
  text-align: center;
}

@media (orientation: portrait) and (pointer: coarse) {
  #rotate-nudge {
    display: block;
  }

  /* Upright is a "turn your phone" screen, not a play screen — a rotated match pauses. The score
     capsule is ~380 px wide and a phone held upright is ~330, so it hung off both edges with
     "PRETTY BOY" sliced in half, under a pause card that already says what to do. */
  #hud {
    display: none;
  }
}

/* ---------- small screens: a landscape phone is ~375 px tall ---------- */

@media (max-height: 520px) {
  .topbar {
    padding-top: 4px;
    padding-bottom: 4px;
  }

  .overlay {
    padding: 6px 14px 14px 6px;
  }

  .overlay-card {
    padding: 14px 16px;
  }

  .overlay-card.sheet {
    padding: 10px 14px;
    gap: 8px;
  }

  .panel {
    padding: 9px 12px;
  }

  /* A short stage cannot take the mockup's ~150 px vertical card twice over, so the card turns on
     its side: portrait beside the text instead of above it, which halves its height and still shows
     every card element. This is now the MIDDLE band only — 430-520 px, i.e. a smallish desktop
     window. A real landscape phone falls into the ≤430 block further down, which compacts the same
     sideways card again until all six fit at 300 px of viewport. */
  #roster-grid {
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 8px;
  }

  .roster-card {
    /* 7, not 8: at 812×375 the two rows came to 218 px inside a 216 px box and the grid grew a
       scrollbar for two pixels of rounding. Measured, not guessed. */
    padding: 7px 10px;
    display: grid;
    /* An EXPLICIT three-row template, which is what lets the portrait span them: renderRoster
       emits exactly name + tagline + pills on both the unlocked and the locked branch, so there
       are no implicit rows for `1 / -1` to fall short of. */
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto auto;
    column-gap: 9px;
    row-gap: 2px;
    text-align: left;
    align-items: center;
  }

  .r-face {
    --face: 44px;
    grid-row: 1 / -1;
    grid-column: 1;
  }

  .r-name,
  .r-tag,
  .r-pills {
    grid-column: 2;
  }

  .r-pills {
    justify-content: flex-start;
    padding-top: 2px;
  }

  /* the tab moves to the corner it can occupy without a portrait under it */
  .r-boss {
    left: auto;
    right: 8px;
    transform: rotate(-3deg);
  }

  .roster-card.boss .r-face {
    margin-top: 0;
  }

  .r-tag {
    font-size: 11px;
  }

  .step-n {
    flex-basis: 26px;
    height: 26px;
    font-size: 12px;
  }

  .step p,
  .panel ul {
    font-size: 12.5px;
  }

  .over-card {
    margin-top: 20px;
  }

  .over-face {
    --face: 58px;
    top: -20px;
  }

  /* The 2a stack has six things in it and a landscape phone gives it ~325 px. The wordmark
     already scales on vh; this takes the air out from between the rest of them. */
  .cover-stack {
    gap: 6px;
    padding-right: 14px;
  }

  .cover-badge {
    padding: 2px 8px;
  }

  .tagline.cover-card {
    padding: 7px 11px;
  }

  .big-btn {
    padding: 9px 26px;
  }

  #title-controls {
    font-size: 10px;
    padding: 3px 9px;
  }
}

@media (max-width: 700px) {
  .brand {
    display: none;
  }

  .cover-stack {
    max-width: min(70%, 460px);
  }
}

/* ---------- touch devices: what a thumb can and cannot do ----------
   Keyed on the POINTER, not on the size. These are affordance changes — a control that only a
   thumb needs, a column only a keyboard can use — and they must not fire on a small desktop
   window, which gets the fitting block below instead. Keeping that split is the one thing to
   preserve when editing either. */

@media (pointer: coarse) {
  /* The whole point of the exercise. The topbar is 47 px of a ~309 px landscape viewport, and the
     renderer turns viewport height directly into visible air over the court (0.9·h·16/w metres of
     it): with Chrome's URL bar this took the beach from 6.3 m of headroom down to 4.5 m, which is
     0.3 s less time to read a falling ball. Everything the bar carried is on the pause card. */
  body.playing .topbar {
    display: none;
  }

  #pause-fab {
    display: grid;
  }

  /* The columns a phone has no room for, and no use for: the keyboard and gamepad help, and the
     controller options. Folded away by default, one tap from being open. */
  .fold-btn {
    display: block;
  }

  /* `.opt-cols .desk-panel`, not a bare `.desk-panel`: `.opt-cols .panel { display: flex }` is two
     classes and would otherwise out-specify this, leaving the folded controller panel on screen —
     which it did, at 353 px inside a 96 px box. Media queries add no specificity. */
  .help-cols .desk-panel,
  .opt-cols .desk-panel {
    display: none;
  }

  #tab-controls.fold-open .desk-panel,
  #options-overlay.fold-open .desk-panel {
    display: flex;
  }

  /* main.js syncPadFold: a phone with no gamepad seen has nothing to do in the Controller panel, so
     it does not get the button either. A tablet keeps it, and so does a phone once a pad wakes up. */
  #options-overlay.pad-hidden #opt-pad-toggle {
    display: none;
  }

  /* With the desktop columns folded, the touch instructions get the whole width — which is what
     makes all three bullets (including the one about the HIT button) visible without scrolling. */
  #tab-controls .help-cols,
  #options-overlay .opt-cols {
    grid-template-columns: 1fr;
  }

  #tab-controls.fold-open .help-cols,
  #options-overlay.fold-open .opt-cols {
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  }
}

/* ---------- a landscape phone: ~300-380 px of viewport, all of it precious ----------
   Keyed on HEIGHT, so a short desktop window is fixed by the same rules. Every number here was
   measured against the fit gate (__vp.fit(), main.js): no element below the fold and no scroll
   container with hidden content, at a 300 px viewport. */

@media (max-height: 430px) {
  /* The score capsule was 68 px tall over the middle half of a 263 px stage — a quarter of the
     play area, in the band the ball flies through, and the thing the off-screen-ball tracker has
     to dodge. Same three-cell sticker, three quarters of the size. */
  #hud {
    top: 4px;
    gap: 5px;
  }

  .score-side {
    padding: 4px 10px;
    gap: 6px;
    font-size: 13px;
  }

  #score-line {
    border-width: 2px;
    border-radius: 12px;
    box-shadow: 0 3px 0 var(--ink);
  }

  .score-num {
    padding: 1px 8px;
    border-radius: 5px;
    font-size: 15px;
  }

  .score-touches {
    padding: 3px 10px;
    gap: 3px;
    border-left-width: 2px;
    border-right-width: 2px;
  }

  .touches-label {
    font-size: 8px;
    letter-spacing: 0.5px;
  }

  /* Match over. `margin-top` was the bug the user hit: it paid for the portrait disc hanging off
     the card's top-left corner, and it pushed the Rematch button 30 px below the fold of a 310 px
     viewport with no way for a thumb to scroll to it. On a short stage the disc comes INSIDE the
     card, beside the headline, and the margin goes away with it. */
  .over-card {
    margin-top: 0;
    padding-top: 10px;
  }

  /* `relative` with the offsets released, NOT `static`: .r-head inside is absolutely positioned
     against this disc, and letting it resolve against the card instead puts the portrait somewhere
     else entirely at eight times the size. */
  .over-face {
    position: relative;
    top: auto;
    left: auto;
    float: left;
    --face: 44px;
    margin: 0 10px 4px 0;
    box-shadow: 2px 2px 0 var(--ink);
  }

  .over-score {
    margin-top: 6px;
    gap: 16px;
  }

  .over-col-num {
    font-size: clamp(34px, 7vw, 48px);
  }

  .over-col-label {
    font-size: 10px;
  }

  .over-dot {
    font-size: 20px;
  }

  .over-line {
    margin-top: 8px;
    padding: 7px 12px;
    font-size: 13px;
  }

  .btn-row {
    margin-top: 9px;
    gap: 8px;
  }

  /* The roster's whole ladder has to be on screen: at 262 px of stage the previous card was 140 px
     tall, so rows 2 and 3 — Glasses, Neck and the BOSS — sat below a fold no thumb could scroll
     past, which is the ladder being unfinishable on the platform the game is aimed at. Three fixed
     columns of a compact card gets all six visible in ~130 px. The tagline stays (it is how you
     choose) but is clamped to one line. */
  #roster-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 7px;
  }

  .roster-card {
    padding: 6px 8px;
    column-gap: 8px;
    row-gap: 0;
  }

  .r-face {
    --face: 38px;
  }

  .r-name {
    font-size: 13px;
  }

  .r-tag {
    font-size: 10.5px;
    line-height: 1.25;
    /* one line, ellipsised: six opponents visible beats three opponents fully described */
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .r-pills {
    padding-top: 1px;
  }

  .r-pills .pill {
    font-size: 9.5px;
    padding: 1px 7px;
  }

  .r-boss {
    top: -10px;
    font-size: 9.5px;
    padding: 1px 8px;
  }

  /* the sheets' own furniture, so the body they wrap has the room the rules above assume */
  .sheet-head h2,
  .overlay-card h2 {
    font-size: 17px;
  }

  .overlay-card.sheet {
    gap: 6px;
    padding: 8px 12px;
  }

  /* Deliberately NOT .fold-btn: it stands in a footer beside "Reset to defaults" and "Got it", and
     a fold button shrunk on its own read as a mistake rather than as a smaller control. It keeps
     .ghost-btn's size and the row still fits. */
  .tab {
    font-size: 11.5px;
    padding: 4px 10px;
    border-width: 2px;
  }

  .sheet-foot {
    padding-top: 0;
  }

  .sheet-foot .fine {
    display: none; /* the footnote is the first thing to go: the button beside it is the way out */
  }

  .sheet-foot .big-btn {
    padding: 7px 20px;
    font-size: 15px;
  }

  .panel ul {
    font-size: 11.5px;
    line-height: 1.32;
  }

  .panel li + li {
    margin-top: 2px;
  }

  .panel h3 {
    font-size: 12.5px;
  }

  .panel,
  .opt-cols .panel {
    padding: 7px 10px;
  }

  .panel-head {
    margin-bottom: 2px;
  }

  .switch span {
    padding: 2px 11px;
    font-size: 11px;
  }

  .tab-panel {
    gap: 7px;
  }

  .help-cols {
    gap: 8px;
  }

  .step + .step {
    margin-top: 6px;
  }

  .step {
    gap: 9px;
  }

  .step-n {
    flex-basis: 22px;
    height: 22px;
    font-size: 11px;
    border-width: 2px;
  }

  .step p {
    font-size: 11.5px;
    line-height: 1.32;
  }

  /* the last 6 px the rules tab was over by, measured at a 300 px viewport */
  .rps {
    padding: 4px 10px;
    gap: 7px;
    border-width: 2px;
    border-radius: 12px;
  }

  .rps-title,
  .rps-chain {
    font-size: 11px;
  }

  .rps-chain b {
    padding: 2px 6px;
  }

  /* The 2a cover stack has six things in it and ~253 px to put them in, which is 11 px less than
     they wanted — the controls hint (the one line that tells a phone player what their thumbs do)
     was the thing hanging off the bottom. Every value below is a shave, not a removal. */
  .cover-stack {
    gap: 4px;
  }

  #title-wordmark {
    font-size: clamp(24px, min(6.4vw, 10.5vh), 76px);
    margin-top: 5px;
  }

  /* An over-title is a SEVENTH thing in the stack the comment above is about, and measured on the
     300 px viewport that harness gates against it costs 11.9 px — which came straight off the
     bottom, i.e. off that same controls hint, which then hung 2 px outside the screen.
     So the edition that has one pays for it, and pays out of air rather than out of the wordmark:
     5 px of the lockup's top margin (the over-title is now the top of the lockup) and 2 px from
     each of the four gaps. That is 13 px against 11.9 px, measured, and the public stack — where
     the span is empty — matches `:empty` instead and is untouched. */
  .cover-stack:has(#title-overline:not(:empty)) {
    gap: 2px;
  }

  #title-wordmark:has(#title-overline:not(:empty)) {
    margin-top: 0;
  }

  .tagline.cover-card {
    font-size: 11.5px;
    padding: 5px 10px;
  }

  .cover-row .ghost-btn {
    font-size: 11.5px;
    padding: 4px 10px;
  }

  .big-btn {
    padding: 7px 22px;
    font-size: 16px;
  }

  /* six sliders in two columns of three: as one column they were 369 px of content in 113 px, so
     the music volume was unreachable on a phone */
  #opt-audio-rows {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 12px;
  }

  #opt-audio-rows .opt-row {
    padding: 1px 0;
    font-size: 11.5px;
  }

  #opt-audio-rows .opt-row span {
    width: 48px;
  }

  .vol {
    height: 11px;
    min-width: 60px;
    border-width: 2px;
  }
}
