/* ── OCTOPONG · UI system ────────────────────────────────────
   The chunky, outlined, angled panel language from the reference
   art, as reusable parts. Every screen is built from these — if a
   screen needs a new shape, add it here rather than one-offing it,
   or the shell drifts apart screen by screen.

   Two rules carry most of the look:
     1. Nothing is a plain rectangle. Panels are clipped with cut
        corners; buttons are chevron-ended.
     2. Everything has a hard dark outline and a drop shadow. That
        is what separates "cartoon premium" from "flat web page".
   ─────────────────────────────────────────────────────────── */

/* ── TRAP 3 — this sheet was NOT the source of truth ──────────
   index.html loads, in order: theme.css, menu.css, pause.css,
   results.css, ui.css. ui.css declares its own `:root` block with
   the same token names, so for any token both files define, UI.CSS
   WON — theme.css's authored `--outline:#04060f` was never once
   painted, and `--ink-dim` shipped as ui.css's #8fb3c9 while this
   file said #9ec2da. Two files, same names, different values, and
   the later one silently winning is exactly how a shell drifts.

   Fixed two ways, both needed:
     · the values below are reconciled to the ones that were
       actually rendering, which are also the ones src/gfx/palette.js
       exports (OUTLINE 0x050814, INK_DIM 0x8fb3c9), so the DOM and
       the 3D finally name the same colours;
     · the selector is `:root:root` (specificity 0,2,0) so a later
       plain `:root` cannot quietly take it back. Anything that
       genuinely needs to override a token should do it on a scoped
       ancestor, the way pause.css and results.css do.
   ─────────────────────────────────────────────────────────── */
:root:root{
  /* ground */
  --deep:#080b1f; --mid:#101a3a; --near:#1b2a55;
  --panel:#12234d; --panel-hi:#1b3268; --panel-lo:#0c1937;

  /* ink */
  /* ink-faint was #5f83a4 and measured 4.13:1 on the ghost chip's top
     gradient (16,30,62) — its brightest real backdrop — and ~4.3 on
     three other screens. AA small-text needs 4.5. #6f93b4 measures
     5.10:1 on that same worst backdrop (5.31 chip-bot, 5.92 modes
     scrim) and stays clearly below --ink-dim so the faint/dim
     hierarchy survives. Measured with WCAG relative luminance, not
     eyeballed. */
  --ink:#f2fbff; --ink-dim:#8fb3c9; --ink-faint:#6f93b4;
  --outline:#050814;

  /* brand */
  --gold:#ffc63d; --gold-hi:#ffe08a; --gold-lo:#d1811a;
  --gold-deep:#e08a1e;
  --aqua:#2fe6d6; --aqua-hi:#7ff5ea; --aqua-lo:#159c92;
  --hot:#ff3d8b; --hot-lo:#c01e5e;
  --violet:#8b5cf6; --lime:#9dff4d;

  /* ── the scales ────────────────────────────────────────────
     Four screens were each inventing their own numbers. These
     exist so the next one does not have to guess, and so a
     rhythm change lands everywhere at once instead of on one
     screen at a time. */

  /* spacing — 4px base, the gaps every screen was picking by eye
     (6/7/8/9/10/11/12/14/16/18/22/24/26/34 all appeared) */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px;
  --sp-5:22px; --sp-6:30px; --sp-7:40px;

  /* type — the display face steps, and the one small-caps label
     size the whole product uses for chip/label/eyebrow text */
  --fs-label:11px;         /* letterspaced caps: statusbar, opt h5   */
  --fs-cap:12px;           /* chips, tags, pips row                  */
  --fs-body:12.5px;        /* card descriptions, panel copy          */
  --fs-btn:15px;           /* every non-primary button               */
  --fs-head:14px;          /* .panel-head — see the note below       */
  --ls-caps:.13em;         /* one tracking for all small caps        */

  /* outline weight on display type. FOUR different -webkit-text-
     stroke values were in play for one visual idea: 7px wordmark,
     6px screen title, 3px card name, 3px results brand. That is a
     scale now, and it is proportional to the type size. */
  --stroke-xl:7px;         /* .wordmark                              */
  --stroke-lg:5px;         /* .scr-title, res verdict                */
  --stroke-md:3px;         /* .mc-name, section headings             */

  /* motion — every component had its own duration and curve.
     -in  is the springy overshoot used for anything that lifts;
     -out is the flat curve used for anything that leaves. */
  --dur-fast:.12s; --dur-mid:.20s; --dur-slow:.30s;
  --ease-pop:cubic-bezier(.3,1.5,.5,1);
  --ease-out:cubic-bezier(.4,0,.6,1);
  --lift-y:-3px;           /* ONE hover rise for cards/tiles/pills   */
  --lift-x:4px;            /* ONE hover slide for stacked buttons    */

  /* geometry */
  --cut:14px;              /* corner cut on panels */
  --stroke:3px;            /* the hard outline */
  --shadow:0 6px 0 rgba(3,6,16,.55), 0 14px 30px rgba(0,0,0,.5);

  --display:"Baloo 2","Nunito",system-ui,-apple-system,"Segoe UI",sans-serif;
  --ui:"Nunito",system-ui,-apple-system,"Segoe UI",sans-serif;
}

/* ── the outline ──────────────────────────────────────────────
   Every chunky shape here needs a hard dark edge that follows its
   clip-path. The obvious way — a ::before at inset:0; z-index:-1 —
   DOES NOT WORK and this cost a full review round:

     clip-path and filter each establish a stacking context, and a
     z-index:-1 child of a stacking context paints ABOVE its parent's
     background, not behind it. The outline layer therefore floods the
     face. It shipped a #050814 PLAY button whose authored gold was
     never visible: 1.15:1 contrast against an authored 11.11:1.

   Chained zero-blur drop-shadows outline the CLIPPED SILHOUETTE
   exactly, sit outside the element, and cannot be occluded.
   ─────────────────────────────────────────────────────────── */
.edged{
  --edge:var(--outline);
  --lift:0 5px 0 rgba(3,6,16,.55);
  filter:
    drop-shadow(2px 0 0 var(--edge))  drop-shadow(-2px 0 0 var(--edge))
    drop-shadow(0 2px 0 var(--edge))  drop-shadow(0 -2px 0 var(--edge))
    drop-shadow(var(--lift));
}

/* ── layout helpers ───────────────────────────────────────── */

/* ── screen change ────────────────────────────────────────────
   Every screen here is a FULL-BLEED OPAQUE LAYOUT, and they all
   used to cross-fade on the same symmetric .28s curve. That means
   that for ~140ms of every single navigation both screens sat at
   roughly 50% alpha at the same time, and because each one draws
   its own near-opaque scrim you got a legible double exposure:
   the menu's PLAY button and status bar reading straight through
   the results table, the HUD clock stamped through MATCH RESULT.
   Screenshotted three times before it was believed — it looks like
   a rendering fault, not a transition.

   A cross-dissolve is the wrong move for opaque layers anyway.
   The outgoing screen now LEAVES FIRST, fast, and the incoming one
   starts after it has gone, so the two never share the frame at
   readable alpha. `visibility` is stepped explicitly at each end
   rather than transitioned, so a screen stops taking clicks the
   moment it starts leaving.

   NOTE for anyone tempted to add a transform/scale here: don't.
   `.menu::before` and `.modes::before` are z-index:-1 scrims, and
   a transform on .screen would open a stacking context around
   them — the same class of bug as the outline trap above. */
