/* ---------------------------------------------------------------------------
   arena-chrome.css — the parts of a host screen that are not about any one game.

   Every rule in here was byte-identical inside wobble, tipsy and friction's own
   <style> blocks. It is the QR ticket, the leaderboard modal, the share and invite
   rows, the sound hint, the toast layer, the ad box, the TV guide, the roster chips,
   the sticker overlay card and the mode buttons — the furniture a room sees around
   whatever game is being played.

   Loaded BEFORE each game's inline <style>, so a game can still override anything
   here. That order was checked, not assumed: no selector in this file is also
   defined in any game's own CSS, so nothing changed hands when it moved.

   Three copies of this drifted apart once already. Friction's roster had become a
   different component, its .chipDot had no styling anywhere, and its countdown card
   was translucent while the other two were solid. Fix things here now.
   --------------------------------------------------------------------------- */
:root {
    --ink: #221E3A;
    --dusk: #2E2A5E;
    --cream: #FDF6E3;
    --cherry: #FF4E6A;
    --mint: #23C8A0;
    --taxi: #FFB800;
    --display: 'Baloo 2', ui-rounded, "Arial Rounded MT Bold", sans-serif;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { height: 100%; overflow: hidden; background: #262250; }
  body { font-family: var(--display); }
  canvas { position: absolute; inset: 0; width: 100%; height: 100%; }

  .sticker {
    position: absolute;
    background: var(--cream);
    color: var(--ink);
    border: 3px solid var(--ink);
    border-radius: 14px;
    box-shadow: 5px 5px 0 rgba(0,0,0,.35);
    font-family: var(--display);
  }
  #timer {
    top: 18px;
    left: 50%;
    transform: translateX(-50%) rotate(-2deg);
    font-size: clamp(28px, 6vh, 54px);
    font-weight: 700;
    color: var(--taxi);
    -webkit-text-stroke: 1.5px var(--ink);
    paint-order: stroke fill; /* stroke behind fill so the border never cuts into the glyphs */
    padding: 4px 26px;
    font-variant-numeric: tabular-nums;
    transition: color 400ms;
  }
  #timer.danger { color: var(--cherry); }
  #timer.bounce { animation: tbounce 500ms; }
  @keyframes tbounce { 30% { transform: translateX(-50%) rotate(-2deg) scale(1.16); } }
  #overlay.curtain { animation: drop 700ms cubic-bezier(.2,1.4,.5,1); }
  @keyframes drop { from { transform: translate(-50%, -160vh) rotate(-1.5deg); } }
  button, a.shareBtn, .shareOpt, #howBtn, #donateBtn { transition: filter 160ms ease, box-shadow 160ms ease, background-color 160ms ease, opacity 160ms ease, color 160ms ease; }
  /* one hover language for every clickable sticker */
  .modeBtn:not(:disabled):hover, #inviteRow button:hover, #lbBtn:hover, #lbClose:hover,
  #invClose:hover, .lbTab:not(.on):hover {
    filter: brightness(1.07);
    box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.4);
  }
  #soundHint { top: 18px; right: 18px; font-size: 13px; padding: 6px 12px; transform: rotate(2deg); opacity: 0.9; }
  #soundHint.hidden { display: none; }
  #howBtn {
    font-family: var(--display);
    font-size: 13px;
    font-weight: 700;
    color: var(--cream);
    opacity: 0.85;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
    text-shadow: 1px 1px 0 rgba(0,0,0,.4);
    margin-left: 4px;
  }
  #howBtn:hover { opacity: 1; color: var(--taxi); }
  #howBtn:focus-visible { outline: 4px solid #4EA5FF; }
  #qrBox {
    right: 10%;
    top: 50%;
    transform: translateY(-50%) rotate(-2.5deg);
    padding: 12px 14px 10px;
    text-align: center;
  }
  /* the seat count straddles the card's bottom edge like a sticker stuck half on —
     the one number a room needs before anyone reaches for a phone */
  #seatBadge {
    position: absolute;
    left: 50%;
    bottom: 0;
    transform: translate(-50%, 50%) rotate(-1.5deg);
    font-family: var(--display);
    font-size: 12.5px;
    font-weight: 700;
    letter-spacing: 0.08em;
    white-space: nowrap;
    color: var(--ink);
    background: var(--mint);
    border: 3px solid var(--ink);
    border-radius: 999px;
    padding: 5px 16px;
    box-shadow: 3px 3px 0 rgba(0,0,0,.35);
  }
  #qrBox .qrLabel { font-size: 13px; font-weight: 700; letter-spacing: 0.05em; margin-bottom: 8px; }
  #qrImg { display: block; width: 168px; height: 168px; margin: 0 auto; }
  #joinCode {
    margin-top: 10px;
    padding-top: 8px;
    border-top: 3px dashed rgba(34, 30, 58, 0.25);
    font-size: 38px;
    font-weight: 700;
    letter-spacing: 0.22em;
    text-indent: 0.22em; /* visually recenters the letter-spaced code */
    color: var(--ink);
  }
  #inviteRow { display: flex; justify-content: center; gap: 12px; align-items: center; margin-top: 12px; font-size: 0.5em; }
  #inviteRow button {
    font-family: var(--display);
    font-weight: 700;
    font-size: 1em;
    color: var(--ink);
    background: var(--cream);
    border: 2.5px solid var(--ink);
    border-radius: 999px;
    padding: 6px 14px;
    box-shadow: 3px 3px 0 rgba(0,0,0,.25);
    cursor: pointer;
  }
  #inviteRow button:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 rgba(0,0,0,.25); }
  #lbBtn {
    top: 18px;
    left: 18px;
    font-size: 14px;
    font-weight: 700;
    padding: 8px 14px;
    transform: rotate(-2deg);
    cursor: pointer;
  }
  #lbBtn:active { transform: rotate(-2deg) translate(2px, 2px); }
  #lbBtn:focus-visible, #lbClose:focus-visible { outline: 4px solid #4EA5FF; }
  #lbModal {
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(-1deg);
    padding: 24px 32px;
    font-size: 18px;
    min-width: 340px;
    z-index: 30;
  }
  #inviteModal {
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(-1deg);
    padding: 24px 32px;
    font-size: 18px;
    max-width: 480px;
    z-index: 30;
  }
  /* modals animate open/close: opacity + spring scale (display:none can't animate).
     Deliberately not gated behind prefers-reduced-motion — short and gentle. */
  #lbModal, #inviteModal {
    opacity: 1;
    visibility: visible;
    transition: opacity 220ms ease, transform 240ms cubic-bezier(.2,1.5,.45,1), visibility 0s;
  }
  /* the ad slot in a modal: hairline rule above it so it reads as a separate thing from the
     card's own content, and centred because the unit is a fixed 300 in a wider card */
  .lbAd {
    margin-top: 16px; padding-top: 14px; text-align: center;
    border-top: 2px dashed rgba(34, 30, 58, 0.18);
  }
  #modalBackdrop {
    position: fixed;
    inset: 0;
    background: rgba(38, 34, 80, 0.72); /* dusk veil */
    opacity: 0;
    pointer-events: none;
    transition: opacity 220ms ease;
    z-index: 25;
  }
  #modalBackdrop.show { opacity: 1; pointer-events: auto; }
  #inviteModal .lbTitle { font-weight: 700; font-size: 24px; text-align: center; margin-bottom: 14px; }
  .invCol { display: flex; flex-direction: column; gap: 10px; }
  .invOpt {
    font-family: var(--display);
    font-weight: 700;
    font-size: 15px;
    text-align: center;
    color: var(--ink);
    background: #fff;
    border: 3px solid var(--ink);
    border-radius: 12px;
    padding: 10px 16px;
    box-shadow: 3px 3px 0 rgba(0,0,0,.25);
    cursor: pointer;
  }
  .invOpt:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 rgba(0,0,0,.25); }
  .invOpt:focus-visible, #invClose:focus-visible { outline: 4px solid #4EA5FF; }
  .invNote { font-size: 13px; text-align: center; opacity: 0.75; line-height: 1.4; margin-top: 12px; }
  /* modal close: a corner ✕, not a word — the modal's own title carries the meaning and the
     button stops competing with the actions inside. Sits on the sticker's top-right corner. */
  #invClose {
    position: absolute;
    /* straddling the corner, tucked in a little from dead-centre so it reads as attached to
       the sticker rather than floating beside it */
    top: -13px;
    right: -13px;
    width: 34px;
    height: 34px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--display);
    font-weight: 700;
    font-size: 17px;
    line-height: 1;
    color: var(--ink);
    background: var(--cream);
    border: 3px solid var(--ink);
    border-radius: 50%;
    box-shadow: 2px 2px 0 rgba(0,0,0,.25);
    cursor: pointer;
  }
  #invClose:active { transform: translate(2px, 2px); box-shadow: none; }
  #lbModal .lbTitle { font-weight: 700; font-size: 24px; text-align: center; margin-bottom: 12px; }
  /* The side chooser — CO-OP against VERSUS, CAMPAIGN against SEASON. It had no rule of its
     own and got by on the modal's inherited text-align, which centres inline-blocks but
     aligns them on the TEXT BASELINE: a pill with padding hangs below the line box it sits
     in, so the row below was drawn seven pixels into it. Flex has no baseline to hang off. */
  #lbKinds { display: flex; justify-content: center; gap: 8px; margin-bottom: 10px; }
  #lbLevel { text-align: center; font-weight: 800; letter-spacing: .08em; font-size: 12px; margin: -4px 0 8px; opacity: .75; }   /* the field the board was won against: EASY or 🔥 HARD, nothing for the normal one */
  /* Above the sides and quieter than them: which of the two games you are looking at is a
     rarer decision than co-op against versus, and it should not shout over it. */
  #lbSource { display: flex; justify-content: center; gap: 8px; margin-bottom: 9px; }
  .lbTab.src { font-size: 11.5px; padding: 4px 13px; }
  #lbTabs { display: flex; justify-content: center; gap: 8px; margin-bottom: 12px; }
  /* THE JOINED PILL, for a game that asks for it: the two kinds read as one control, the chosen half filled,
     exactly the RACE | CUP toggle on the end card (Riho, 2026-09-08). */
  #lbKinds.seg { gap: 0; }
  #lbKinds.seg .lbTab { border-radius: 0; box-shadow: none; }
  #lbKinds.seg .lbTab:first-child { border-radius: 999px 0 0 999px; }
  #lbKinds.seg .lbTab:last-child { border-radius: 0 999px 999px 0; border-left: 0; }
  #lbKinds.seg .lbTab.kind.on { background: var(--ink); color: var(--cream); }
  /* The second chooser: WHAT is ranked, when a game has more than one answer per side.
     Wraps, because Throttle names six circuits plus the campaign total and a row of seven
     will not sit on one line inside the modal. Smaller than the side buttons on purpose —
     the side is the coarse choice and this is the one you flick through. */
  #lbPicks {
    display: flex; flex-wrap: wrap; justify-content: center;
    gap: 6px; margin: 0 auto 10px; max-width: 520px;
  }
  .lbTab.pick { font-size: 11px; padding: 4px 11px; letter-spacing: 0.04em; }
  /* The grouped picks, folded into one pill. Same button, plus a caret and a panel — a
     dropdown that looked like a select would be the only control on these screens that did.
     The panel is absolute so opening it never pushes the rows below it down the card. */
  .lbDrop { position: relative; display: inline-flex; }
  .lbCaret { margin-left: 5px; font-size: 9px; line-height: 1; display: inline-block; }
  .lbDrop.open .lbCaret { transform: rotate(180deg); }
  .lbMenu {
    position: absolute;
    top: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    display: flex; flex-direction: column; gap: 5px;
    padding: 8px;
    background: var(--cream);
    border: 2.5px solid var(--ink);
    border-radius: 14px;
    box-shadow: 4px 4px 0 rgba(0,0,0,.28);
    z-index: 40;                 /* above the rows, and above the modal's own stacking */
    max-height: 46vh; overflow-y: auto;
  }
  .lbMenu[hidden] { display: none; }
  .lbMenu .lbTab { white-space: nowrap; width: 100%; }
  .lbTab {
    font-family: var(--display);
    font-weight: 700;
    font-size: 12px;
    letter-spacing: 0.05em;
    color: var(--ink);
    background: #fff;
    border: 2.5px solid var(--ink);
    border-radius: 999px;
    padding: 5px 14px;
    cursor: pointer;
  }
  .lbTab.on { background: var(--taxi); box-shadow: 2px 2px 0 rgba(0,0,0,.25); }
  .lbTab:focus-visible { outline: 4px solid #4EA5FF; }
  #lbClose:active { transform: translate(2px, 2px); box-shadow: none; }
  /* The two modes explain themselves on the button that starts them, instead of in a
     paragraph the room reads once and never again. Bigger than the .tvTip bubble below —
     this one answers "which of these do I press", which is a larger question than "how do
     I get this on a TV".

     Hover OR focus, never click: click on these buttons starts the game, so pinning a tip
     to it would put a tap between the room and the round. Focus is what a TV remote and a
     keyboard give you before they activate anything, so those get the tip too. */
  #modeBtns { position: relative; }
  .modeTip {
    position: absolute;
    left: 50%;
    bottom: calc(100% + 12px);
    transform: translateX(-50%);
    width: max-content;
    max-width: min(360px, 88vw);
    font-family: var(--display);
    font-weight: 700;
    font-size: 14.5px;
    line-height: 1.4;
    text-align: center;
    color: var(--cream);
    background: var(--ink);
    border-radius: 12px;
    padding: 9px 14px;
    box-shadow: 3px 3px 0 rgba(0,0,0,.3);
    text-shadow: none;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    z-index: 60;
  }
  .modeTip:not(:empty) { opacity: 1; visibility: visible; }

  /* The card is tight, so the bubble lands on the line above the buttons. Rather than
     stack two things on top of each other, that line steps back while a tip is open —
     nobody needs telling to press START while they are reading what START does. */
  #ovBody, #logoImg { transition: opacity 140ms ease; }
  #overlay.tipOpen #ovBody { opacity: 0.12; }
  /* the bubble grew a clip and now reaches the wordmark, so that steps back too — the
     same reasoning as the line below it: nobody is reading a logo while a tip is open */
  #overlay.tipOpen #logoImg { opacity: 0.3; }
  /* No hover to preview with: the two tips simply stand under the buttons. */
  @media (hover: none) {
    .modeTip { display: none; }
    .modeHint { display: block; }
  }
  .modeHint {
    display: none;
    font-size: 0.5em;
    line-height: 1.45;
    opacity: 0.8;
    margin-top: 10px;
  }
  .modeBtn {
    font-family: var(--display);
    font-size: 0.72em;
    font-weight: 700;
    color: var(--ink);
    border: 3px solid var(--ink);
    border-radius: 999px;
    padding: 10px 22px;
    box-shadow: 4px 4px 0 rgba(0,0,0,.3);
    cursor: pointer;
  }
  .modeBtn.coop { background: var(--mint); }
  .modeBtn.versus { background: var(--taxi); }
  .modeBtn:disabled { filter: grayscale(0.8); opacity: 0.55; cursor: not-allowed; box-shadow: none; }
  .modeBtn:not(:disabled):active { transform: translate(3px, 3px); box-shadow: 1px 1px 0 rgba(0,0,0,.3); }
  .modeBtn:focus-visible { outline: 4px solid #4EA5FF; }

  .lbRow { display: flex; gap: 10px; align-items: baseline; padding: 2px 0; }
  .lbRow .r { color: var(--grape, #9B5CF6); font-weight: 700; width: 1.4em; text-align: right; }
  .lbRow .n { opacity: 0.75; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 20em; }

  /* The "next round in Ns" line the game-over card counts down on. I deleted this once as
     dead CSS after grepping the markup for id="ovCount" and finding none — every game
     CREATES it in JS when the round ends, so the grep was looking in the wrong place. */
  #ovCount { display: block; margin-top: 8px; font-size: 0.58em; opacity: 0.7; font-variant-numeric: tabular-nums; }

  /* The seats, bottom-right. Four games each had this rule and Throttle had none — its own
     header comment claimed the roster came from this file, which it did not, so its chips
     rendered in document flow at the top of the page instead of on the seat rail.
     --roster-gap is the only part that varies: a 46px tile leaning 24 degrees is 61px wide,
     so the games that lean their chips need more air than the ones that do not. */
  #roster {
    position: absolute;
    bottom: 18px;
    right: 18px;
    display: flex;
    gap: var(--roster-gap, 10px);
  }
  .chipWrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    animation: chipIn 450ms cubic-bezier(.2,1.6,.4,1);
  }
  @keyframes chipIn { from { transform: scale(0) rotate(-24deg); opacity: 0; } }

  #shareRow { position: absolute; left: 18px; bottom: 18px; display: none; gap: 10px; align-items: center; }
  #donateBtn { display: none; }
  #adBox { display: none; margin-top: 14px; text-align: center; min-height: 0; }
  #sharePop {
    position: absolute;
    left: 0;
    bottom: 78px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    background: var(--cream);
    border: 3px solid var(--ink);
    border-radius: 14px;
    box-shadow: 5px 5px 0 rgba(0,0,0,.35);
    padding: 12px;
    width: 250px;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 200ms ease, transform 220ms cubic-bezier(.2,1.5,.45,1), visibility 0s;
  }
  #sharePop.hidden {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform: translateY(12px) scale(0.9);
    transition: opacity 160ms ease, transform 160ms ease, visibility 0s 160ms;
  }
  .shareOpt {
    font-family: var(--display);
    font-weight: 700;
    font-size: 14px;
    text-align: left;
    color: var(--ink);
    background: #fff;
    border: 2.5px solid var(--ink);
    border-radius: 10px;
    padding: 8px 12px;
    text-decoration: none;
    box-shadow: 2px 2px 0 rgba(0,0,0,.2);
    cursor: pointer;
  }
  .shareOpt:hover { background: var(--taxi); }
  .shareOpt:active { transform: translate(2px, 2px); box-shadow: none; }
  .shareOpt:focus-visible { outline: 4px solid #4EA5FF; }
  .shareBtn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 63px;
    height: 63px;
    background: var(--cream);
    color: var(--ink);
    border: 3px solid var(--ink);
    border-radius: 50%;
    box-shadow: 3px 3px 0 rgba(0,0,0,.35);
    font-family: var(--display);
    font-weight: 700;
    font-size: 23px;
    text-decoration: none;
    transform: rotate(-2deg);
  }
  .shareBtn:nth-child(even) { transform: rotate(2deg); }
  .shareBtn:hover { background: var(--taxi); }
  .shareBtn:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 rgba(0,0,0,.35); }
  .shareBtn:focus-visible { outline: 4px solid #4EA5FF; }

  /* 📺 lobby-only helper: how to get this screen onto a TV — one quiet line of plain-text
     options plus one shared tip line, tucked along the bottom edge under the invite area */
  .tvGuide {
    position: absolute;
    left: 50%;
    bottom: 16px;
    transform: translateX(-50%);
    display: none;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 4px 8px;
    color: var(--cream);
    max-width: min(580px, 92vw); /* capped so the open tip never widens into the roster */
  }
  .tvSafe { margin-top: 4px; font-size: 11.5px; line-height: 1.4; opacity: 0.55; }
  /* quieter than the safety line above it: that one is a warning, this is paperwork */
  .tvLegal { margin-top: 3px; font-size: 11px; opacity: 0.5; }
  /* COLOUR THEM, or they are not there.
     arena-chrome.css carries no blanket `a` rule — every link in this chrome names its own
     colour — and .tvLegal was added without one, so Terms, Privacy and Licences rendered in
     the browser's default #0000EE: dark blue, 11px, at 42% opacity, on a dark indigo
     arena. Present in the DOM, clickable, and invisible to a human. Riho, looking at the
     bottom of the lobby: "the last links not visible".
     Dotted underline because that is what this row already does (.tvTxt) and what the
     footer on every other page type does. */
  .tvSafe a, .tvLegal a {
    color: inherit;
    text-decoration: underline dotted;
    text-underline-offset: 2px;
  }
  .tvLabel { font-size: 12px; font-weight: 700; line-height: 1.15; letter-spacing: 0.02em; opacity: 0.7; }
  .tvRow { display: flex; flex-wrap: wrap; justify-content: center; }
  .tvOpt {
    font-family: var(--display);
    font-weight: 700;
    font-size: 12px;
    line-height: 1.15;
    color: var(--cream);
    background: none;
    border: 0;
    padding: 0;
    opacity: 0.7;
    cursor: pointer;
  }
  .tvOpt:not(:first-child)::before { content: "·"; margin: 0 7px; }
  .tvTxt { text-decoration: underline dotted; text-underline-offset: 3px; }
  @media (hover: hover) {
    .tvOpt:hover { opacity: 1; }
    .tvOpt:hover .tvTxt { text-decoration-style: solid; }
  }
  .tvOpt:focus-visible { outline: 4px solid #4EA5FF; }

  /* ONE HOVER COLOUR FOR THE WHOLE CHROME ROW.
     These four sit on the same line at the bottom of every arena — How to play, All games,
     the TV options and the safety note — and each had grown its own answer to being hovered:
     How to play went yellow, All games and the TV options only brightened, and the safety
     link had no hover rule at all. Riho, looking at the row: "they are different, make all
     yellow". Grouped rather than repeated four times, so the fifth thing added to this row
     inherits the answer instead of inventing one.

     The ids have to be named here rather than relying on the class rule below, because an id
     beats a class and #allGames:hover would otherwise keep the colour it never set. */
  #howBtn:hover,
  #allGames:hover,
  .tvOpt:hover,
  .tvOpt.on,
  .tvSafe a:hover,
  .tvLegal a:hover { color: var(--taxi); }
  .tvOpt.on { opacity: 1; }
  .tvOpt.on .tvTxt { text-decoration-style: solid; }
  /* a real tooltip: floats ABOVE the row so it never shoves the layout, and never opens
     downward off the bottom of the page. Ink card on any background, transient by nature.
     Kept rendered (not display:none) so the aria-live region can announce. */
  .tvTip {
    position: absolute;
    left: 50%;
    bottom: calc(100% + 10px);
    transform: translateX(-50%);
    width: max-content;
    max-width: min(300px, 86vw);
    font-family: var(--display);
    font-weight: 700;
    font-size: 12.5px;
    line-height: 1.35;
    text-align: center;
    color: var(--cream);
    background: var(--ink);
    border-radius: 10px;
    padding: 8px 12px;
    box-shadow: 3px 3px 0 rgba(0,0,0,.3);
    text-shadow: none;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    z-index: 60;
  }
  .tvTip:not(:empty) { opacity: 1; visibility: visible; }

  #toastLayer {
    position: absolute;
    right: 20px;
    bottom: 110px;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 8px;
    pointer-events: none;
    z-index: 20;
  }
  .joinToast {
    background: var(--cream);
    color: var(--ink);
    border: 3px solid var(--ink);
    border-radius: 12px;
    padding: 7px 16px;
    font-family: var(--display);
    font-weight: 700;
    font-size: 15px;
    box-shadow: 4px 4px 0 rgba(0,0,0,.35);
    transform: rotate(-2deg);
    animation: toastIn 350ms cubic-bezier(.2,1.6,.4,1);
    transition: opacity 400ms ease, transform 400ms ease;
  }
  .joinToast.bye { opacity: 0; transform: rotate(-2deg) translateY(10px); }
  @keyframes toastIn { from { transform: rotate(-2deg) scale(0.3) translateX(60px); opacity: 0; } }
  .chipName {
    font-family: var(--display);
    font-size: 10px;
    color: var(--cream);
    max-width: 56px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-shadow: 1px 1px 0 rgba(0,0,0,.45);
  }
  /* The in-round join card: a scannable QR, not a label that needs clicking — nobody can
     click a TV. Same card as the lobby's, small enough to sit out of the way all round. */
  #ticket {
    /* HIDDEN BY DEFAULT, here rather than in each game's own sheet.
       Every host's state machine sets this explicitly — `s === 'waiting' ? 'none' : 'block'`
       — but it does that when the JS runs, and the markup paints before then. Four hosts
       covered the gap with a `#ticket { display: none }` of their own; Throttle did not, so
       its corner scan card flashed up for a frame on every load and then vanished. Riho saw
       it: "the watch page qr code area shows for a sec in host page".
       Defaulting it off here closes the gap for the sixth game too. */
    display: none;
    bottom: 18px;
    left: 18px;
    padding: 8px 10px 6px;
    text-align: center;
    line-height: 1;
  }
  #ticket .qrLabel { font-size: 10px; font-weight: 700; letter-spacing: 0.05em; margin-bottom: 5px; }
  #ticketQr { display: block; width: 104px; height: 104px; margin: 0 auto; }
  #ticket #codeTxt, #ticket #ticketCode {
    display: block;
    margin-top: 6px;
    padding-top: 5px;
    border-top: 3px dashed rgba(34, 30, 58, 0.25);
    font-family: var(--display);
    font-size: 22px;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-indent: 0.2em;
    line-height: 1.1;
    color: var(--ink);
  }
  /* Sits with How to play in the chrome row, not in the bottom-right — that corner belongs
     to the player chips, and taking it pushed every seat up the screen. */
  #allGames {
    display: none;
    font-family: var(--display);
    font-size: 13px;
    font-weight: 700;
    color: var(--cream);
    opacity: 0.85;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
    text-shadow: 1px 1px 0 rgba(0,0,0,.4);
    margin-left: 10px;
  }
  #allGames:hover { opacity: 1; }
  #allGames:focus-visible { outline: 4px solid #4EA5FF; }
  .chip {
    width: 46px;
    height: 46px;
    border: 3px solid var(--ink);
    border-radius: 10px;
    box-shadow: 3px 3px 0 rgba(0,0,0,.3);
    position: relative;
    transition: transform 90ms linear;
  }
  .chip::before, .chip::after {
    content: "";
    position: absolute;
    top: 9px;
    width: 13px;
    height: 13px;
    background: #fff;
    border: 2.5px solid var(--ink);
    border-radius: 50%;
  }
  .chip::before { left: 4px; } .chip::after { right: 4px; }
  .droplet {
    position: absolute;
    top: 50%;
    width: 12px;
    height: 12px;
    margin: -6px 0 0 -6px;
    border-radius: 50%;
    border: 2px solid rgba(34,30,58,.6);
    transition: left 90ms linear;
  }
  #bubble {
    position: absolute;
    top: 50%;
    width: 24px;
    height: 24px;
    margin: -12px 0 0 -12px;
    background: var(--cream);
    border: 3px solid var(--ink);
    border-radius: 50%;
    transition: left 90ms linear;
  }
  #bubble::before, #bubble::after {
    content: "";
    position: absolute;
    top: 5px;
    width: 4px;
    height: 4px;
    background: var(--ink);
    border-radius: 50%;
  }
  #bubble::before { left: 4px; } #bubble::after { right: 4px; }
  #bubble .m {
    position: absolute;
    left: 7px;
    bottom: 3px;
    width: 8px;
    height: 4px;
    border: 2px solid var(--ink);
    border-top: none;
    border-radius: 0 0 8px 8px;
  }
  #bubble.worry .m { border-radius: 8px 8px 0 0; border-top: 2px solid var(--ink); border-bottom: none; height: 3px; bottom: 4px; }
  #overlay .big { font-size: 1.6em; font-weight: 700; margin-bottom: 6px; }
  #overlay .sub { font-size: 0.62em; opacity: 0.75; margin-top: 8px; line-height: 1.4; }
  #overlay.hidden { display: none; }
  #logoImg { width: 520px; max-width: 60%; margin: 0 auto 32px; }
  /* the menu itself only appears once the entrance is armed — no unstyled flash */
  #overlay, #qrBox, #lbBtn, #howBtn, #shareRow, #soundHint, .tvGuide { visibility: hidden; }
  body.anim #overlay, body.anim #qrBox, body.anim #lbBtn,
  body.anim #howBtn, body.anim #shareRow, body.anim #soundHint, body.anim .tvGuide { visibility: visible; }

  /* entrance choreography, mirroring the portal — replays on every open (bfcache too) */
  @keyframes hRise { from { transform: translateY(20px); opacity: 0; } }
  @keyframes hStamp { from { transform: translateY(-50%) rotate(-2.5deg) scale(0.8); opacity: 0; } }
  @keyframes hPop { from { transform: scale(0.7); opacity: 0; } }
  body.anim #logoImg { animation: hRise 620ms cubic-bezier(.2,1.4,.4,1) both; }
  body.anim #ovBody { animation: hRise 600ms ease-out 160ms both; }
  body.anim #ovSub { animation: hRise 600ms ease-out 240ms both; }
  body.anim #modeBtns { animation: hRise 600ms ease-out 340ms both; }
  body.anim #inviteRow { animation: hRise 600ms ease-out 430ms both; }
  body.anim #qrBox { animation: hStamp 620ms cubic-bezier(.2,1.5,.4,1) 520ms both; }
  body.anim #lbBtn, body.anim #soundHint, body.anim #shareRow, body.anim #howBtn {
    animation: hPop 500ms cubic-bezier(.2,1.5,.4,1) 640ms both;
  }
  /* the TV guide keeps its centering transform through the pop, like the QR box does */
  body.anim .tvGuide { animation: tvPop 500ms cubic-bezier(.2,1.5,.4,1) 700ms both; }
  @keyframes tvPop { from { transform: translateX(-50%) scale(0.7); opacity: 0; } }

  /* join box breathes while nobody has joined, then settles */
  #qrBox.waiting { animation: qrBreath 2.6s ease-in-out infinite alternate; }
  /* after the entrance, hand over to the breathing without losing either */
  body.anim #qrBox.waiting {
    animation: hStamp 620ms cubic-bezier(.2,1.5,.4,1) 520ms both,
               qrBreath 2.6s ease-in-out 1.15s infinite alternate;
  }
  @keyframes qrBreath {
    from { transform: translateY(-50%) rotate(-2.5deg) scale(1); }
    to { transform: translateY(-50%) rotate(-2.5deg) scale(1.045); }
  }

  #ovBody.count {
    font-size: 2.4em;
    font-weight: 700;
    color: var(--taxi);
    -webkit-text-stroke: 2px var(--ink);
    paint-order: stroke fill; /* stroke behind fill so the border never cuts into the glyphs */
    display: inline-block;
    animation: countSlam 420ms cubic-bezier(.2,1.7,.4,1);
  }
  @keyframes countSlam { from { transform: scale(2.1) rotate(-6deg); opacity: 0; } }
  /* waiting screen: logo floats on the dusk itself, no card behind it */
  #overlay.bare {
    background: none;
    border-color: transparent;
    box-shadow: none;
    color: var(--cream);
    text-shadow: 2px 2px 0 rgba(0,0,0,.35);
  }
  #overlay:not(.bare) #logoImg { display: none; } /* cream logo would vanish on the cream card */
  #overlay.bare #ovTitle { display: none; }

  /* ---- the end-of-round card ----------------------------------------------------------
   *
   * One card for every ending in every game. Slippy Shapes had it first — a primary button
   * with a draining ring and a LEAVE beside it — and the other four just counted themselves
   * down and carried on, giving the room no way to stop except closing the tab.
   *
   * The primary's LABEL is chosen by what just happened, not by which game:
   *   TRY AGAIN   the attempt failed
   *   CONTINUE    more of the run is still to come
   *   PLAY AGAIN  the whole run finished
   */
  #endRow { display: none; justify-content: center; margin-top: 16px; }
  #overlay.ending #endRow { display: flex; }
  /* the card owns the screen while it is up: nothing to misclick behind it */
  #overlay.ending #modeBtns, #overlay.ending #inviteRow { display: none !important; }
  #endBtns { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; align-items: center; }
  /* The count rides inside the button it belongs to, on the trailing edge: the thing that is
     about to happen and the thing you would press to make it happen now are one object. */
  #btnEndGo { display: inline-flex; align-items: center; gap: 12px; }
  /* Same size, so a thumb or a cursor finds either one. Only the primary is coloured,
     because only one of these two keeps the evening going. */
  #btnEndLeave { background: transparent; box-shadow: none; }
  #btnEndLeave:hover { background: rgba(34, 30, 58, 0.07); }
  /* the third way on (a game's own picker, the arena kept): a button, not the colour that keeps the evening going */
  #btnEndPick { background: var(--cream); }
  /* the reason line is a sentence, not a label: let it break where a sentence should */
  #overlay.ending #ovSub { text-wrap: balance; }

  /* one ring, two places: the offer to go again, and the wait to get back to the lobby */
  .cardRing { position: relative; width: 38px; height: 38px; flex: 0 0 38px; }
  .cardRing svg { width: 100%; height: 100%; transform: rotate(-90deg); display: block; }
  .cardRing circle { fill: none; stroke-width: 11; stroke-linecap: round; }
  .cardRing .track { stroke: rgba(34, 30, 58, 0.2); }
  .cardRing .fill { stroke: var(--ink); stroke-dasharray: 276.46; stroke-dashoffset: 0; }
  /* held: the arc goes hollow, so a number that has stopped moving reads as waiting for you
     rather than as a page that has died */
  #endRow.held #endRing .fill { stroke: rgba(34, 30, 58, 0.35); }
  /* nobody has answered the card in several full offers: stop counting at them */
  #endRow.expired #endRing { display: none; }
  .cardSecs {
    position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
    font-family: var(--display); font-size: 15px; font-weight: 700; color: var(--ink);
    font-variant-numeric: tabular-nums;
  }

  /* Pressing LEAVE used to hand back a card that sat there saying nothing, which reads
     exactly like a button that did not work. The press has an answer: the two buttons go,
     and this counts the room back to the lobby. */
  #ceremonyRow { display: none; align-items: center; justify-content: center; gap: 10px; margin-top: 16px; }
  #overlay.ceremony #ceremonyRow { display: flex; }
  #overlay.ceremony #modeBtns, #overlay.ceremony #inviteRow { display: none !important; }
  #ceremonyLabel {
    font-family: var(--display); font-size: 14px; font-weight: 700;
    letter-spacing: 0.04em; color: rgba(34, 30, 58, 0.6);
  }
  /* CLOSING is a wait, not a count. LEAVE opens the new arena immediately now and this
     holds only until it answers — usually a fraction of a second. A ring frozen at full
     would read as a countdown that has jammed, so it turns instead: the one shape that
     says "still going" without claiming to know how long. */
  #ceremonyRow.closing .cardSecs { display: none; }
  #ceremonyRow.closing #ceremonyRing .fill {
    stroke-dasharray: 70 210;
    animation: jlpClosing 900ms linear infinite;
  }
  @keyframes jlpClosing { to { transform: rotate(360deg); } }
  #ceremonyRow.closing #ceremonyRing svg { transform: rotate(-90deg); }
  #ceremonyRow.closing #ceremonyRing circle { transform-origin: 50% 50%; }