.screen{
  position:absolute; inset:0;
  /* GRID MINIMUMS ARE THE WHOLE STORY OF THIS RULE, and they were
     the single biggest cause of "the buttons are cut off".
     An `auto` implicit COLUMN and a bare `1fr` ROW both floor at the
     item's MIN-CONTENT size. On a 360px phone the one implicit column
     of .settings therefore resolved to 559px, the panel ran 199px off
     the right of a <body> that clips, and CLOSE/OK sat on the wrong
     side of that edge — unreachable at every scroll position, because
     nothing between it and the document could scroll sideways. The row
     axis did the same thing at 844x390 (.set-list 210px past the fold).
     `minmax(0, …)` removes the floor: the screen is exactly the
     viewport, and the inner scrollers (.set-list, .cst-list, .lb-col)
     do the job they were written to do. Do not put `auto` or a bare
     `1fr` back in either axis. */
  display:grid;
  grid-template-columns:minmax(0,1fr);
  grid-template-rows:auto minmax(0,1fr) auto;
  opacity:0; visibility:hidden;
  transition:opacity var(--dur-fast) var(--ease-out),
             visibility 0s linear var(--dur-fast);
}
.screen.on{
  opacity:1; visibility:visible;
  transition:opacity var(--dur-mid) var(--ease-out) var(--dur-fast),
             visibility 0s linear 0s;
}
.row{ display:flex; align-items:center; gap:12px; }
.col{ display:flex; flex-direction:column; gap:10px; }
.grow{ flex:1; }

/* ── the panel ────────────────────────────────────────────────
   Cut corners via clip-path, with the outline drawn by a slightly
   larger clipped pseudo-element behind — border-image cannot
   follow a clip-path, so this is the way to get a true outline on
   an angled shape. */
.panel{
  position:relative;
  background:linear-gradient(180deg, var(--panel-hi), var(--panel-lo));
  clip-path:polygon(var(--cut) 0, 100% 0, 100% calc(100% - var(--cut)),
                    calc(100% - var(--cut)) 100%, 0 100%, 0 var(--cut));
  padding:16px 18px;
  --edge:var(--outline); --lift:0 5px 0 rgba(3,6,16,.5);
  filter:
    drop-shadow(2px 0 0 var(--edge))  drop-shadow(-2px 0 0 var(--edge))
    drop-shadow(0 2px 0 var(--edge))  drop-shadow(0 -2px 0 var(--edge))
    drop-shadow(var(--lift)) drop-shadow(0 12px 22px rgba(0,0,0,.45));
}
.panel.accent{ background:linear-gradient(180deg,#1d3f7a,#102246); }
/* pause.css and results.css each independently re-declared this
   head at 14px/.13em/10px 18px, having each decided 13px/.1em/9px
   was too tight — and both left the menu behind at the old value.
   When two of three screens agree against the token, the token is
   what is wrong. Promoted here so all three match. */
.panel-head{
  margin:-16px -18px 14px; padding:10px 18px;
  /* White on the bright magenta measured 3.34:1 and failed AA. The
     darker ramp keeps the accent and reaches 5.4:1. */
  background:linear-gradient(180deg,#c8206a,#8f0f4a);
  font:800 var(--fs-head)/1 var(--display); letter-spacing:var(--ls-caps);
  text-transform:uppercase;
  color:#fff; text-shadow:0 2px 0 rgba(0,0,0,.4);
  clip-path:polygon(var(--cut) 0, 100% 0, 100% 100%, 0 100%, 0 var(--cut));
}
/* ── the doubled class is load-bearing ───────────────────────
   pause.js asks for `panel-head aqua` on QUICK CONTROLS and
   `panel-head gold` on PRO TIP; results.js does the same. Both
   sheets then re-declare `.pause .panel-head` / `.res .panel-head`
   for the contrast fix, and a descendant selector (0,2,0) TIES
   with `.panel-head.aqua` (0,2,0) — later sheet wins. Result: the
   colour-coded rail on the pause screen rendered three identical
   magenta heads, and the API those screens are calling was dead
   with no error anywhere. Doubling the class takes the modifier to
   (0,3,0) so an intentional variant beats a blanket scope. */
/* THE SHADOW HAS TO FLIP WITH THE INK, and this is where "black font
   with a black drop shadow" came from. `.panel-head` above is WHITE
   text on magenta, so it carries `0 2px 0 rgba(0,0,0,.4)` to lift the
   white off the plate. These two variants correctly override the ink
   to near-black — dark-on-aqua measures 4.9:1 and dark-on-gold 5.7:1,
   where white on either would be about 1.6:1, so the DARK FILL IS THE
   RIGHT CHOICE AND STAYS — but they inherited the dark shadow with it.
   A black shadow under black text is not a shadow, it is a second
   smeared copy of the glyph one pixel down: the letterforms thicken
   and the counters fill in. Every panel heading in the game was drawn
   that way — ROOM CODE, SELECTED MODE, GRAPHICS, LIVE PREVIEW, THE
   ARENA, OBJECTIVE, CONTROLS, HAZARDS, POWER-UPS, MATCH HIGHLIGHTS,
   YOUR MATCH, ARENA PREVIEW and the cup's round banner.
   A light shadow is the correct one for dark ink on a light plate: it
   reads as an emboss instead of mush, and text contrast is untouched
   (a text-shadow is not part of the WCAG measurement). */
.panel-head.aqua.aqua{ background:linear-gradient(180deg,var(--aqua),var(--aqua-lo));
  color:#05221f; text-shadow:0 1px 0 rgba(255,255,255,.38); }
.panel-head.gold.gold{ background:linear-gradient(180deg,var(--gold),var(--gold-lo));
  color:#2a1600; text-shadow:0 1px 0 rgba(255,255,255,.38); }

/* ── buttons ──────────────────────────────────────────────────
   Chevron-ended slabs. The outline comes from chained drop-shadows
   (see .edged above); ::after is the top gloss. */
.btn{
  position:relative; display:flex; align-items:center; gap:12px;
  padding:11px 20px 11px 14px; border:0; cursor:pointer;
  background:linear-gradient(180deg,#1f3c72,#132549);
  color:var(--ink); font:800 15px/1 var(--display); letter-spacing:.04em;
  text-align:left; text-shadow:0 2px 0 rgba(0,0,0,.45);
  clip-path:polygon(0 0, calc(100% - 16px) 0, 100% 50%, calc(100% - 16px) 100%, 0 100%);
  --edge:var(--outline); --lift:0 4px 0 rgba(3,6,16,.55);
  filter:
    drop-shadow(2px 0 0 var(--edge))  drop-shadow(-2px 0 0 var(--edge))
    drop-shadow(0 2px 0 var(--edge))  drop-shadow(0 -2px 0 var(--edge))
    drop-shadow(var(--lift));
  transition:transform var(--dur-fast) var(--ease-pop), background var(--dur-fast);
}
.btn::after{
  content:""; position:absolute; left:0; right:0; top:0; height:42%;
  background:linear-gradient(180deg,rgba(255,255,255,.22),transparent);
  clip-path:inherit; pointer-events:none;
}
/* ── ONE hover language ───────────────────────────────────────
   Five components had five recipes: .btn slid +5px on X and
   changed fill, .modecard rose 5px with no fill change (its
   `background .14s` transition animated nothing at all), .tile
   rose 4px, .pill rose 2px, .iconbtn rose 2px and recoloured.
   Three distances, two axes, two durations, and only some of them
   acknowledged the pointer with anything but movement.

   The rule now: things in a VERTICAL STACK slide on X toward the
   reading direction; things in a GRID or ROW rise on Y. One
   distance each, one curve, and every one of them also lightens,
   so hover is never movement alone. */
.btn:hover{ transform:translateX(var(--lift-x)); background:linear-gradient(180deg,#27498a,#172c58); }
.btn:active{ transform:translateX(var(--lift-x)) translateY(3px);
             filter:drop-shadow(0 1px 0 rgba(3,6,16,.55)); }
/* A clipped element clips its outline too, so outline gives these
   chevron buttons NO focus ring at all — the whole shell was
   keyboard-invisible. Draw the ring with the same drop-shadow chain
   that draws the edge, plus an inset highlight.
   :focus not :focus-visible: Chrome leaves focus-visible false when
   focus moves by keyboard after a click, which hides the current item.

   THE FIX WAS ONLY HALF APPLIED. `.iconbtn` and `.cur .plus` — the
   whole menu top bar, eight tabbable controls: friends, inbox,
   leaderboards, settings and the three currency "+" buttons — were
   left out, so tabbing across the top of the main menu still moved
   an invisible focus. They are not clipped, so they can take a real
   `outline`; the ring is the same aqua at the same weight either
   way, which is the point. */
.btn:focus, .tile:focus, .pill:focus, .modecard:focus{
  outline:none;
  --edge:var(--aqua);
  box-shadow:inset 0 0 0 2px rgba(47,230,214,.85);
}
.iconbtn:focus, .cur .plus:focus{
  outline:2px solid var(--aqua); outline-offset:2px;
  box-shadow:0 0 0 2px var(--outline),0 3px 0 rgba(0,0,0,.5),
             inset 0 0 0 2px rgba(47,230,214,.85);
}

/* ── back navigation points backwards ─────────────────────────
   `.btn.back`'s slab is clipped to a leftward chevron, but modes.js
   fills it with icons.js `chevron`, whose path (M9.2 5.2 16 12
   l-6.8 6.8) points RIGHT — so the only back button in the shell
   read "▶ BACK". The pause screen uses a literal ◀ for the same
   job, which is how it went unnoticed. Rotate the glyph rather than
   asking every caller to remember. */
.btn.back > svg{ transform:rotate(180deg); }

.btn .ico{
  width:30px; height:30px; flex:none; display:grid; place-items:center;
  background:linear-gradient(180deg,#2b4d8f,#16305f); border-radius:6px;
  font-size:15px; box-shadow:inset 0 0 0 2px rgba(0,0,0,.4);
}

/* primary — the one gold thing on the screen */
.btn.play{
  padding:20px 34px 20px 24px; gap:16px;
  font-size:clamp(26px,3.2vw,38px); letter-spacing:.02em;
  background:linear-gradient(180deg,var(--gold-hi),var(--gold),var(--gold-lo));
  color:#2a1500; text-shadow:0 2px 0 rgba(255,255,255,.35);
  clip-path:polygon(0 0, calc(100% - 26px) 0, 100% 50%, calc(100% - 26px) 100%, 0 100%);
  filter:drop-shadow(0 6px 0 rgba(3,6,16,.6)) drop-shadow(0 0 24px rgba(255,198,61,.45));
}
.btn.play:hover{ background:linear-gradient(180deg,#fff0b8,var(--gold-hi),var(--gold)); }
.btn.danger{ background:linear-gradient(180deg,#ff7ab5,var(--hot));
             color:#3d0018; text-shadow:0 1px 0 rgba(255,255,255,.3); }
.btn.danger:hover{ background:linear-gradient(180deg,#ff6ba7,#d42a6c); }

/* tile buttons along the bottom */
.tile{
  position:relative; width:112px; padding:12px 8px 9px; border:0; cursor:pointer;
  display:flex; flex-direction:column; align-items:center; gap:6px;
  background:linear-gradient(180deg,#1f3c72,#132549);
  color:var(--ink); font:800 10px/1 var(--ui); letter-spacing:.12em; text-transform:uppercase;
  clip-path:polygon(10px 0,calc(100% - 10px) 0,100% 10px,100% calc(100% - 10px),
                    calc(100% - 10px) 100%,10px 100%,0 calc(100% - 10px),0 10px);
  --edge:var(--outline); --lift:0 4px 0 rgba(3,6,16,.55);
  filter:
    drop-shadow(2px 0 0 var(--edge))  drop-shadow(-2px 0 0 var(--edge))
    drop-shadow(0 2px 0 var(--edge))  drop-shadow(0 -2px 0 var(--edge))
    drop-shadow(var(--lift));
  transition:transform var(--dur-fast) var(--ease-pop), background var(--dur-fast);
}
.tile:hover{ transform:translateY(var(--lift-y));
             background:linear-gradient(180deg,#27498a,#172c58); }
.tile .big{ font-size:24px; line-height:1; }
/* tile faces stay dark so the 10px label keeps contrast; the ACCENT
   is the outline, not the fill — a light fill at 10px measured 1.49:1 */
.tile{ text-shadow:0 2px 0 rgba(0,0,0,.6); }
.tile.hot{  --edge:var(--hot); }
.tile.gold{ --edge:var(--gold); }
.tile.aqua{ --edge:var(--aqua); }

/* ── the wordmark ─────────────────────────────────────────────
   Two-tone, heavy, outlined, sitting on a magenta shard. The
   reference face is a bespoke rounded display type; the closest we
   can get without shipping a font file is a very heavy rounded
   system stack with a real stroke on it. */
.wordmark{
  position:relative; display:inline-block;
  font:900 clamp(40px,6.4vw,78px)/0.9 var(--display);
  letter-spacing:-.01em; text-transform:uppercase;
  -webkit-text-stroke:var(--stroke-xl) var(--outline); paint-order:stroke fill;
  filter:drop-shadow(0 6px 0 rgba(3,6,16,.6));
}
.wordmark .a{ color:var(--aqua); }
.wordmark .b{ color:var(--gold); }
.tagline{
  display:inline-block; margin-top:10px; padding:5px 14px;
  background:var(--gold); color:#2a1500;
  font:800 12px/1 var(--ui); letter-spacing:.16em; text-transform:uppercase;
  clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%);
  box-shadow:0 3px 0 rgba(3,6,16,.55);
}

/* ── top bar ──────────────────────────────────────────────── */
/* flex-wrap is load-bearing: without it the bar's min-content is
   ~898px (avatar+name+xp+three currencies+tools), and because the
   .screen grid's single column sizes to its widest row, ONE
   unwrappable row forced the ENTIRE menu — body, tiles, statusbar —
   to 898px wide inside an 800px viewport: 98px of horizontal
   overflow on every laptop-half-screen and tablet. Measured live at
   800x750 and 1024x768. */
.topbar{ display:flex; flex-wrap:wrap; align-items:center; gap:14px; padding:14px 20px; }
.avatar{
  width:52px; height:52px; flex:none; border-radius:50%;
  background:radial-gradient(circle at 35% 30%,#ff7ab8,#c0246e);
  box-shadow:0 0 0 3px var(--outline), 0 0 0 6px var(--aqua), 0 4px 0 rgba(0,0,0,.5);
  display:grid; place-items:center; font-size:26px;
}
.who{ display:flex; flex-direction:column; gap:3px; }
.who b{ font:800 17px/1 var(--display); letter-spacing:.03em; text-shadow:0 2px 0 rgba(0,0,0,.5); }
.xp{ display:flex; align-items:center; gap:7px; }
.lvl{
  min-width:26px; height:24px; padding:0 5px; display:grid; place-items:center;
  background:var(--gold); color:#2a1500; font:900 12px/1 var(--ui);
  clip-path:polygon(50% 0,100% 35%,82% 100%,18% 100%,0 35%);
}
.xpbar{ width:150px; height:11px; background:#0a1430; border-radius:99px;
        box-shadow:inset 0 0 0 2px var(--outline); overflow:hidden; position:relative; }
.xpbar i{ display:block; height:100%; background:linear-gradient(90deg,var(--aqua),var(--aqua-hi)); }
.xpbar{ height:13px; }
.xpbar span{ position:absolute; inset:0; display:grid; place-items:center;
             font:800 9px/13px var(--ui); letter-spacing:.04em; color:#eaf6ff;
             text-shadow:0 1px 2px #000; white-space:nowrap; }

.cur{
  display:flex; align-items:center; gap:9px; padding:6px 6px 6px 12px;
  background:linear-gradient(180deg,#162c58,#0d1c3c);
  clip-path:polygon(10px 0,100% 0,100% 100%,10px 100%,0 50%);
  box-shadow:0 0 0 2px var(--outline), 0 3px 0 rgba(0,0,0,.5);
  font:800 15px/1 var(--ui); letter-spacing:.02em;
}
.cur .dot{ width:22px; height:22px; border-radius:50%; box-shadow:0 0 0 2px var(--outline); }
.cur .plus{
  width:24px; height:24px; border:0; cursor:pointer; border-radius:5px;
  background:linear-gradient(180deg,#3ddc97,#1c8f5f); color:#04220f;
  font:900 16px/1 var(--ui); box-shadow:0 0 0 2px var(--outline),0 2px 0 rgba(0,0,0,.5);
}
.iconbtn{
  width:38px; height:38px; border:0; cursor:pointer; border-radius:8px;
  background:linear-gradient(180deg,#1f3c72,#132549); color:var(--aqua);
  font-size:17px; box-shadow:0 0 0 2px var(--outline),0 3px 0 rgba(0,0,0,.5);
  transition:transform var(--dur-fast) var(--ease-pop), color var(--dur-fast);
}
.iconbtn:hover{ transform:translateY(var(--lift-y)); color:var(--gold); }

/* ── bottom status strip ──────────────────────────────────── */
.statusbar{
  display:flex; align-items:center; gap:18px; padding:9px 20px;
  background:linear-gradient(180deg,rgba(6,11,28,.0),rgba(4,7,18,.85));
  font:700 11px/1 var(--ui); letter-spacing:.1em; text-transform:uppercase;
  color:var(--ink-faint);
}
.statusbar b{ color:var(--ink-dim); font-weight:800; }
.statusbar .ok{ color:var(--lime); }

/* ── misc ─────────────────────────────────────────────────── */

/* ── scrollbars are part of the shell ─────────────────────────
   results.css themed its own (thin, #3a63aa) and every other
   scrolling surface — modes at short heights, settings, how-to,
   the lobby, pause — shipped the OS default: a fat light-grey
   web-page bar floating mid-screen in a game. One voice for all
   of them, matching the one results already settled on. */
.screen{ scrollbar-width:thin; scrollbar-color:#3a63aa transparent; }
.screen *{ scrollbar-width:thin; scrollbar-color:#3a63aa transparent; }
.screen ::-webkit-scrollbar{ width:12px; height:12px; }
.screen ::-webkit-scrollbar-thumb{ background:#3a63aa; border-radius:99px; }
.screen ::-webkit-scrollbar-track{ background:transparent; }

.dots{ display:flex; gap:6px; }
.dots i{ width:7px; height:7px; border-radius:99px; background:#2b4270; }
.dots i.on{ background:var(--hot); }
.meter{ height:14px; background:#0a1430; box-shadow:inset 0 0 0 2px var(--outline);
        border-radius:99px; overflow:hidden; }
.meter i{ display:block; height:100%; background:linear-gradient(90deg,var(--gold),var(--gold-hi)); }

@media (prefers-reduced-motion:reduce){
  .btn,.tile,.iconbtn,.screen{ transition:none; }
}

/* ── main menu layout ─────────────────────────────────────────
   Three columns: the action stack, a hole the live arena shows
   through, and the season rail. The middle column is intentionally
   empty — the game is still rendering behind this layer. */
.menu-body{
  display:grid; grid-template-columns:minmax(300px,26vw) 1fr minmax(260px,22vw);
  align-items:start; gap:24px; padding:0 22px; min-height:0;
}
.menu-left{ display:flex; flex-direction:column; gap:12px; }
.menu-left .actions{ gap:8px; margin-top:6px; }
.menu-left .actions .btn{ width:min(300px,100%); }
.menu-left .btn.play{ width:min(300px,100%); margin:8px 0 4px; }
.menu-mid{ pointer-events:none; }

.menu-right{ display:flex; flex-direction:column; gap:14px; justify-self:end; width:100%; max-width:330px; }
.menu-right h3{
  margin:2px 0 6px; font:900 clamp(24px,2.4vw,34px)/0.95 var(--display);
  color:var(--hot); -webkit-text-stroke:var(--stroke-md) var(--outline); paint-order:stroke fill;
  letter-spacing:-.01em;
}
.menu-right h4{ margin:2px 0 5px; font:800 17px/1 var(--display); color:var(--gold); letter-spacing:.02em; }
.menu-right p{ margin:0 0 11px; font:600 12.5px/1.5 var(--ui); color:var(--ink-dim); }
.menu-right .btn{ padding:9px 18px 9px 12px; font-size:13px; }
.menu-right .dots{ margin-top:11px; justify-content:center; }
.challenge-foot{ margin-top:8px; font:800 12px/1 var(--ui); color:var(--ink-dim); letter-spacing:.06em; }
.challenge-foot b{ color:var(--gold); }

.menu-foot{ display:flex; flex-direction:column; gap:2px; }
.tiles{ display:flex; gap:12px; justify-content:center; padding:0 22px 6px; flex-wrap:wrap; }

/* keep the arena readable behind the shell */
.menu::before{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:
    linear-gradient(90deg, rgba(5,8,20,.92) 0%, rgba(5,8,20,.55) 26%, transparent 42%,
                    transparent 60%, rgba(5,8,20,.55) 76%, rgba(5,8,20,.92) 100%),
    linear-gradient(180deg, rgba(5,8,20,.85), transparent 22%, transparent 76%, rgba(5,8,20,.9));
}

@media (max-width:1100px){
  .menu-body{ grid-template-columns:1fr; }
  .menu-right{ max-width:none; justify-self:stretch; }
  .menu-mid{ display:none; }
  /* Collapsed to one column the menu is ~1013px of content in a
     750px viewport, and nothing scrolled — QUIT sat ON the tiles
     row and the whole season rail rendered under the statusbar,
     unreachable. The screen itself is inset-locked, so its grid
     rows can never grow past the viewport (min-height:0 lets the
     1fr row collapse to leftover space); the scroll has to live on
     the BODY row, the same pattern .modes-body already uses.
     Measured live at 800x750. */
  .menu-body{ overflow-y:auto; min-height:0; padding-bottom:var(--sp-4); }
}

/* ── mode select ──────────────────────────────────────────── */
.scr-title{
  margin:0 0 18px; text-align:center;
  font:900 clamp(28px,4vw,52px)/1 var(--display); text-transform:uppercase;
  -webkit-text-stroke:var(--stroke-lg) var(--outline); paint-order:stroke fill;
  filter:drop-shadow(0 5px 0 rgba(3,6,16,.6));
}
.scr-title .a{ color:var(--aqua); } .scr-title .b{ color:var(--gold); }
/* ── mode select had no vertical composition ──────────────────
   `.screen` is a 3-row grid (auto 1fr auto) and this is the 1fr
   row, so it stretched full height while its content stayed packed
   at the top — leaving a ~440px hole between the OPPONENTS pills
   and the START MATCH button on a 1270px viewport, roughly 58% of
   the screen showing nothing but a dimmed arena. The menu fills its
   space and the pause screen centres in its space; only this screen
   read as a web form with the submit button kicked to the footer.
   It centres now, which is also what makes the arena behind it read
   as a stage rather than as dead air. */
.modes-body{
  overflow:auto; padding:var(--sp-2) var(--sp-5) var(--sp-6);
  display:flex; flex-direction:column; justify-content:center;
  /* `safe` so centring can never push the first row out of reach
     when the column overflows; browsers without it keep plain
     center from the line above. The fatter bottom padding is the
     fold clearance: at 800x750 with the UNEVEN side pickers open,
     the last pill row was cut mid-glyph at the scroll edge with
     START MATCH sitting directly on the slice. */
  justify-content:safe center;
  gap:var(--sp-5); min-height:0;
}
.mode-grid{
  display:grid; grid-template-columns:repeat(auto-fit,minmax(232px,1fr));
  gap:14px; max-width:1080px; margin:0 auto; width:100%;
}
.modecard{
  position:relative; border:0; cursor:pointer; text-align:left;
  padding:15px 16px; min-height:118px; color:var(--ink);
  display:flex; flex-direction:column; gap:7px;
  background:linear-gradient(180deg,#1c3667,#101f42);
  clip-path:polygon(var(--cut) 0,100% 0,100% calc(100% - var(--cut)),
                    calc(100% - var(--cut)) 100%,0 100%,0 var(--cut));
  --edge:var(--outline); --lift:0 5px 0 rgba(3,6,16,.55);
  filter:
    drop-shadow(2px 0 0 var(--edge))  drop-shadow(-2px 0 0 var(--edge))
    drop-shadow(0 2px 0 var(--edge))  drop-shadow(0 -2px 0 var(--edge))
    drop-shadow(var(--lift));
  transition:transform var(--dur-fast) var(--ease-pop), background var(--dur-fast);
}
.modecard:hover{ transform:translateY(var(--lift-y));
                 background:linear-gradient(180deg,#24438033,#1c3667),
                            linear-gradient(180deg,#24438f,#152c5c); }

/* ── one "this one is selected" language ──────────────────────
   These two live 40px apart on the same screen and answered the
   same question — "which of these did I pick?" — in two different
   voices. `.pill.on` fills solid gold with a near-white rim and is
   unmissable. `.modecard.on` got a 2px gold rim on a barely-shifted
   blue face, next to five other cards that also have visible rims,
   so the default selection read as no selection at all: you could
   not tell SABOTEUR was already chosen, which made START MATCH feel
   like it was about to do something arbitrary.

   The card cannot simply go solid gold — it carries body copy, and
   a 12px description on gold fails contrast. So the card keeps a
   dark face and takes everything else the pill has: the gold rim
   goes to full weight, a gold wash lifts the face, a gold bar seals
   the cut corner, and the whole thing sits forward. Same signal,
   same colour, same confidence, at the scale each control allows. */
.modecard.on{
  --edge:var(--gold);
  --lift:0 5px 0 rgba(3,6,16,.55);
  background:
    linear-gradient(180deg,rgba(255,198,61,.20),rgba(255,198,61,0) 62%),
    linear-gradient(180deg,#27509b,#152c5c);
  transform:translateY(var(--lift-y));
  filter:
    drop-shadow(3px 0 0 var(--edge))  drop-shadow(-3px 0 0 var(--edge))
    drop-shadow(0 3px 0 var(--edge))  drop-shadow(0 -3px 0 var(--edge))
    drop-shadow(var(--lift)) drop-shadow(0 0 18px rgba(255,198,61,.35));
}
/* the selected card's own cut corner, filled — the pill's "on"
   state reads as a solid slab and this is the card's equivalent */
.modecard.on::after{
  content:""; position:absolute; right:0; bottom:0;
  width:calc(var(--cut) * 2); height:calc(var(--cut) * 2);
  background:linear-gradient(135deg,transparent 49%,var(--gold) 50%);
  pointer-events:none;
}
.modecard.on:hover{ transform:translateY(calc(var(--lift-y) * 1.6)); }
.mc-name{ font:900 19px/1 var(--display); letter-spacing:.04em;
          -webkit-text-stroke:var(--stroke-md) var(--outline); paint-order:stroke fill; }
.m-sab .mc-name{ color:var(--aqua); }  .m-cha .mc-name{ color:var(--violet); }
.m-ars .mc-name{ color:var(--gold); }  .m-mae .mc-name{ color:var(--hot); }
.m-cla .mc-name{ color:#dff7ff; }      .m-pow .mc-name{ color:var(--lime); }
.m-cus .mc-name{ color:var(--gold); }   /* CUSTOM — a door, not a rule set */
.mc-desc{ font:600 12px/1.5 var(--ui); color:var(--ink-dim); }

.opt-rows{ display:flex; gap:34px; justify-content:center; flex-wrap:wrap; }
.opt h5{ margin:0 0 8px; font:800 11px/1 var(--ui); letter-spacing:.18em;
         color:var(--ink-faint); text-align:center; }
.pills{ display:flex; gap:7px; flex-wrap:wrap; justify-content:center; }
.pill{
  position:relative; border:0; cursor:pointer; min-width:42px; padding:9px 14px;
  background:linear-gradient(180deg,#1c3667,#101f42); color:var(--ink-dim);
  font:800 13px/1 var(--ui); letter-spacing:.08em;
  clip-path:polygon(7px 0,100% 0,calc(100% - 7px) 100%,0 100%);
  --edge:var(--outline); --lift:0 3px 0 rgba(3,6,16,.55);
  filter:
    drop-shadow(1.5px 0 0 var(--edge)) drop-shadow(-1.5px 0 0 var(--edge))
    drop-shadow(0 1.5px 0 var(--edge)) drop-shadow(0 -1.5px 0 var(--edge))
    drop-shadow(var(--lift));
  transition:transform var(--dur-fast) var(--ease-pop), background var(--dur-fast);
}
.pill:hover{ transform:translateY(var(--lift-y));
             background:linear-gradient(180deg,#24438f,#152c5c); }
.pill.on{ background:linear-gradient(180deg,var(--gold),var(--gold-lo)); color:#2a1500; }
.pill.on{ --edge:#fff3c4; }
/* The foot was fully transparent over a scrollable body, so pill
   rows scrolled through START MATCH's row and read between the
   buttons. The ramp grounds the foot the same way .res-top grounds
   the results header — content fades before it reaches the button. */
.modes-foot{
  display:flex; justify-content:center; padding:var(--sp-3) 0 var(--sp-6);
  background:linear-gradient(180deg, rgba(5,8,20,0), rgba(5,8,20,.82) 55%, rgba(5,8,20,.92));
}
.btn.back{ padding:9px 16px; font-size:13px;
           clip-path:polygon(16px 0,100% 0,100% 100%,16px 100%,0 50%); }

/* ── four screens, four relationships with the arena ──────────
   The menu composes a real vignette over the live game — dark at
   the edges, a clear hole in the middle — so the arena reads as
   depth behind the shell. Mode select dropped a single flat
   rgba(5,8,20,.86) sheet, which dims the middle it wants you to see
   and leaves the top corners bright enough that crowd colour fights
   the title. Same scrim family as the menu now: edges go down, the
   centre stays open, and the two screens finally feel like the same
   room. (pause.css and results.css each have their own third and
   fourth version of this; those are theirs to align.) */
.modes::before{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:
    linear-gradient(90deg, rgba(5,8,20,.94) 0%, rgba(5,8,20,.70) 18%,
                    rgba(5,8,20,.52) 38%, rgba(5,8,20,.52) 62%,
                    rgba(5,8,20,.70) 82%, rgba(5,8,20,.94) 100%),
    linear-gradient(180deg, rgba(5,8,20,.92) 0%, rgba(5,8,20,.62) 26%,
                    rgba(5,8,20,.62) 70%, rgba(5,8,20,.94) 100%);
}

/* ── HUD ──────────────────────────────────────────────────── */
.hud{ pointer-events:none; }
.hud-top{ display:flex; align-items:center; gap:9px; padding:14px 18px; }
.hud-bottom{ display:flex; justify-content:center; padding:0 0 26px; }
.chip{
  display:flex; align-items:center; gap:8px; padding:7px 13px;
  background:linear-gradient(180deg,rgba(28,54,103,.94),rgba(13,26,58,.94));
  font:800 12px/1 var(--ui); letter-spacing:.12em; color:var(--ink);
  clip-path:polygon(8px 0,100% 0,calc(100% - 8px) 100%,0 100%);
  box-shadow:0 0 0 2px var(--outline),0 3px 0 rgba(0,0,0,.5);
  /* a chip is one short label; at 375px wide "8 LEFT" was wrapping
     to two lines and standing taller than its whole row */
  white-space:nowrap;
}
/* …with ONE exception, and it is deliberate. `.chip.warn` is the
   offline/local-only notice, and it carries a SENTENCE — "LOCAL ONLY
   — NO ONLINE BOARD IN THIS BUILD" is 361px flat, wider than a Galaxy
   S8+. Held on one line it ran 15px off the right of a <body> that
   clips, which is exactly the thing this chip exists to explain. */
.chip.warn{ white-space:normal; line-height:1.3; }
/* A chip with no content is not a chip. hud.js clears the cup chip
   with empty text after a tournament, which left a bare 26x14
   parallelogram floating in the lane of every following match —
   measured live: #hudCup at [285,20,26,14] during a versus match.
   Guard the whole class: an empty chip never has a legitimate
   render, whoever forgets to hide one next. */
.chip:empty{ display:none; }
/* At 375px the lane overflowed: the mode chip was clipped mid-word
   at the viewport edge and ESC — PAUSE sat entirely off-screen at
   x=438. Wrap instead of clip, tighten the metrics, and drop the
   ESC hint — it advertises a key a phone does not have. #hudMode
   is excepted from the ghost cull: knowing the mode matters.
   (This block must stay AFTER the base .chip rule — same
   specificity, cascade order is what makes it win.) */
@media (max-width:560px){
  /* one line now, so the toast only has to clear one row */
  :root{ --toast-top:70px; }
  .hud-top{ flex-wrap:nowrap; gap:5px; padding:9px 10px; }
  .chip{ padding:5px 6px; font-size:10px; letter-spacing:.04em; gap:5px; }
  /* MEASURED at 375px with the longest real label (TEAMS - SABOTEUR):
     the row wanted 381px and the mode chip wanted 17 more than it
     got. The pips chip was the widest fixed item at 99px, so the
     23px comes from there and from a pixel of chip padding each,
     rather than from truncating a label. */
  .pips{ gap:2px; }
  .pips i{ width:9px; height:7px; }
  .hud-top .chip.ghost:not(#hudMode){ display:none; }
  /* THE ONE CHIP ALLOWED TO GIVE UP WIDTH. Everything else is a
     short fixed label that means nothing truncated — "4 V" is not a
     format — whereas TEAMS - SABOTEUR still reads cut short. */
  #hudMode{ min-width:0; overflow:hidden; text-overflow:ellipsis;
            display:block; white-space:nowrap; }
  .hud-top > .chip:not(#hudMode){ flex:none; }
  /* NO SPACER AT ALL HERE. It exists to push the mode and pause to
     the right edge, and at this width there is no slack to push
     into — it was taking 37px as flex:1 while the mode chip
     truncated beside it. Removing it is the last 8px the longest
     real label needed to fit whole. */
  .hud-top > .grow{ display:none; }
}
.chip.ghost{ color:var(--ink-faint); background:linear-gradient(180deg,rgba(16,30,62,.7),rgba(9,18,40,.7)); }
.chip b{ color:var(--aqua); }
.pips{ display:flex; gap:4px; }
.pips i{ width:16px; height:8px; border-radius:2px; background:var(--lime);
         box-shadow:0 0 0 1.5px var(--outline), 0 0 8px rgba(157,255,77,.7); }
.pips i.off{ background:#26365c; box-shadow:0 0 0 1.5px var(--outline); }
/* Shell-wide toast host. Lives outside .screen.hud so a toast fired
   from the menu is actually visible — see the note in hud.js.

   TOP CENTRE. It used to sit at bottom:96px, which on a handheld is
   under the saboteur strip — that strip is anchored to the bottom in
   portrait and is taller than 96px, so the notification was being
   painted over by the panel it most often refers to. The top band is
   the one place free on every layout: the HUD chips are a single row
   there and the toast clears them. */
.hud-toastlayer{
  position:absolute; left:0; right:0;
  /* 82, not 72: the chip row ends at y=63 on desktop and 72 left a
     nine-pixel gap that read as touching. */
  top:calc(env(safe-area-inset-top, 0px) + var(--toast-top, 82px));
  display:flex; justify-content:center;
  pointer-events:none; z-index:60;
}

.toast{
  padding:12px 24px; opacity:0; transform:translateY(-10px);
  max-width:min(92vw, 620px); text-align:center; text-wrap:balance;
  background:linear-gradient(180deg,var(--gold),var(--gold-lo)); color:#2a1500;
  font:900 14px/1 var(--display); letter-spacing:.1em; text-transform:uppercase;
  clip-path:polygon(12px 0,100% 0,calc(100% - 12px) 100%,0 100%);
  box-shadow:0 0 0 3px var(--outline),0 5px 0 rgba(0,0,0,.5);
  transition:opacity .22s, transform .22s cubic-bezier(.3,1.5,.5,1);
}
.toast.on{ opacity:1; transform:translateY(0); }

/* ── match countdown ─────────────────────────────────────────
   The simulation has always run a real three-second countdown and
   ticked a sound each second; nothing drew it, so every match
   opened on three dead seconds with a still arena and no ball.
   Centred over the arena, above the HUD chips but below any
   overlay. Scale and opacity only — both composited, so this
   cannot cost layout during the busiest moment of the match. */
.hud-count{
  position:absolute; inset:0; display:grid; place-items:center;
  pointer-events:none; opacity:0; z-index:3;
}
.hud-count.on{ opacity:1; }
.hud-count span{
  font:900 clamp(96px,17vmin,220px)/1 var(--display);
  letter-spacing:.02em;
  color:var(--aqua);
  -webkit-text-stroke:var(--stroke-xl,10px) var(--outline);
  paint-order:stroke fill;
  text-shadow:0 10px 0 var(--outline), 0 0 46px rgba(47,230,214,.55);
  transform-origin:50% 50%;
}
/* Anticipation then overshoot, so the number arrives with weight
   instead of appearing. Each tick restarts it — see hud.js. */
.hud-count.tick span{ animation:hudCount .92s cubic-bezier(.16,1.4,.36,1) both; }
@keyframes hudCount{
  0%   { transform:scale(1.9); opacity:0; }
  14%  { transform:scale(.92); opacity:1; }
  30%  { transform:scale(1.06); }
  46%  { transform:scale(1); }
  100% { transform:scale(.86); opacity:0; }
}
/* GO is the start signal, not another number: it takes the accent
   colour, sits a touch wider, and pushes OUT on the way off rather
   than shrinking away like a tick. */
.hud-count.is-go span{
  color:var(--lime,#9dff4d);
  letter-spacing:.06em;
  text-shadow:0 10px 0 var(--outline), 0 0 56px rgba(157,255,77,.60);
}
.hud-count.is-go.tick span{
  animation:hudGo 1.05s cubic-bezier(.16,1.4,.36,1) both;
}
@keyframes hudGo{
  0%   { transform:scale(1.55); opacity:0; }
  12%  { transform:scale(.94);  opacity:1; }
  28%  { transform:scale(1.10); }
  46%  { transform:scale(1); }
  100% { transform:scale(1.28); opacity:0; }
}
html.op-a11y-still .hud-count.is-go.tick span{
  animation:none; transform:none; opacity:1;
}
@media (prefers-reduced-motion:reduce){
  .hud-count.is-go.tick span{ animation:none; transform:none; opacity:1; }
}

html.op-a11y-still .hud-count.tick span,
html.op-a11y-still .hud-count span{ animation:none; transform:none; opacity:1; }
@media (prefers-reduced-motion:reduce){
  .hud-count.tick span{ animation:none; transform:none; opacity:1; }
}

/* ── match ribbon ────────────────────────────────────────────
   The simulation raises these for KNOCKOUT, OWN GOAL, FRIENDLY
   FIRE, boons claimed and refused hazard placements — all of it
   was computed and discarded because nothing drew it.
   Sits above the toast lane so the two can overlap without
   colliding: a toast is the shell talking to you, a ribbon is the
   match reacting to what you just did. */
.hud-ribbon{
  position:absolute; left:0; right:0; bottom:24%;
  display:grid; place-items:center; pointer-events:none;
  opacity:0; z-index:2;
}
.hud-ribbon.on{ animation:hudRibbon 1.5s cubic-bezier(.16,1.2,.3,1) both; }
.hud-ribbon span{
  font:900 clamp(20px,3.4vmin,38px)/1 var(--display);
  letter-spacing:var(--ls-caps,.06em);
  color:var(--gold,#ffc63d);
  -webkit-text-stroke:var(--stroke-md,4px) var(--outline);
  paint-order:stroke fill;
  text-shadow:0 4px 0 var(--outline);
  padding:.28em .7em;
}
@keyframes hudRibbon{
  0%   { opacity:0; transform:translateY(14px) scale(.92); }
  12%  { opacity:1; transform:translateY(0) scale(1.04); }
  22%  { transform:scale(1); }
  78%  { opacity:1; transform:translateY(0) scale(1); }
  100% { opacity:0; transform:translateY(-10px) scale(.98); }
}
html.op-a11y-still .hud-ribbon.on{ animation:none; opacity:1; }
@media (prefers-reduced-motion:reduce){
  .hud-ribbon.on{ animation:none; opacity:1; }
}

/* ── the HUD must not depend on a transition to disappear ────
   .screen hides via a 0.12s-delayed visibility flip so screen
   changes can crossfade. But a transition only completes when the
   page's clock is running: background the tab at the moment the
   HUD stands down (alt-tab as the match ends) and the flip never
   happens — an invisible, still-hit-testable layer sits over the
   results screen indefinitely. The overlay screens keep the fade;
   the in-match chrome gets a hard gate. Same reasoning as the
   tray's own display:none (tray.css). */
.screen.hud:not(.on){ display:none; }

/* ── TOUCH TARGETS ───────────────────────────────────────────
   Measured on a phone: .btn came out 33px tall, .pill 28px, and a
   <select> 34px. The floor for a finger is 44px, and this shell is
   meant to be played on a phone.

   WHY NOT A ::before HIT PAD, which is the usual trick and costs no
   layout at all: every control on this screen is CLIP-PATHED — the
   hex bars, the pills, the chips — and a clip-path clips the element's
   pseudo-elements with it. A pad drawn outside the shape is simply not
   there. So the box has to actually grow, which is why this is gated
   on `pointer:coarse` and not shipped to the desktop layout: the
   mouse-driven build keeps the tighter rhythm it was designed with.

   Growth is vertical only and never touches font size, padding-inline,
   or the clip shapes, so nothing here changes a line length or a
   wrapping point — the full device matrix was re-measured after it
   landed. `place-content:center` is needed because .pill is a plain
   inline-block; min-height alone would leave its label at the top. */
@media (pointer:coarse){
  #ui :is(.btn,.pill){ min-height:44px; }
  #ui .pill{ min-width:44px; }
  #ui .pill{ display:inline-flex; align-items:center; justify-content:center; }
  #ui :is(select,input[type="text"],input[type="search"],input:not([type])){ min-height:44px; }
}

/* ── IN-MATCH CHROME STANDS DOWN FOR A FULL-SCREEN OVERLAY ───
   Screenshotted, not assumed: with PAUSE open the HUD's clock,
   "YOU" pips and "8 LEFT" chip were still painting at the top-left,
   the mode/ESC chips at the top-right, and the tray's SABOTEUR
   dormant card at the bottom-left — all of them ON TOP of the pause
   menu's own MATCH SUMMARY panel, which says the same numbers again.
   That is the "menus showing other menus" report, literally.

   main.js already does this by hand for RESULTS, SERIES and the CUP
   card (`this.hud.show(false); this.tray.show(false)` in the
   game-over branch). It does NOT do it for pause, because pause is a
   toggle the match survives. So the rule is stated here once,
   declaratively, for every full-screen overlay that can sit over a
   live match — and it stays correct if a new one is added.

   `visibility`, not `display`: the HUD is a live-updating layer with
   cached element references and the tray runs its own sync every
   frame. Hiding without re-laying-out means both come back exactly
   as they were, with no reflow and no re-read of the simulation. */
#ui:has(> .screen.pause.on, > .screen.res.on, > .screen.cup.on, > .screen.series.on)
  :is(.screen.hud, .screen.tray-layer, .hud-toastlayer){
  visibility:hidden;
}

/* ══ BUTTON SHAPE — THE ONE KNOB ═══════════════════════════════
   The shell was authored with chevron-ended and angle-cut controls:
   .btn is a polygon with a 16px point on its trailing edge, .pill is
   a parallelogram, .set-nav.on is a jagged shard. Handsome, but every
   one of them is a bespoke silhouette, which means two controls side
   by side never share an edge — and hand-drawn button art dropped in
   later would have to fight a clip-path it did not know about.

   So: BUTTONS ARE RECTANGLES. This block is the whole change. Two
   tokens and one selector list, in one place, rather than rewriting
   ninety authored clip-paths — and it is one block to comment out if
   the chevrons are ever wanted back, which is why it is written as a
   block rather than smeared across twelve sheets.

     --btn-shape   the silhouette. `inset(0)` is a plain rectangle.
     --btn-radius  corner softening, 0 by default. Set it to 4px and
                   every control below rounds together.

   `#ui` is deliberate: it scores (1,1,0), which beats every
   class-only rule in every screen sheet regardless of source order,
   so `.pause-stack .btn` and `.set-nav.on` are covered without an
   !important anywhere.

   `inset(0)` rather than `clip-path:none` is also deliberate, and it
   is load-bearing: the presence of a clip-path is what makes these
   elements clip their own `outline` (trap 2, top of this file), which
   is why every focus state in the shell is drawn with a brighter
   --edge in the drop-shadow chain instead. Removing the clip outright
   would put a second, differently-shaped focus ring back on every
   control in the game. It also keeps the stacking context each of
   these already had, so nothing re-layers.

   WHAT STAYS SHAPED, and why — these are not buttons-with-a-shape,
   the shape IS the meaning:
     · .lb-swatch / .sel-sw   seat-colour hexes. The hex is the seat
                              token, matched in the ring and the sim.
     · .rank-chip             a medal, not a control.
     · .chip                  a badge. It reports, it does not act.
     · .mn-tool / .mn-plus    circular icon tools in the top bar.
     · .panel / .panel-head   panels are not buttons; the cut corner
                              is the product's whole panel language.
     · .mn-btn                finished artwork. No clip-path to change.
     · cards (.modecard, .lb-arena, .lb-room, .sel-card)  left alone
                              on purpose: they are surfaces, and
                              re-cutting them is a design change
                              rather than a button change.
   ══════════════════════════════════════════════════════════════ */
:root:root{
  --btn-shape:inset(0);
  --btn-radius:0px;
}
#ui :is(
  .btn, .pill,
  .set-nav, .set-nudge, .set-sw, .set-stepval,
  .cst-mini, .cst-sw, .cst-text,
  .bd-tab,
  .lb-kick, .lb-input, .lb-sel,
  .mn-cta, .mn-sc, .mn-invite,
  .tray-slot, .tray-tab
){
  clip-path:var(--btn-shape);
  border-radius:var(--btn-radius);
}

/* ── MODES: the TEAMS panel ──────────────────────────────────
   Names, colours and seating live on the mode screen as well as in
   the CUSTOM tuner. Deliberately NOT the tuner's `cst-` classes:
   custom.css is lazily injected by custom.js and this screen must
   not depend on another screen having been opened first. The rules
   behind the controls are shared (teams.js); only the paint is
   local. Rectangular, matching the button pass. */
.opt.mo-wide{ flex-basis:100%; min-width:0; }
.mo-hint{
  margin:8px auto 0; text-align:center; text-wrap:balance;
  /* min(), not a bare 52ch: 52ch is wider than a 375px phone and
     the note ran off the right edge rather than wrapping */
  max-width:min(52ch, 100%);
  font:600 12px/1.5 var(--ui); color:var(--ink-dim);
}

.mo-names{ display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
.mo-nameline{ display:flex; align-items:center; gap:8px; }
/* No colour bar: the swatch row directly beneath says the same thing
   at a size you can actually read, and two colour cues for one fact
   is one more than the row needs. */
.mo-sidelab{
  display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
  font:800 11px/1 var(--ui); letter-spacing:.14em; color:var(--ink-dim);
  padding:6px 9px; background:rgba(6,12,28,.55);
}
.mo-name{
  min-width:0; width:13ch; padding:8px 10px; color:var(--ink);
  font:800 13px/1 var(--ui); letter-spacing:.08em; text-transform:uppercase;
  background:linear-gradient(180deg,#0e1c3c,#0a1430);
  border:2px solid rgba(127,245,234,.28); border-radius:0;
}
.mo-name::placeholder{ color:var(--ink-faint); opacity:.75; }
.mo-name:focus{ outline:none; border-color:var(--gold); }

.mo-cols{ display:flex; flex-direction:column; gap:9px; align-items:center; }
.mo-colrow{ display:flex; gap:7px; align-items:center; flex-wrap:wrap; justify-content:center; }
.mo-fam{ min-width:0; padding:8px 11px; font-size:11px; }
/* .pill has no gap of its own — the icon was touching the R */
.mo-reshuffle{ display:inline-flex; align-items:center; gap:7px; }
/* A SWATCH IS A CHOICE OF PALETTE INDEX, not a colour picker — the
   sim recovers colorIdx by looking the hex up in OP.PALETTE, and a
   colour it cannot find falls back to the seat number, which detaches
   the colourblind glyph from its wall. */
.mo-sw{
  width:30px; height:30px; padding:0; border:0; cursor:pointer;
  background:rgba(6,12,28,.55); position:relative;
  box-shadow:inset 0 0 0 2px rgba(3,6,16,.75);
  transition:transform var(--dur-fast) var(--ease-pop);
}
.mo-sw i{ position:absolute; inset:5px; background:var(--tm); display:block; }
.mo-sw:hover:not(:disabled){ transform:translateY(-2px); }
.mo-sw.on{ box-shadow:inset 0 0 0 2px var(--gold), 0 0 0 2px rgba(3,6,16,.75); }
.mo-sw.on i{ inset:4px; }
.mo-sw:disabled{ opacity:.28; cursor:not-allowed; }

/* THE RING, so SHUFFLE is something you watch happen rather than
   something you are told about. Seat 0 sits at the bottom, which is
   where the local player's wall is in every arena. */
.mo-ring{
  position:relative; width:104px; height:104px; margin:10px auto 0;
  border:2px dashed rgba(127,245,234,.22); border-radius:50%;
}
.mo-seat{
  position:absolute; width:15px; height:15px; margin:-7.5px 0 0 -7.5px;
  background:var(--tm); box-shadow:0 0 0 2px rgba(3,6,16,.8);
}
@media (prefers-reduced-motion: reduce){
  .mo-sw:hover:not(:disabled){ transform:none; }
}

/* ── HUD pause ───────────────────────────────────────────────
   A real button, not the old ghost chip that only NAMED a key. It
   keeps the chip look on a pointer device and collapses to a bare
   glyph on touch, where the words "ESC —" are a lie. */
/* NO BOX. It is a control, not a readout, and the chip plate around
   it made it read as one more label in a row of labels. Background,
   the angled clip and the drop shadow all come off; the glyph and
   its hit area stay. */
.chip.ghost.hud-pause,
.hud-pause{
  cursor:pointer; pointer-events:auto; display:inline-flex;
  align-items:center; gap:7px; border:0; font:inherit;
  background:none; box-shadow:none; clip-path:none;
  padding:7px 6px; color:var(--ink-dim);
}
.hud-pause:active{ transform:translateY(1px); }
.hud-pause:hover{ color:var(--ink); }
.hud-pause-i{
  width:11px; height:13px; flex:none;
  /* two bars, drawn with a border rather than two elements */
  border-left:4px solid currentColor; border-right:4px solid currentColor;
  display:none;
}
/* Touch: the glyph replaces the words, and the hit area goes to 44px
   square — the smallest target a thumb reliably hits, and the reason
   the chip cannot simply stay a chip here. */
@media (pointer: coarse){
  .hud-pause-t{ display:none; }
  .hud-pause-i{ display:block; }
  .hud-pause{ min-width:44px; min-height:44px; justify-content:center; padding:0 12px; }
}
/* The 560px rule above culls every ghost chip as keyboard chrome.
   This one is the opposite — it is the ONLY way into pause without a
   keyboard, so it is explicitly exempt.

   THE :not(#hudMode) IS WHY THIS SELECTOR LOOKS ODD. An id inside
   :not() still counts as an id, so that cull scores (1,3,1) and a
   plain .hud-top .chip.ghost.hud-pause at (0,4,1) loses to it no
   matter how many classes are added. Repeating the :not() is what
   buys back the id and puts this rule one class ahead. Measured: the
   button was 0x0 on a 375px viewport before this. */
@media (max-width:560px){
  .hud-top .chip.ghost.hud-pause:not(#hudMode){ display:inline-flex; }
}
