
    /* The game's own mouse arrow: the classic shape, softened. Links
       and buttons keep the system hand; text fields keep the I-beam. */
    html, body { cursor: url("cursor.svg") 4 3, auto; }
    input, textarea, [contenteditable="true"] { cursor: text; }

    :root {
      /* Backgrounds */
      --tr-bg-deep: #050811;
      --tr-bg-main: #080d18;
      --tr-bg-elevated: #0d1422;

      /* Glass surfaces */
      --tr-glass-low: rgba(10, 16, 29, 0.78);
      --tr-glass-medium: rgba(11, 18, 32, 0.9);
      --tr-glass-strong: rgba(8, 13, 24, 0.97);

      /* Neon colors */
      --tr-cyan: #5fd4ff;
      --tr-cyan-bright: #9ce8ff;
      --tr-cyan-soft: rgba(95, 212, 255, 0.18);

      --tr-violet: #9874ff;
      --tr-violet-bright: #bea7ff;
      --tr-violet-dark: #5731c7;
      --tr-violet-soft: rgba(152, 116, 255, 0.18);

      --tr-green: #78e5b2;
      --tr-green-soft: rgba(120, 229, 178, 0.16);

      --tr-yellow: #ffd878;
      --tr-yellow-soft: rgba(255, 216, 120, 0.16);

      --tr-red: #ff6f82;
      --tr-red-dark: #a9233c;
      --tr-red-soft: rgba(255, 111, 130, 0.16);

      /* Text */
      --tr-text-primary: #f3f7ff;
      --tr-text-secondary: #b6c2d5;
      --tr-text-muted: #76849a;

      /* Borders */
      --tr-border-subtle: rgba(151, 184, 222, 0.12);
      --tr-border-cyan: rgba(95, 212, 255, 0.32);
      --tr-border-violet: rgba(152, 116, 255, 0.34);
      --tr-border-active: rgba(183, 153, 255, 0.58);

      /* Gradients */
      --tr-panel-gradient:
        radial-gradient(circle at 50% 0%, rgba(95, 212, 255, 0.055), transparent 42%),
        linear-gradient(180deg, rgba(13, 20, 35, 0.97), rgba(6, 11, 21, 0.99));
      --tr-panel-violet-gradient:
        radial-gradient(circle at 50% 0%, rgba(152, 116, 255, 0.11), transparent 46%),
        linear-gradient(180deg, rgba(14, 19, 35, 0.97), rgba(7, 10, 21, 0.99));
      --tr-control-gradient:
        linear-gradient(180deg, rgba(21, 30, 49, 0.96), rgba(9, 15, 28, 0.98));
      --tr-control-hover-gradient:
        linear-gradient(180deg, rgba(31, 43, 69, 0.98), rgba(14, 21, 39, 0.99));
      --tr-primary-gradient:
        linear-gradient(135deg, #a17cff 0%, #7550e8 48%, #4c2bb6 100%);
      --tr-primary-hover-gradient:
        linear-gradient(135deg, #b895ff 0%, #8965f5 48%, #5d38cd 100%);
      --tr-cyan-violet-gradient:
        linear-gradient(135deg, #63d8ff 0%, #8174ff 52%, #a06fff 100%);

      /* Shadows */
      --tr-shadow-panel:
        0 18px 48px rgba(0, 0, 0, 0.46),
        0 0 26px rgba(95, 212, 255, 0.055);
      --tr-shadow-violet:
        0 12px 32px rgba(0, 0, 0, 0.4),
        0 0 24px rgba(152, 116, 255, 0.13);
      --tr-shadow-cyan:
        0 12px 32px rgba(0, 0, 0, 0.4),
        0 0 24px rgba(95, 212, 255, 0.13);
      --tr-inset-highlight:
        inset 0 1px rgba(255, 255, 255, 0.055),
        inset 0 0 0 1px rgba(255, 255, 255, 0.018);

      /* Radius */
      --tr-radius-small: 10px;
      --tr-radius-medium: 16px;
      --tr-radius-large: 22px;
      --tr-radius-pill: 999px;

      /* Motion */
      --tr-transition-fast: 140ms ease;
      --tr-transition-standard: 190ms cubic-bezier(0.2, 0.8, 0.2, 1);

      --bg-deep: var(--tr-bg-deep);
      --bg-main: var(--tr-bg-main);
      --surface-1: var(--tr-glass-low);
      --surface-2: var(--tr-glass-medium);
      --surface-3: var(--tr-glass-strong);
      --border-soft: var(--tr-border-subtle);
      --border-medium: var(--tr-border-cyan);
      --border-accent: var(--tr-border-violet);
      --text-primary: var(--tr-text-primary);
      --text-secondary: var(--tr-text-secondary);
      --text-muted: var(--tr-text-muted);
      --accent-blue: var(--tr-cyan);
      --accent-blue-soft: var(--tr-cyan-bright);
      --accent-violet: var(--tr-violet);
      --accent-success: var(--tr-green);
      --accent-warning: var(--tr-yellow);
      --accent-danger: var(--tr-red);
      --shadow-panel: var(--tr-shadow-panel);
      --shadow-raised: 0 10px 24px rgba(0, 0, 0, 0.28);
      --glow-blue: 0 0 18px rgba(95, 212, 255, 0.10);
      --glow-violet: 0 0 16px rgba(152, 116, 255, 0.10);

      /* Panel frame: shared by the boards, the HOLD/NEXT cards and the score
         modules. Deliberately quiet -- depth comes from the bevel and shadow,
         never from a glow. --panel-accent is only used on state changes. */
      --panel-edge: rgba(151, 184, 222, 0.13);
      --panel-edge-hi: rgba(151, 184, 222, 0.22);
      --panel-accent: rgba(95, 212, 255, 0.34);
    }
    * { box-sizing: border-box; }
    html, body { height: 100%; }
    html { -webkit-text-size-adjust: 100%; }
    body {
      margin: 0;
      font-family: Arial, Helvetica, sans-serif;
      background:
        radial-gradient(circle at 50% 8%, rgba(63, 78, 130, 0.14) 0%, transparent 38%),
        radial-gradient(circle at 50% 72%, rgba(44, 95, 108, 0.07) 0%, transparent 45%),
        linear-gradient(180deg, #111522 0%, var(--bg-main) 45%, var(--bg-deep) 100%);
      color: var(--text-primary);
      display: grid;
      place-items: center;
      overflow: hidden;
    }
    
    /* .vote-actions styled the two ban-vote buttons that used to sit
       on every scoreboard row. They are gone, and nothing else in the
       page carries the class. */

    #pumpfunBackground {
      position: fixed;
      inset: 0;
      pointer-events: none;
      overflow: hidden;
      z-index: 0;
    }
    .pumpfun-float {
      position: absolute;
      left: 0;
      top: 0;
      will-change: transform;
      user-select: none;
      -webkit-user-drag: none;
      filter: drop-shadow(0 0 6px rgba(105, 214, 154, 0.16));
      opacity: 0.92;
    }
    .app, .account-chip, .music-flyout, .controls-panel, .profile-panel, .reward-distribution-panel {
      position: relative;
      z-index: 1;
    }
    body, .app, .reward-distribution-panel, .winners-panel, .controls-panel, .profile-panel {
      padding-top: env(safe-area-inset-top);
      padding-right: env(safe-area-inset-right);
      padding-bottom: env(safe-area-inset-bottom);
      padding-left: env(safe-area-inset-left);
    }

    .controls-visibility-toggle{
        display: none;
    }

    .app {
      width: 100%;
      min-height: 100vh;
      min-height: 100dvh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px 12px;
      margin: 0;
      box-sizing: border-box;
    }
    .app,
    .controls-panel,
    .profile-panel,
    .user-profile-panel,
    .reward-distribution-panel,
    .winners-panel,
    .security-alert-card,
    .welcome-card,
    .music-popover,
    .game-chat-card,
    .game-timer-banner,
    .match-status-overlay,
    .scoreboard-panel-inner,
    .reward-panel,
    .controls-card,
    .profile-card,
    .winners-card,
    .change-password-card {
      color: var(--tr-text-primary);
    }
    .controls-card,
    .profile-card,
    .user-profile-card,
    .winners-card,
    .reward-panel,
    .game-chat-card,
    .scoreboard-panel-inner,
    .music-popover,
    .change-password-card,
    .security-alert-card,
    .player-inspect-card,
    .replay-card,
    .reward-distribution-panel .winners-card,
    .welcome-card {
      position: relative;
      overflow: hidden;
      background: var(--tr-panel-gradient);
      border: 1px solid var(--tr-border-subtle);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }
    .controls-card::before,
    .profile-card::before,
    .user-profile-card::before,
    .winners-card::before,
    .reward-panel::before,
    .game-chat-card::before,
    .scoreboard-panel-inner::before,
    .music-popover::before,
    .change-password-card::before,
    .security-alert-card::before,
    .player-inspect-card::before,
    .replay-card::before,
    .reward-distribution-panel .winners-card::before,
    .welcome-card::before {
      content: "";
      position: absolute;
      top: 0;
      left: 12%;
      right: 12%;
      height: 1px;
      pointer-events: none;
      background: linear-gradient(90deg, transparent, rgba(95, 212, 255, 0.65), rgba(152, 116, 255, 0.65), transparent);
      opacity: 0.68;
    }
    .game-layout {
      display: flex;
      align-items: start;
      justify-content: center;
      gap: 32px;
      width: max-content;
      max-width: 100%;
      margin: 0 auto;
    }
    .board-side {
      display: flex;
      align-items: flex-start;
      gap: 12px;
      flex: none;
      --side-board-offset: 78px;
      padding-top: var(--side-board-offset);
    }
    .board-side + .board-side {
      position: relative;
      padding-left: 24px;
    }
    .board-side + .board-side::before {
      content: "";
      position: absolute;
      left: 0;
      top: var(--side-board-offset, 78px);
      bottom: 0;
      width: 4px;
      border-radius: 999px;
      background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(111, 176, 255, 0.35), rgba(255,255,255,0.02));
      box-shadow: 0 0 8px rgba(111, 176, 255, 0.16);
      pointer-events: none;
    }
    .hold-column,
    .next-column {
      display: flex;
      flex-direction: column;
      gap: 10px;
      width: 112px;
      align-self: start;
      justify-self: start;
      margin: 0;
      padding: 0;
    }
    .main-column {
      display: grid;
      width: max-content;
      min-width: 0;
      gap: 10px;
      justify-items: center;
      align-content: start;
    }
    .main-column.f2main-column {
      width: max-content;
      justify-items: center;
    }
    
    .f2main-column {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
    }

    .top-bar {
      width: 100%;
      display: flex;
      justify-content: flex-end;
      align-items: center;
    }

    
    @keyframes rewardPulse {
      0%, 100% { box-shadow:
        0 0 0 1px rgba(125, 51, 255, 0.12) inset,
        0 18px 50px rgba(0, 0, 0, 0.45),
        0 0 32px rgba(37, 183, 255, 0.08);
        transform: translateX(-50%) scale(1);
      }
      45% { box-shadow:
        0 0 0 1px rgba(56, 160, 255, 0.32) inset,
        0 18px 64px rgba(0, 0, 0, 0.58),
        0 0 58px rgba(37, 183, 255, 0.26);
        transform: translateX(-50%) scale(1.04);
      }
    }
    @keyframes rewardShake {
      0%, 100% { filter: brightness(1); }
      25% { filter: brightness(1.08); }
      50% { filter: brightness(1.16); }
      75% { filter: brightness(1.08); }
    }
    .reward-panel {
      width: auto;
      border-radius: 20px;
      padding: 12px 14px;
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.96), rgba(9, 14, 24, 0.98));
      border: 1px solid rgba(105, 214, 154, 0.18);
      box-shadow:
        0 0 0 1px rgba(155, 125, 255, 0.12) inset,
        0 18px 50px rgba(0, 0, 0, 0.45),
        0 0 32px rgba(105, 214, 154, 0.08);
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      box-sizing: border-box;
      cursor: pointer;
      transition: transform .18s ease, filter .18s ease, box-shadow .18s ease;
    }
    .reward-panel.compact {
      width: auto;
      min-width: 280px;
    }
    .reward-panel.top-total {
      position: static;
      width: auto;
      min-width: 150px;
      align-items: stretch;
    }
    .reward-panel:hover {
      transform: translateY(-3px) scale(1.02);
      filter: brightness(1.08);
      box-shadow:
        0 0 0 1px rgba(155, 125, 255, 0.18) inset,
        0 20px 44px rgba(0, 0, 0, 0.45),
        0 0 30px rgba(105, 214, 154, 0.14);
    }
    .reward-panel:active {
      transform: translateY(-1px) scale(1.01);
      filter: brightness(1.12);
    }
    .reward-title {
      font-size: 11px;
      font-weight: 900;
      letter-spacing: .18em;
      color: var(--muted);
      text-transform: uppercase;
    }
    .reward-value {
      display: flex;
      align-items: center;
      gap: 10px;
      font-size: 24px;
      font-weight: 900;
      color: #f5f4ef;
      white-space: nowrap;
    }
    .reward-value img {
      width: 26px;
      height: 26px;
      display: block;
    }

    .liveboards-shell { display: none; }

    .game-column {
      display: grid;
      gap: 14px;
      justify-items: center;
      align-content: start;
    }
    .play-shell { justify-self: start; }
    .score-row {
      display: flex;
      justify-content: flex-start;
      width: 100%;
    }
    .piece-stack {
      align-self: start;
      justify-self: end;
      display: grid;
      gap: 14px;
      width: 124px;
    }
    .score-chip {
      display: inline-flex;
      align-items: center;
      gap: 10px;
      padding: 12px 14px;
      border-radius: 18px;
      background: linear-gradient(180deg, rgba(20, 27, 39, 0.99), rgba(12, 17, 26, 0.99));
      border: 1px solid var(--border-soft);
      box-shadow: 0 1px 0 rgba(255, 255, 255, 0.025) inset, 0 12px 28px rgba(0, 0, 0, 0.30);
      transform: translateY(10px);
      margin-top: 0px;
      border-color: #7dd3fc;
      transition: border-color 200ms ease, box-shadow 200ms ease;
    }
    .score-chip-roundbox {
      display: grid;
      place-items: center;
      min-width: 74px;
      min-height: 72px;
      padding: 8px 10px;
      border-radius: 14px;
      background: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012));
      border: 1px solid rgba(145, 165, 195, 0.12);
      box-shadow: none;
    }
    .score-chip-main,
    .score-chip-rankbox,
    .score-chip-roundbox {
      display: grid;
      place-items: center;
      min-width: 72px;
      min-height: 72px;
      padding: 8px 10px;
      border-radius: 14px;
      background: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012));
      border: 1px solid rgba(145, 165, 195, 0.12);
      box-shadow: none;
      transition: border-color 220ms ease, box-shadow 220ms ease;
    }
    /* .player-lines-sent used to add `align-content: center`, which packed the
       label and value together and left the air at the top and bottom instead
       of between them -- the opponent module, without it, let the rows stretch
       and breathe. Both sides now share the plain .score-chip-main layout.
       `position: relative` here anchored the old in-box help button, which has
       since moved to the controls flyout. */
    .score-chip-main { min-width: 120px; }
    .score-chip-rankbox { min-width: 88px; position: relative; }

    /* ---- rank protection, shown on the rank it protects ----
       Bottom-right of the rank box, small and quiet until it is
       needed. It is a property of the rank rather than a stat of its
       own, so it sits on the box instead of beside it. */
    .rank-shield {
      position: absolute;
      right: 4px;
      bottom: 2px;
      width: 22px;
      height: 22px;
      display: grid;
      place-items: center;
      color: #9fc0e4;
      pointer-events: none;
    }
    .rank-shield[hidden] { display: none; }
    .rank-shield svg {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
    }
    .rank-shield .rs-body {
      stroke: currentColor;
      stroke-width: 2;
      fill: rgba(12, 18, 30, 0.85);
    }
    .rank-shield-count {
      position: relative;
      /* nudged up: the shield's point takes the bottom third, so a
         centred glyph reads low inside it */
      margin-top: -2px;
      font-size: 10px;
      font-weight: 900;
      line-height: 1;
      color: #cfe0f2;
    }
    /* The in-place flare that used to live here is gone. The absorb
       is a full-screen sequence now -- see .rank-shield-ghost -- and
       two animations on the same shield would have played over each
       other, one in the chip and one in the middle of the board. */
      .score-chip-main .label,
      .score-chip-rankbox .label,
      .score-chip-roundbox .label { margin: 0; font-size: 9px; color: var(--text-muted); }
      .score-chip-roundbox .value,
      .score-chip-main .value {
        margin: 0;
        font-size: 25px;
        line-height: 1;
        display: inline-block;
        transform-origin: 50% 60%;
        will-change: transform;
      }
    .score-chip-rank { margin-top: -2px; font-size: 35px; font-weight: 900; letter-spacing: .12em; color: var(--text-primary); text-transform: uppercase; line-height: 1; text-shadow: none; }
      .score-chip-rankbox .score-chip-rank { margin-top: 0; }
    /* =========================================================
       PLAYERS PANEL
       Fixed to the right edge rather than added as a third column, so the
       two centred boards keep their position at every width. It slides out
       on its own tab, which stays on screen when collapsed; the open/closed
       choice is remembered, and it starts closed below 1500px where the
       boards already use most of the width.
       ========================================================= */
    .players-panel {
      position: fixed;
      top: 14px;
      bottom: calc(14px + env(safe-area-inset-bottom, 0px));
      right: 14px;
      z-index: 38;
      /* 272 until the rank and stars moved into the row. Twenty more
         so the nickname keeps most of the width it had rather than
         paying for the new column on its own -- the panel is fixed
         and overlays the page, so this costs nothing but the chat
         launcher's offset, which follows it through --players-dock. */
      width: 292px;
      display: flex;
      flex-direction: column;
      border: 1px solid var(--panel-edge);
      border-radius: 14px;
      background:
        radial-gradient(120% 60% at 50% 0%, rgba(95, 212, 255, 0.05), transparent 62%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.97), rgba(9, 13, 22, 0.99));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        inset 0 0 0 1px rgba(0, 0, 0, 0.32),
        0 16px 40px rgba(0, 0, 0, 0.42);
      backdrop-filter: blur(6px);
      -webkit-backdrop-filter: blur(6px);
      transition: transform .28s cubic-bezier(.2, .85, .3, 1);
    }
    /* the tab sits outside the left edge, so it rides back into view as the
       panel slides off */
    .players-panel.collapsed {
      transform: translateX(calc(100% + 14px));
    }
    .players-panel-tab {
      position: absolute;
      left: -26px;
      top: 50%;
      transform: translateY(-50%);
      width: 26px;
      height: 58px;
      padding: 0;
      display: grid;
      place-items: center;
      cursor: pointer;
      border: 1px solid var(--panel-edge);
      border-right: 0;
      border-radius: 10px 0 0 10px;
      background: linear-gradient(180deg, rgba(20, 27, 42, 1), rgba(10, 15, 24, 1));
      box-shadow: -6px 0 16px rgba(0, 0, 0, 0.34);
      color: rgba(207, 228, 245, 0.72);
      font-size: 15px;
      line-height: 1;
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .players-panel-tab:hover {
      color: #eaf6ff;
      border-color: var(--panel-edge-hi);
    }
    .players-panel-tab span {
      display: block;
      transition: transform .28s cubic-bezier(.2, .85, .3, 1);
    }
    .players-panel.collapsed .players-panel-tab span { transform: rotate(180deg); }
    .players-panel-head {
      position: relative;
      flex: 0 0 auto;
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 8px;
      padding: 13px 14px 11px;
      border-bottom: 1px solid var(--panel-edge);
    }
    .players-panel-head::after {
      content: "";
      position: absolute;
      top: 0;
      left: 14px;
      right: 14px;
      height: 1px;
      background: linear-gradient(90deg, transparent, rgba(95, 212, 255, 0.45), transparent);
      opacity: .75;
    }
    .players-panel-title {
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: #7fd2f5;
    }
    .players-panel-count {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .08em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .players-online-dot {
      width: 7px;
      height: 7px;
      border-radius: 50%;
      background: #69d69a;
      box-shadow: 0 0 6px rgba(105, 214, 154, 0.5);
    }
    /* Search by nickname, between the head and the list */
    .players-search {
      flex: 0 0 auto;
      position: relative;
      display: block;
      margin: 9px 9px 0;
    }
    .players-search svg {
      position: absolute;
      left: 10px;
      top: 50%;
      width: 15px;
      height: 15px;
      transform: translateY(-50%);
      fill: none;
      stroke: var(--tr-text-muted);
      stroke-width: 2;
      stroke-linecap: round;
      pointer-events: none;
    }
    .players-search input {
      width: 100%;
      height: 34px;
      padding: 0 10px 0 32px;
      border-radius: 10px;
      border: 1px solid var(--panel-edge);
      background: rgba(6, 11, 21, 0.55);
      color: var(--tr-text-primary);
      font: inherit;
      font-size: 13px;
      outline: none;
      transition: border-color var(--tr-transition-fast);
    }
    .players-search input::placeholder { color: var(--tr-text-muted); }
    .players-search input:focus { border-color: var(--tr-border-cyan); }
    body.is-mobile .players-search input { font-size: 16px; height: 36px; }
    .players-panel-list {
      flex: 1 1 auto;
      min-height: 0;
      overflow-y: auto;
      overscroll-behavior: contain;
      padding: 9px;
      display: grid;
      gap: 5px;
      align-content: start;
    }
    .players-panel-list::-webkit-scrollbar { width: 6px; }
    .players-panel-list::-webkit-scrollbar-track { background: transparent; }
    .players-panel-list::-webkit-scrollbar-thumb {
      border-radius: 999px;
      background: rgba(151, 184, 222, 0.18);
    }
    .players-panel-list::-webkit-scrollbar-thumb:hover { background: rgba(151, 184, 222, 0.3); }
    /* flag, name + address, rank + stars, presence dot. The rank used to
       lead the row and was taken out for it: a number in front of every
       flag reads as a list index rather than as a rank, and it cost the
       nickname width. It is on the right now, and sized by the star pips
       rather than by the digits, so a rank 100 row is no wider than a
       rank 7 one. */
    .players-row {
      display: grid;
      grid-template-columns: 28px 1fr 30px 10px;
      align-items: center;
      gap: 10px;
      padding: 9px 10px;
      border-radius: 10px;
      border: 1px solid transparent;
      /* the energy bar hangs off the bottom edge rather than taking a
         column; see playersPanelEnergy for why it is not one */
      position: relative;
      transition: background var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    /* The board's cell again, at list size, riding on the address
       line. Fifty of these are on screen at once, so it carries no
       number and sits at a lower opacity than the two larger ones --
       it is a reading to glance across, not one to study. */
    .players-idline {
      display: flex;
      align-items: center;
      gap: 6px;
      min-width: 0;
    }
    /* the address gives up the width, having been truncated anyway */
    .players-idline .players-address { flex: 1 1 auto; min-width: 0; }
    .players-energy {
      flex: none;
      display: inline-flex;
      align-items: center;
      pointer-events: none;
      /* the shared ramp; the fallback is for a row whose energy is
         not known, which is a neutral reading rather than a bright
         one standing in for a figure nobody has */
      --cell: var(--energy-tint, rgba(151, 184, 222, 0.55));
    }
    .players-energy-cell {
      position: relative;
      width: 28px;
      height: 9px;
      border-radius: 3px;
      border: 1px solid var(--cell);
      background: rgba(6, 11, 21, 0.7);
      overflow: hidden;
      opacity: .8;
    }
    .players-energy-fill {
      position: absolute;
      inset: 0 auto 0 0;
      background: var(--cell);
      opacity: .5;
      transition: width var(--tr-transition-standard);
    }
    .players-energy-nub {
      flex: none;
      width: 2px;
      height: 4px;
      margin-left: 1px;
      border-radius: 0 1px 1px 0;
      background: var(--cell);
      opacity: .7;
    }
    .players-row.is-online { background: rgba(255, 255, 255, 0.022); }
    .players-row.is-me {
      border-color: rgba(95, 212, 255, 0.26);
      background: rgba(95, 212, 255, 0.055);
    }
    /* Every row opens that player's card, so every row says so. The
       lift is one step of background and a hairline border -- the same
       two properties the row already transitions, and the same pair
       is-me uses at a lower strength, so a hovered row reads as
       "pickable" rather than as a third state of its own.

       Offline rows included. They are the rows somebody is most
       likely to be looking somebody up from, and a list where only
       some rows answered a click would be the more confusing rule. */
    .players-row { cursor: pointer; }
    .players-row:hover {
      background: rgba(255, 255, 255, 0.055);
      border-color: rgba(151, 184, 222, 0.22);
    }
    .players-row.is-me:hover {
      background: rgba(95, 212, 255, 0.085);
      border-color: rgba(95, 212, 255, 0.34);
    }
    /* The Watch button is its own target inside the row and keeps its
       own hover; without this the row's cursor would claim it. */
    .players-row .players-watch { cursor: pointer; }
    .players-row .players-watch.is-disabled { cursor: default; }
    .players-flag {
      width: 28px;
      height: 19px;
      border-radius: 4px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.04);
    }
    .players-flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .players-meta { min-width: 0; display: grid; gap: 2px; }
    .players-name {
      font-size: 13px;
      font-weight: 700;
      color: var(--tr-text-muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .players-row.is-online .players-name { color: #e4eef8; }
    .players-address {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 10px;
      letter-spacing: .01em;
      color: rgba(151, 184, 222, 0.5);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    /* The rank over its stars, right aligned so the digits line up down
       the list and the eye can read the column as a ladder. Quiet by
       default: this is fifty rows of it, and the panel is something you
       glance at rather than study. */
    .players-score {
      display: grid;
      justify-items: end;
      gap: 3px;
      min-width: 30px;
    }
    .players-rank {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      font-weight: 700;
      line-height: 1;
      color: rgba(151, 184, 222, 0.55);
    }
    .players-row.is-online .players-rank { color: rgba(196, 220, 244, 0.9); }
    .players-stars { display: flex; align-items: center; gap: 2px; height: 4px; }
    .players-pip {
      width: 4px;
      height: 4px;
      border-radius: 50%;
      background: rgba(151, 184, 222, 0.22);
    }
    /* the scoreboard's own star gold, so a row here and the chip at the
       top of a match are obviously the same thing */
    .players-pip.is-on { background: #ffc860; }
    /* At the ceiling: one purple pip and a count, matching the score row
       and the RANK module's violet rather than adding a third accent. */
    .players-score.is-capped .players-stars { gap: 3px; height: auto; }
    .players-score.is-capped .players-pip.is-on { background: #b79cff; }
    .players-star-total {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 9px;
      font-weight: 700;
      line-height: 1;
      color: #dcd0ff;
    }
    /* the only difference between an online and an offline row that has to
       survive a glance */
    .players-status {
      width: 9px;
      height: 9px;
      border-radius: 50%;
      background: rgba(151, 184, 222, 0.2);
    }
    .players-row.is-online .players-status {
      background: #69d69a;
      box-shadow: 0 0 6px rgba(105, 214, 154, 0.45);
    }
    /* A third presence state, on the same dot rather than beside it.
       Violet because cyan already means "this row is you" two rules
       up, and green means online -- a player mid-round is a different
       thing from both and should not have to borrow either colour. */
    .players-row.is-inmatch .players-status {
      background: var(--tr-violet);
      box-shadow: 0 0 6px rgba(152, 116, 255, 0.5);
    }
    /* A fourth state on the same dot. Amber because green already
       means online and violet means mid-round, and the muted gold the
       mobile notice heading uses keeps it on palette instead of
       introducing a bright one. It pulses on the live dot's own curve:
       a search is a thing in progress and the pulse says so, which
       leaves the colour free to say only which state it is. */
    .players-row.is-finding .players-status {
      background: #e0b872;
      box-shadow: 0 0 6px rgba(224, 184, 114, 0.45);
      animation: playersLivePulse 1.9s ease-in-out infinite;
    }
    .players-row.is-finding {
      border-color: rgba(224, 184, 114, 0.18);
      background: rgba(224, 184, 114, 0.04);
    }
    .players-row.is-inmatch {
      border-color: rgba(152, 116, 255, 0.22);
      background: rgba(152, 116, 255, 0.05);
      /* `gap: 10px` on the row governs both axes; the second line
         wants less air above it than the columns want between them */
      row-gap: 6px;
    }
    /* ...and the row that says so in words, on a line of its own.
       The panel is 292px wide: a pill and a button squeezed in beside
       the flag, the name, the address and the rank would leave the
       nickname about sixty pixels, and the nickname is what somebody
       is scanning for. Only the handful of rows mid-round pay the
       extra height. */
    .players-live {
      grid-column: 1 / -1;
      display: flex;
      align-items: center;
      gap: 7px;
      min-width: 0;
    }
    .players-live-pill {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      flex: none;
      padding: 3px 8px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid rgba(152, 116, 255, 0.3);
      background: var(--tr-violet-soft);
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-violet-bright);
    }
    .players-live-dot {
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: var(--tr-violet-bright);
      animation: playersLivePulse 1.9s ease-in-out infinite;
    }
    /* Opacity only. A pulsing scale or a spreading shadow would be an
       arcade tell; this is the quiet "still running" signal the rest
       of the panel uses. */
    @keyframes playersLivePulse {
      0%, 100% { opacity: 1; }
      50% { opacity: .35; }
    }
    .players-live-vs {
      min-width: 0;
      flex: 1 1 auto;
      font-size: 10px;
      color: rgba(151, 184, 222, 0.62);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .players-watch {
      flex: none;
      margin-left: auto;
      padding: 4px 10px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-control-gradient);
      box-shadow: var(--tr-inset-highlight);
      color: var(--tr-text-secondary);
      font: inherit;
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .06em;
      cursor: pointer;
      transition:
        color var(--tr-transition-standard),
        border-color var(--tr-transition-standard),
        background var(--tr-transition-standard);
    }
    button.players-watch:hover {
      color: var(--tr-text-primary);
      border-color: var(--tr-border-violet);
      background: var(--tr-control-hover-gradient);
    }
    /* Not a button at all when it cannot be pressed -- it is rendered
       as a span -- so this only has to look inert. */
    .players-watch.is-disabled {
      opacity: .38;
      cursor: default;
    }
    .players-empty {
      padding: 20px 12px;
      text-align: center;
      font-size: 11px;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    @media (prefers-reduced-motion: reduce) {
      .players-panel,
      .players-panel-tab span { transition: none; }
    }
    /* The game chat docks to the players panel rather than to the viewport: it sits
       in the bottom-right corner while the list is collapsed, and slides clear of it
       while the list is open. `players-open` is put on <body> by the same code that
       collapses the panel, so the two move together.
       The dock is the panel's own right gutter (14px) + its width (292px) + a gap.
       Both numbers move together: widening the panel without this leaves the
       chat launcher sitting under it. */
    :root {
      --game-chat-gutter: 16px;
      --players-dock: 0px;
    }
    body.players-open { --players-dock: 318px; }
    /* no room beside the boards once the layout is stacked; with the list gone the
       chat returns to the corner, so the dock has to be cancelled here too */
    @media (orientation: portrait), (max-width: 900px) {
      .players-panel { display: none; }
      body.players-open { --players-dock: 0px; }
    }

    /* ---- board help buttons: "how scoring works" and "how to play" ----
       These were a 40px violet circle holding a "?" sitting next to a 100px
       PNG, so they read as two unrelated controls stacked on the board edge.
       They are now a matched 44px pair on the same panel frame as the rest of
       the chrome, with inline SVG glyphs instead of a bitmap: sharp at any
       size, recoloured from the theme, and one fewer image request. A star
       for scoring, which is the same shape as the rating stars above the
       board, and a gamepad for controls. */
    .score-help,
    .controls-icon {
      position: relative;
      width: 44px;
      height: 44px;
      padding: 0;
      display: grid;
      place-items: center;
      cursor: pointer;
      border: 1px solid var(--panel-edge);
      border-radius: 12px;
      background:
        radial-gradient(120% 86% at 50% 0%, var(--help-wash), transparent 64%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.99), rgba(9, 13, 22, 1));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 8px 20px rgba(0, 0, 0, 0.28);
      color: var(--help-icon);
      transition:
        border-color var(--tr-transition-standard),
        box-shadow var(--tr-transition-standard),
        color var(--tr-transition-standard),
        filter var(--tr-transition-standard),
        translate var(--tr-transition-fast);
    }
    .score-help {
      --help-icon: #ffc860;
      --help-edge: rgba(255, 200, 96, 0.34);
      --help-wash: rgba(255, 200, 96, 0.06);
    }
    .controls-icon {
      --help-icon: #7fd2f5;
      --help-edge: rgba(95, 212, 255, 0.34);
      --help-wash: rgba(95, 212, 255, 0.06);
    }
    .score-help svg,
    .controls-icon svg {
      width: 21px;
      height: 21px;
      display: block;
      pointer-events: none;
      /* shifted off centre to clear the question mark in the corner */
      transform: translate(-2px, -2px);
    }
    /* The question mark is what says these open help rather than act on the
       game. It sits in the corner as a badge so the subject glyph still reads
       at a glance, with a dark ring so it punches cleanly out of whatever
       part of the glyph is behind it. */
    .help-mark {
      position: absolute;
      right: 3px;
      bottom: 3px;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-family: Arial, Helvetica, sans-serif;
      font-size: 11px;
      font-weight: 900;
      line-height: 1;
      color: var(--help-icon);
      background: linear-gradient(180deg, rgba(16, 22, 34, 1), rgba(9, 13, 22, 1));
      border: 1px solid var(--help-edge);
      box-shadow: 0 0 0 2px rgba(9, 13, 22, 0.94);
      pointer-events: none;
    }
    /* the star is solid and the gamepad is drawn in line, weighted so the two
       glyphs read at the same strength side by side */
    .score-help svg path {
      fill: currentColor;
      stroke: rgba(0, 0, 0, 0.32);
      stroke-width: 0.75;
      stroke-linejoin: round;
    }
    .controls-icon svg .pad-body,
    .controls-icon svg .pad-dpad {
      fill: none;
      stroke: currentColor;
      stroke-width: 1.6;
      stroke-linejoin: round;
      stroke-linecap: round;
    }
    .controls-icon svg .pad-key {
      fill: currentColor;
      stroke: none;
    }
    .score-help:hover,
    .controls-icon:hover {
      border-color: var(--help-edge);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.05),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 10px 24px rgba(0, 0, 0, 0.32);
    }
    .score-chip-main .value.stat-pop {
      animation: statPopLines 340ms cubic-bezier(.2,.9,.25,1) both;
    }
    .score-chip-roundbox .value.stat-pop {
      animation: statPopKO 340ms cubic-bezier(.2,.9,.25,1) both;
    }
    /* colour is left undeclared at 0%/100% so these animate from and back to
       the module's own --module-value tint instead of snapping to white */
    @keyframes statPopLines {
      0% { transform: scale(1); text-shadow: none; }
      30% { transform: scale(1.42); color: var(--tr-cyan-bright); text-shadow: 0 0 10px rgba(95, 212, 255, 0.55), 0 0 18px rgba(95, 212, 255, 0.28); }
      60% { transform: scale(0.96); color: var(--tr-cyan-bright); text-shadow: 0 0 8px rgba(95, 212, 255, 0.35); }
      100% { transform: scale(1); text-shadow: none; }
    }
    @keyframes statPopKO {
      0% { transform: scale(1) rotate(0deg); text-shadow: none; }
      25% { transform: scale(1.5) rotate(-3deg); color: var(--tr-yellow); text-shadow: 0 0 10px rgba(255, 216, 120, 0.6), 0 0 18px rgba(255, 216, 120, 0.3); }
      55% { transform: scale(0.94) rotate(2deg); color: var(--tr-yellow); text-shadow: 0 0 8px rgba(255, 216, 120, 0.34); }
      100% { transform: scale(1) rotate(0deg); text-shadow: none; }
    }
    @media (prefers-reduced-motion: reduce) {
      .score-chip-main .value.stat-pop,
      .score-chip-roundbox .value.stat-pop {
        animation: none;
      }
    }
    /* The score chip used to recolour its whole border per match phase
       (cyan idle / yellow countdown / green active). Removed -- the chip keeps
       one neutral edge and the colour lives in the individual modules instead.
       setMatchBoardPhase() still writes data-match-phase; nothing styles it now. */
    .account-chip {
      position: fixed;
      top: 16px;
      left: 16px;
      z-index: 40;
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 10px 14px;
      border: 0;
      border-radius: 20px;
      background:
        radial-gradient(circle at 20% 0%, rgba(95, 212, 255, 0.08), transparent 46%),
        var(--tr-control-gradient);
      color: var(--tr-text-primary);
      border: 1px solid var(--tr-border-subtle);
      box-shadow:
        var(--tr-inset-highlight),
        0 10px 28px rgba(0, 0, 0, 0.4),
        0 0 20px rgba(95, 212, 255, 0.055);
      cursor: pointer;
      overflow: hidden;
      transition: background var(--tr-transition-standard), border-color var(--tr-transition-standard), box-shadow var(--tr-transition-standard), filter var(--tr-transition-standard), opacity var(--tr-transition-standard);
      display: flex;
      opacity: 1;
    }
    /* No position of its own any more.

       This used to be pinned at top:170px/left:16px with five
       breakpoints re-deriving that top from the chip stack above
       it. The lobby copy now sits in .bottom-left-dock beside the
       audio button and is laid out by it; the dock's own scaling
       covers every one of those breakpoints at once.

       The class is also on the Proceed button inside the spin
       modal, which was inheriting all of that -- a button in the
       middle of a dialog being told to pin itself to the top left
       of the window. Being static is what that one wanted all
       along. */
    .daily-spin-btn {
      border: 1px solid var(--tr-border-subtle);
      border-radius: 16px;
      padding: 10px 14px;
      align-items: center;
      background:
        radial-gradient(circle at 20% 0%, rgba(95, 212, 255, 0.08), transparent 46%),
        var(--tr-control-gradient);
      color: var(--tr-text-primary);
      font-weight: 900;
      letter-spacing: .08em;
      text-transform: uppercase;
      box-shadow:
        var(--tr-inset-highlight),
        0 10px 28px rgba(0, 0, 0, 0.4),
        0 0 20px rgba(95, 212, 255, 0.055);
      cursor: pointer;
      display: inline-flex;
      opacity: 1;
    }
    .daily-spin-btn:hover {
      background:
        radial-gradient(circle at 25% 0%, rgba(152, 116, 255, 0.13), transparent 50%),
        var(--tr-control-hover-gradient);
      border-color: var(--tr-border-violet);
      box-shadow:
        var(--tr-inset-highlight),
        0 13px 34px rgba(0, 0, 0, 0.46),
        0 0 25px rgba(152, 116, 255, 0.15),
        0 0 10px rgba(95, 212, 255, 0.08);
      filter: brightness(1.06);
    }
    .daily-spin-btn,
    .daily-spin-btn:hover {
      border-color: var(--tr-border-subtle);
    }
    /* A nub hanging off the bottom of the account chip rather than a second
       floating pill. The account chip is 64px tall (42px flag + 20px padding +
       2px border) at 16px from the top, so its bottom edge is at 80px; the nub
       starts 2px above that so no hairline gap can show, and sits behind the
       chip so the overlap reads as one stacked control.
       Every `top` in the scaled breakpoints below is the same arithmetic:
       chip top + (64 - 2) * scale. */
    .holdings-chip {
      position: fixed;
      left: 16px;
      z-index: 39;
      display: inline-flex;
      align-items: center;
      gap: 9px;
      padding: 8px 8px 8px 11px;
      border: 1px solid var(--tr-border-subtle);
      border-radius: 4px 4px 14px 14px;
      background:
        radial-gradient(circle at 20% 0%, rgba(95, 212, 255, 0.08), transparent 46%),
        var(--tr-control-gradient);
      color: var(--tr-text-primary);
      box-shadow:
        var(--tr-inset-highlight),
        0 10px 24px rgba(0, 0, 0, 0.38);
      overflow: hidden;
      transition: background var(--tr-transition-standard), border-color var(--tr-transition-standard), box-shadow var(--tr-transition-standard), filter var(--tr-transition-standard), opacity var(--tr-transition-standard);
      opacity: 1;
    }
    .holdings-wallet {
      flex: 0 0 auto;
      width: 17px;
      height: 17px;
      color: rgba(151, 184, 222, 0.72);
    }
    /* A second nub under the holdings one, carrying this player's
       running SOL. Same shape and same 2px overlap, so the chip and
       the two nubs read as one stacked control. The wallet is the
       green one from the Earnings stat on the profile panel. */
    .earnings-chip {
      position: fixed;
      left: 16px;
      z-index: 38;
      box-sizing: border-box;
      min-height: 44px;
      display: inline-flex;
      align-items: center;
      gap: 9px;
      padding: 8px 13px;
      border: 1px solid var(--tr-border-subtle);
      border-radius: 4px 4px 14px 14px;
      background:
        radial-gradient(circle at 20% 0%, rgba(120, 229, 178, 0.07), transparent 46%),
        var(--tr-control-gradient);
      color: var(--tr-text-primary);
      box-shadow:
        var(--tr-inset-highlight),
        0 10px 24px rgba(0, 0, 0, 0.38);
      transition: border-color var(--tr-transition-standard), box-shadow var(--tr-transition-standard);
    }
    .earnings-wallet {
      flex: 0 0 auto;
      width: 17px;
      height: 17px;
      color: var(--tr-green);
    }
    .earnings-value {
      font-size: 15px;
      font-weight: 900;
      line-height: 1;
      letter-spacing: .04em;
      color: #f5f4ef;
      white-space: nowrap;
      /* the figure ticks up digit by digit, so it must not reflow
         while it does */
      font-variant-numeric: tabular-nums;
    }
    .earnings-sol { flex: 0 0 auto; width: 14px; height: 14px; display: block; }
    /* The gain, floated off the right-hand edge. Off the right and not
       above because the holdings nub overlaps this one by 2px and sits
       a z-index higher; this nub's own z-index makes it a stacking
       context, so a child can never paint over its neighbour however
       high it is raised. Being a child is what keeps it attached
       through every responsive reposition and scale of the stack. */
    .earnings-delta {
      position: absolute;
      left: calc(100% + 9px);
      top: 50%;
      /* centred on the nub when it is standing still, which is where
         reduced motion leaves it */
      transform: translateY(-50%);
      display: flex;
      align-items: center;
      gap: 4px;
      opacity: 0;
      pointer-events: none;
      white-space: nowrap;
      font-size: 14px;
      font-weight: 900;
      letter-spacing: .03em;
      /* Solana purple, so the gain, the coins that carried it and the
         pulse on the nub are all plainly the same event */
      color: rgb(186, 131, 255);
      font-variant-numeric: tabular-nums;
    }
    .earnings-delta-sol { width: 11px; height: 11px; display: block; }
    /* money leaving, a battle's stake: a muted rose rather than an alarm */
    .earnings-delta.is-spent { color: rgb(236, 150, 170); }
    /* While a battle's stakes are paid in, the wallet sits above the
       versus intro's backdrop (118), so the coins can be seen leaving
       it. The coins themselves are already above everything. */
    body.battle-stakes-open .earnings-chip { z-index: 120 !important; }
    .account-chip,
    .holdings-chip,
    .earnings-chip {
      position: fixed !important;
      left: 16px !important;
      right: auto !important;
      transform-origin: top left;
      z-index: 41;
    }
    .account-chip { top: 16px !important; }
    /* 16 + (64 - 2) */
    .holdings-chip { top: 78px !important; z-index: 39; }
    /* 78 + (44 - 2): the same overlap again, one nub further down */
    .earnings-chip { top: 120px !important; z-index: 38; }
    /* The daily spin button used to continue this stack at 174px.
       It lives in the bottom-left dock now, so the chips end here. */
    /* one line: amount and unit sit side by side instead of stacked */
    .holdings-chip .holdings-value {
      display: flex;
      align-items: baseline;
      gap: 5px;
      min-width: 0;
      font-size: 15px;
      font-weight: 900;
      line-height: 1;
      letter-spacing: .04em;
      color: #f5f4ef;
      white-space: nowrap;
    }
    /* the amount takes the row's own size and holder colour; only the unit differs */
    .holdings-chip .holdings-amount { font: inherit; color: inherit; }
    .holdings-chip .holdings-unit {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .1em;
      color: rgba(151, 184, 222, 0.62);
    }
    /* Loading spinner inside holdings value while waiting for holdings to load */
    .holdings-chip .holdings-value.loading { position: relative; }
    .holdings-chip .holdings-spinner {
      width: 15px;
      height: 15px;
      border-radius: 50%;
      border: 2px solid rgba(255,255,255,0.12);
      border-top-color: #69d69a;
      box-sizing: border-box;
      animation: holdingsSpin 1s linear infinite;
    }
    @keyframes holdingsSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
    .holdings-chip .holdings-value.holdings-holder { color: #69d69a; }
    .holdings-chip .holdings-value.holdings-non-holder { color: #ff6060; }
    .holdings-reload-btn {
      width: 26px;
      height: 26px;
      border: 0;
      border-radius: 8px;
      background: transparent;
      box-shadow: none;
      padding: 0;
      cursor: pointer;
      overflow: hidden;
      flex: 0 0 auto;
      display: grid;
      place-items: center;
    }
    .holdings-reload-btn .reload-icon {
      font-size: 16px;
      line-height: 1;
      font-weight: 900;
      color: #cfdcea;
    }
    .holdings-reload-btn.spin .reload-icon {
      animation: holdingsReloadSpin .45s linear;
    }
    @keyframes holdingsReloadSpin {
      from { transform: rotate(0deg); }
      to { transform: rotate(360deg); }
    }
    .account-flag {
      width: 42px;
      height: 42px;
      border-radius: 50%;
      overflow: hidden;
      flex: 0 0 auto;
      background: rgba(255,255,255,.05);
      border: 1px solid rgba(55, 168, 255, 0.18);
    }
    .account-flag img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }
    .account-meta {
      display: grid;
      gap: 2px;
      min-width: 0;
      text-align: left;
      justify-items: start;
    }
    .account-name {
      font-size: 14px;
      font-weight: 900;
      line-height: 1.1;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      max-width: 160px;
    }
    .account-address {
      font-size: 10px;
      color: var(--muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      max-width: 160px;
      letter-spacing: .04em;
    }
    .bottom-left-dock {
      position: fixed;
      left: 16px;
      bottom: calc(18px + env(safe-area-inset-bottom, 0px));
      z-index: 220;
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 10px;
      transform-origin: bottom left;
      pointer-events: none;
    }
    .bottom-left-dock > * { pointer-events: auto; }
    /* The audio button and Daily Spin, side by side. In portrait
       .music-flyout is pulled out to the bottom right and this row
       is left holding the one button, which is what it should look
       like there. */
    .dock-row {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .music-flyout {
      position: relative;
      z-index: 2;
      --music-flyout-x: 0px;
      --music-flyout-y: 0px;
    }
    .music-button {
      width: 44px;
      height: 44px;
      border: 1px solid var(--tr-border-subtle);
      border-radius: 14px;
      background:
        radial-gradient(circle at 20% 0%, rgba(95, 212, 255, 0.08), transparent 46%),
        var(--tr-control-gradient);
      box-shadow:
        var(--tr-inset-highlight),
        0 10px 28px rgba(0, 0, 0, 0.4),
        0 0 20px rgba(95, 212, 255, 0.055);
      display: grid;
      place-items: center;
      cursor: pointer;
      color: #dff4ff;
      z-index: 220;
      font-size: 20px;
    }
    .music-popover {
      position: absolute;
      bottom: calc(100% + 10px);
      display: grid;
      gap: 10px;
      padding: 12px;
      border-radius: 16px;
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.98), rgba(9, 14, 24, 0.99));
      border: 1px solid rgba(56, 160, 255, 0.18);
      box-shadow:
        0 0 0 1px rgba(125, 51, 255, 0.12) inset,
        0 14px 34px rgba(0, 0, 0, 0.36);
      opacity: 0;
      pointer-events: none;
      transform: translate3d(var(--music-flyout-x), var(--music-flyout-y), 0) scale(0.98);
      transition: opacity .18s ease, transform .18s ease;
      min-width: 220px;
    }
    .music-flyout.open .music-popover {
      opacity: 1;
      pointer-events: auto;
      transform: translate3d(var(--music-flyout-x), var(--music-flyout-y), 0) scale(1);
    }

    .music-flyout.right-anchor .music-popover {
      transform-origin: bottom right;
    }
    .music-flyout.right-anchor .music-popover::before {
      left: auto;
      right: 14px;
    }
    .music-flyout.left-anchor .music-popover {
      transform-origin: bottom left;
    }
    .music-flyout.left-anchor .music-popover::before {
      left: 14px;
      right: auto;
    }
    .music-toggle-row {
      display: flex;
      justify-content: center;
    }
    .music-mute-btn {
      border: 1px solid rgba(56, 160, 255, 0.18);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.96), rgba(9, 14, 24, 0.98));
      color: #dff4ff;
      border-radius: 12px;
      padding: 8px 12px;
      font-size: 10px;
      font-weight: 900;
      letter-spacing: .12em;
      text-transform: uppercase;
      cursor: pointer;
      box-shadow:
        0 0 0 1px rgba(155, 125, 255, 0.12) inset,
        0 12px 24px rgba(0, 0, 0, 0.28),
        0 0 18px rgba(105, 214, 154, 0.08);
    }
    .music-row {
      display: flex;
      align-items: center;
      gap: 10px;
      min-width: 180px;
    }
    .volume-slider {
      width: 150px;
      accent-color: #33b5ff;
    }
    .volume-value {
      min-width: 34px;
      text-align: right;
      font-size: 12px;
      color: var(--muted);
    }
    /* ---- board popups: combo / t-spin / perfect clear ----
       Shared shell so all three read as one family. Every entrance keyframe must
       land on the same transform as .show, otherwise the popup jumps when the
       animation ends and the transition takes back over. */
    .combo-popup,
    .tspin-popup,
    .perfect-popup {
      position: absolute;
      left: 50%;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      white-space: nowrap;
      font-weight: 900;
      line-height: 1;
      text-transform: uppercase;
      opacity: 0;
      pointer-events: none;
      transform: translateX(-50%) translateY(6px) scale(0.86);
      transition: opacity .16s ease, transform .22s cubic-bezier(.2, .9, .25, 1);
      --popup-ring: rgba(255, 255, 255, 0.6);
    }
    .combo-popup.show,
    .tspin-popup.show,
    .perfect-popup.show {
      opacity: 1;
      transform: translateX(-50%) translateY(-4px) scale(1);
    }
    /* shine sweep: an animated background layer, so the outer glow is never clipped */
    .combo-popup::before,
    .tspin-popup::before,
    .perfect-popup::before {
      content: "";
      position: absolute;
      inset: 0;
      border-radius: inherit;
      background: linear-gradient(105deg, transparent 38%, rgba(255, 255, 255, 0.55) 50%, transparent 62%);
      background-size: 260% 100%;
      background-position: 160% 0;
      background-repeat: no-repeat;
      opacity: 0;
      pointer-events: none;
    }
    /* ring that snaps out from the edge on each trigger */
    .combo-popup::after,
    .tspin-popup::after,
    .perfect-popup::after {
      content: "";
      position: absolute;
      inset: -2px;
      border-radius: inherit;
      border: 2px solid var(--popup-ring);
      opacity: 0;
      pointer-events: none;
    }
    .combo-popup.bump::before,
    .tspin-popup.bump::before,
    .perfect-popup.bump::before { animation: popupShine .72s ease-out; }
    .combo-popup.bump::after,
    .tspin-popup.bump::after,
    .perfect-popup.bump::after { animation: popupRing .62s ease-out; }
    @keyframes popupShine {
      0% { background-position: 160% 0; opacity: 0; }
      18% { opacity: 1; }
      100% { background-position: -60% 0; opacity: 0; }
    }
    @keyframes popupRing {
      0% { opacity: .85; transform: scale(.92); }
      100% { opacity: 0; transform: scale(1.34); }
    }

    .combo-popup {
      top: 22px;
      padding: 7px 14px;
      border-radius: 999px;
      font-size: 15px;
      letter-spacing: .10em;
      color: #eaf9ff;
      text-shadow: 0 0 12px rgba(255, 255, 255, .40), 0 1px 0 rgba(0, 0, 0, .35);
      background: linear-gradient(180deg, rgba(64, 205, 255, 0.96), rgba(96, 60, 255, 0.96));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.35),
        inset 0 0 0 1px rgba(255, 255, 255, 0.10),
        0 8px 22px rgba(0, 0, 0, 0.34),
        0 0 26px rgba(66, 190, 255, 0.35);
      --popup-ring: rgba(120, 220, 255, 0.85);
      z-index: 20;
    }
    /* combo escalates: cyan -> violet -> hot */
    .combo-popup[data-tier="2"] {
      background: linear-gradient(180deg, rgba(178, 120, 255, 0.97), rgba(104, 40, 224, 0.97));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.35),
        inset 0 0 0 1px rgba(255, 255, 255, 0.10),
        0 8px 22px rgba(0, 0, 0, 0.34),
        0 0 30px rgba(158, 100, 255, 0.45);
      --popup-ring: rgba(190, 146, 255, 0.9);
    }
    .combo-popup[data-tier="3"] {
      font-size: 17px;
      color: #2a1200;
      text-shadow: 0 1px 0 rgba(255, 255, 255, .35);
      background: linear-gradient(180deg, rgba(255, 206, 82, 0.98), rgba(255, 104, 52, 0.98));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.45),
        inset 0 0 0 1px rgba(255, 255, 255, 0.16),
        0 8px 24px rgba(0, 0, 0, 0.36),
        0 0 36px rgba(255, 150, 60, 0.55);
      --popup-ring: rgba(255, 208, 112, 0.95);
    }
    .combo-popup.bump { animation: comboPop .42s cubic-bezier(.2, .9, .25, 1); }
    .combo-popup[data-tier="3"].bump { animation: comboPopHot .52s cubic-bezier(.2, .9, .25, 1); }
    @keyframes comboPop {
      0% { transform: translateX(-50%) translateY(8px) scale(.72); }
      40% { transform: translateX(-50%) translateY(-9px) scale(1.16); }
      66% { transform: translateX(-50%) translateY(-1px) scale(.97); }
      100% { transform: translateX(-50%) translateY(-4px) scale(1); }
    }
    @keyframes comboPopHot {
      0% { transform: translateX(-50%) translateY(10px) scale(.70) rotate(-4deg); }
      34% { transform: translateX(-50%) translateY(-13px) scale(1.24) rotate(3deg); }
      54% { transform: translateX(-50%) translateY(-1px) scale(.96) rotate(-2deg); }
      76% { transform: translateX(-50%) translateY(-7px) scale(1.06) rotate(1deg); }
      100% { transform: translateX(-50%) translateY(-4px) scale(1) rotate(0deg); }
    }

    .tspin-popup {
      top: 64px;
      padding: 10px 20px;
      border-radius: 14px;
      font-size: 15px;
      letter-spacing: .22em;
      color: #f6f1ff;
      text-shadow: 0 0 14px rgba(190, 150, 255, .65), 0 1px 0 rgba(0, 0, 0, .35);
      background: linear-gradient(180deg, rgba(148, 84, 255, 0.98), rgba(74, 26, 178, 0.98));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.30),
        inset 0 0 0 1px rgba(255, 255, 255, 0.10),
        0 10px 26px rgba(0, 0, 0, 0.36),
        0 0 32px rgba(140, 80, 255, 0.45);
      --popup-ring: rgba(186, 140, 255, 0.9);
      z-index: 19;
    }
    .tspin-popup.bump { animation: tspinPop .52s cubic-bezier(.2, .9, .25, 1); }
    @keyframes tspinPop {
      0% { transform: translateX(-50%) translateY(10px) scale(.74) rotate(-5deg); opacity: 0; }
      42% { transform: translateX(-50%) translateY(-10px) scale(1.14) rotate(2deg); opacity: 1; }
      68% { transform: translateX(-50%) translateY(-1px) scale(.98) rotate(-1deg); }
      100% { transform: translateX(-50%) translateY(-4px) scale(1) rotate(0deg); opacity: 1; }
    }

    .perfect-popup {
      top: 116px;
      padding: 11px 20px;
      border-radius: 14px;
      font-size: 15px;
      letter-spacing: .14em;
      color: #2c1a00;
      text-shadow: 0 1px 0 rgba(255, 255, 255, .40);
      background: linear-gradient(180deg, #ffe493 0%, #ffc44f 48%, #ff9a2e 100%);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.60),
        inset 0 0 0 1px rgba(255, 255, 255, 0.24),
        0 10px 26px rgba(0, 0, 0, 0.34),
        0 0 40px rgba(255, 186, 70, 0.55);
      --popup-ring: rgba(255, 224, 140, 0.95);
      /* above the combo (20) and t-spin (19) pills it used to sit under;
         only the K.O. stamp outranks it */
      z-index: 24;
    }
    .perfect-popup.bump { animation: perfectPop .62s cubic-bezier(.2, .9, .25, 1); }
    @keyframes perfectPop {
      0% { transform: translateX(-50%) translateY(14px) scale(.60); opacity: 0; }
      28% { transform: translateX(-50%) translateY(-15px) scale(1.26); opacity: 1; }
      46% { transform: translateX(-50%) translateY(-1px) scale(.95); }
      64% { transform: translateX(-50%) translateY(-9px) scale(1.09); }
      82% { transform: translateX(-50%) translateY(-2px) scale(.99); }
      100% { transform: translateX(-50%) translateY(-4px) scale(1); opacity: 1; }
    }
    @media (prefers-reduced-motion: reduce) {
      .combo-popup.bump,
      .tspin-popup.bump,
      .perfect-popup.bump,
      .combo-popup.bump::before,
      .tspin-popup.bump::before,
      .perfect-popup.bump::before,
      .combo-popup.bump::after,
      .tspin-popup.bump::after,
      .perfect-popup.bump::after {
        animation: none;
      }
    }
    /* KO flash: played over the board when the stack tops out, while play continues */
    .ko-flash {
      position: absolute;
      /* matches .board-wrap padding so the flash covers exactly the playfield */
      inset: 12px;
      display: grid;
      place-items: center;
      border-radius: 5px;
      overflow: hidden;
      opacity: 0;
      pointer-events: none;
      /* It had no z-index, so the combo (20), t-spin (19) and perfect-clear
         pills all painted over the stamp. A KO decides the match; nothing
         else on a board outranks it. */
      z-index: 26;
    }
    .ko-flash::before {
      content: "";
      position: absolute;
      inset: 0;
      background:
        radial-gradient(circle at 50% 50%, rgba(255, 74, 96, 0.34), transparent 70%),
        linear-gradient(180deg, rgba(186, 22, 48, 0.42), rgba(104, 6, 26, 0.58));
      box-shadow:
        inset 0 0 70px rgba(255, 62, 86, 0.55),
        inset 0 0 0 2px rgba(255, 96, 118, 0.65);
    }
    .ko-flash-label {
      position: relative;
      font-size: 62px;
      font-weight: 900;
      letter-spacing: .14em;
      color: #fff;
      text-shadow:
        0 0 18px rgba(255, 62, 86, 0.9),
        0 0 44px rgba(255, 62, 86, 0.6),
        0 4px 0 rgba(104, 6, 26, 0.85);
    }
    .ko-flash.show { animation: koFlashFade 820ms ease-out forwards; }
    .ko-flash.show .ko-flash-label { animation: koFlashStamp 820ms cubic-bezier(.2, .9, .25, 1) forwards; }
    @keyframes koFlashFade {
      0% { opacity: 0; }
      7% { opacity: 1; }
      60% { opacity: 1; }
      100% { opacity: 0; }
    }
    @keyframes koFlashStamp {
      0% { transform: scale(2.2) rotate(-9deg); opacity: 0; }
      14% { transform: scale(0.92) rotate(-5deg); opacity: 1; }
      26% { transform: scale(1.06) rotate(-5deg); }
      38% { transform: scale(1) rotate(-5deg); }
      100% { transform: scale(1) rotate(-5deg); opacity: 1; }
    }
    @media (prefers-reduced-motion: reduce) {
      .ko-flash.show { animation: koFlashFade 820ms linear forwards; }
      .ko-flash.show .ko-flash-label { animation: none; opacity: 1; transform: none; }
    }
    .board-shell {
      position: relative;
      width: max-content;
      overflow: visible;
      margin-top: calc(-1 * var(--side-board-offset, 78px));
    }
    /* Sits above the score chip. A row spanning the shell rather than a
       left-aligned column: the stars stay on the left and the energy chip
       takes the right end. .board-shell is width:max-content and the score
       chip is its widest in-flow child, so left:0/right:0 lands on the
       chip's own edges. The opponent's copy has no energy chip and
       space-between leaves its stars exactly where they were. */
    .board-score {
      position: absolute;
      bottom: calc(100% + 8px);
      left: 0;
      right: 0;
      z-index: 20;
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      width: auto;
      pointer-events: auto;
      transform: none;
      overflow: visible;
    }
    .score-stars {
      position: relative;
      z-index: 0;
      display: flex;
      align-items: center;
      justify-content: flex-start;
      gap: 4px;
      width: max-content;
      flex: none;
      overflow: visible;
      margin: 0;
      transform: none;
    }
    /* inline SVG rather than the two 250-380KB PNGs: sharp at any size and
       recoloured from the theme */
    .score-stars .star {
      display: block;
      width: 24px;
      height: 24px;
      flex: none;
      overflow: visible;
      filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45));
    }
    .score-stars .star path {
      fill: none;
      stroke: rgba(151, 184, 222, 0.34);
      stroke-width: 1.5;
      stroke-linejoin: round;
    }
    .score-stars .star.is-on path {
      fill: #ffc860;
      stroke: rgba(255, 226, 158, 0.85);
    }
    /* ---------------------------------------------------------
       THE TOP OF THE LADDER

       Rank stops at 100. Past it a win still banks a star, so the
       count keeps climbing with nothing to spend it on -- which is
       the point: it is what says how long somebody has been here.
       Five pips cannot draw forty of them, so the row becomes one
       star and a number.

       Purple rather than a fourth accent colour: the RANK module
       beside it is already violet (--module-accent on
       .score-chip-rankbox), so the two things that mean "this
       player is at the ceiling" say it the same way.
       --------------------------------------------------------- */
    .score-stars-total {
      display: none;
      font-family: var(--font-mono, ui-monospace, monospace);
      font-size: 15px;
      font-weight: 800;
      letter-spacing: 0.04em;
      color: #dcd0ff;
      text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
      line-height: 24px;
    }
    .score-stars.is-capped .score-stars-total { display: block; }
    /* The four pips the count replaces. Hidden rather than removed,
       because the animation code still indexes the five elements
       that are always in the markup. */
    .score-stars.is-capped .star:not(:first-child) { display: none; }
    .score-stars.is-capped .star.is-on path {
      fill: #b79cff;
      stroke: rgba(220, 208, 255, 0.85);
    }
    /* ---------------------------------------------------------
       STAR CHANGES

       Played once, after the result screen is closed, so the change is
       something the player watches happen rather than something that had
       already happened behind a panel.

       A promotion is the awkward one: several stars leave at once and the
       rank goes up, and the two have to read as one event rather than two
       coincidences. The stars fly into the rank chip -- a cloned ghost per
       star, positioned by script, so the row underneath can reset without
       waiting -- and the rank pops as they land.
       --------------------------------------------------------- */
    .score-stars .star.star-gain {
      animation: starGain 520ms cubic-bezier(.2, 1.5, .4, 1) both;
    }
    @keyframes starGain {
      0% { transform: scale(0) rotate(-140deg); opacity: 0; }
      60% { transform: scale(1.35) rotate(8deg); opacity: 1; }
      100% { transform: scale(1) rotate(0deg); opacity: 1; }
    }
    .score-stars .star.star-lose {
      animation: starLose 440ms cubic-bezier(.4, 0, .7, .3) both;
    }
    @keyframes starLose {
      0% { transform: scale(1); opacity: 1; }
      35% { transform: scale(1.2) translateY(-2px); opacity: 1; }
      100% { transform: scale(.35) translateY(9px); opacity: 0; }
    }
    /* the flying copy. Fixed, so it is not clipped by the board, and
       pointer-events:none so it cannot swallow a click mid-flight. */
    .star-ghost {
      position: fixed;
      z-index: 150;
      pointer-events: none;
      width: 24px;
      height: 24px;
    }
    .star-ghost path {
      fill: #ffc860;
      stroke: rgba(255, 226, 158, 0.85);
      stroke-width: 1.5;
      stroke-linejoin: round;
    }
    /* A won star is announced in the middle of the screen before it is
       filed away into its slot, so the player sees what they earned rather
       than catching a 24px icon changing colour. The dimming layer is what
       makes it read as an event; the star itself is the same shape as the
       one in the row, just enormous. */
    .star-award-veil {
      position: fixed;
      inset: 0;
      z-index: 148;
      pointer-events: none;
      opacity: 0;
      background: radial-gradient(60% 45% at 50% 50%, rgba(255, 200, 96, 0.14), transparent 70%),
        rgba(2, 5, 11, 0.55);
    }
    .star-ghost.star-award {
      width: 150px;
      height: 150px;
      filter: drop-shadow(0 10px 34px rgba(255, 200, 96, 0.45));
    }
    /* the same dimming layer stands behind the travelling rank panel, so a
       promotion reads as an event rather than a box drifting across a live
       board. Tinted to the direction it is going. */
    .star-award-veil.is-rank-up {
      background: radial-gradient(58% 44% at 50% 50%, rgba(183, 156, 255, 0.18), transparent 70%),
        rgba(2, 5, 11, 0.62);
    }
    .star-award-veil.is-rank-down {
      background: radial-gradient(58% 44% at 50% 50%, rgba(255, 118, 128, 0.14), transparent 70%),
        rgba(2, 5, 11, 0.58);
    }
    /* Cooler and deeper than the others: a shield holding is not a
       celebration, it is something being survived. */
    .star-award-veil.is-shield {
      background: radial-gradient(58% 44% at 50% 50%, rgba(140, 190, 240, 0.17), transparent 70%),
        rgba(2, 5, 11, 0.66);
    }

    /* ---- the shield, full size ----
       A clone, like the star and the rank panel: the real one has to
       keep its place in the rank box while this is away, and a fixed
       element cannot be clipped by the chip's overflow. */
    .rank-shield-ghost {
      position: fixed;
      z-index: 152;
      pointer-events: none;
      display: grid;
      place-items: center;
      transform-origin: 50% 50%;
      will-change: transform;
      filter: drop-shadow(0 14px 38px rgba(120, 175, 230, 0.4));
    }
    .rank-shield-ghost .rsg-shield {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
    }
    .rank-shield-ghost .rs-body {
      stroke: #bcd8f4;
      stroke-width: 1.6;
      fill: rgba(16, 26, 42, 0.92);
      transition: stroke 160ms ease, fill 160ms ease;
    }
    /* struck: the metal lights up and holds it for a beat */
    .rank-shield-ghost.is-struck .rs-body {
      stroke: #ffe6a8;
      fill: rgba(255, 205, 120, 0.22);
    }
    .rank-shield-ghost.is-cracking .rs-body {
      stroke: #ffb0a0;
      fill: rgba(255, 130, 110, 0.18);
    }
    .rank-shield-ghost .rsg-minus {
      position: relative;
      font-size: 54px;
      font-weight: 900;
      line-height: 1;
      letter-spacing: .02em;
      color: #ffe6a8;
      opacity: 0;
      text-shadow: 0 4px 18px rgba(0, 0, 0, 0.6);
    }
    /* the free charge a rank up gives: a +1 and what it is */
    .rank-shield-ghost.is-granted .rsg-plus { color: #cfe6ff; }
    .rank-shield-ghost.is-granted.is-struck .rs-body {
      stroke: #cfe6ff;
      fill: rgba(140, 190, 240, 0.22);
    }
    .rank-shield-ghost .rsg-caption {
      position: absolute;
      top: 100%;
      left: 50%;
      width: 240px;
      margin: 14px 0 0 -120px;
      text-align: center;
      font-size: 13px;
      font-weight: 700;
      letter-spacing: .08em;
      line-height: 1.45;
      text-transform: uppercase;
      color: #dbe8f6;
      opacity: 0;
      text-shadow: 0 2px 12px rgba(0, 0, 0, 0.7);
    }
    /* Each shard is the whole shield clipped to one wedge, so the
       pieces are genuinely pieces of it rather than generic debris. */
    .rank-shield-ghost .rsg-shard {
      position: absolute;
      inset: 0;
      will-change: transform, opacity;
    }
    .rank-shield-ghost .rsg-shard svg {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
    }
    .rank-shield-ghost .rsg-shard .rs-body {
      stroke: #ffc0ae;
      fill: rgba(255, 140, 118, 0.2);
    }
    /* the five lighting up together, just before a promotion */
    .score-stars .star.star-shine {
      animation: starShine 620ms ease-out both;
    }
    @keyframes starShine {
      0% { transform: scale(1); filter: none; }
      45% { transform: scale(1.45); filter: drop-shadow(0 0 10px rgba(255, 216, 140, 0.9)); }
      100% { transform: scale(1); filter: none; }
    }
    /* The rank module leaves the chip and comes back. A clone travels so
       the chip does not collapse and reflow the row while it is away; the
       original is hidden for the duration and nothing moves around it.

       The clone keeps .score-chip-rankbox -- that is where its border,
       background and accent bar come from -- and the panel pass further
       down this file gives that class `position: relative`. At equal
       specificity the later rule wins, so a plain `.rank-panel-ghost`
       here lost the position and the clone laid itself out in the flow at
       the bottom of <body>, off screen: the chip went blank and nothing
       arrived in the middle. Both classes in the selector puts the ghost
       a step above the component rules it is borrowing from. */
    .score-chip-rankbox.rank-panel-ghost {
      position: fixed;
      z-index: 152;
      pointer-events: none;
      margin: 0;
      transition: none;
      box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
    }
    .rank-panel-ghost.is-rising {
      border-color: rgba(255, 200, 96, 0.55);
    }
    .rank-panel-ghost.is-falling {
      border-color: rgba(255, 118, 128, 0.5);
    }
    .rank-panel-ghost .score-chip-rank {
      transition: color 200ms ease;
    }
    .rank-panel-ghost.is-rising .score-chip-rank { color: #ffd88c; }
    .rank-panel-ghost.is-falling .score-chip-rank { color: #ff9aa2; }
    .score-chip-rank.rank-bump {
      animation: rankBump 520ms cubic-bezier(.2, 1.4, .4, 1) both;
    }
    @keyframes rankBump {
      0% { transform: scale(1); }
      40% { transform: scale(1.5); color: #ffc860; }
      100% { transform: scale(1); }
    }
    .score-chip-rank.rank-drop {
      animation: rankDrop 520ms ease both;
    }
    @keyframes rankDrop {
      0% { transform: scale(1); }
      40% { transform: scale(.7); color: #ff7680; }
      100% { transform: scale(1); }
    }
    @media (prefers-reduced-motion: reduce) {
      .score-stars .star.star-gain,
      .score-stars .star.star-lose,
      .score-stars .star.star-shine,
      .score-chip-rank.rank-bump,
      .score-chip-rank.rank-drop { animation: none; }
      .star-ghost,
      .star-award-veil,
      .rank-shield-ghost,
      .score-chip-rankbox.rank-panel-ghost { display: none; }
    }
    /* ---------------------------------------------------------
       ENERGY

       Blue marks a boosted account -- players/{soladdress}.maxEnergy, the
       flag that raises the ceiling from 50 to 80 -- and amber is everyone
       else. It says which kind of account this is, not how much is left in
       it, so the colour holds steady while the figure moves. The whole
       switch is two custom properties, so the bolt, the figure, the border
       and the tint can never disagree about which state they are in.
       Deliberately flat: a thin border and a faint wash, no glow, so it
       reads as part of the board furniture rather than an arcade meter.
       --------------------------------------------------------- */
    .energy-chip {
      /* The ramp, from energyTint: red at the bottom, yellow at 50,
         blue at 80. The fallback is the middle of it, for the moment
         before a reading has arrived. */
      --energy-accent: var(--energy-tint, #ffc860);
      --energy-accent-soft: color-mix(in srgb, var(--energy-accent) 26%, transparent);
      --energy-accent-wash: color-mix(in srgb, var(--energy-accent) 8%, transparent);
      display: inline-flex;
      align-items: center;
      gap: 7px;
      flex: none;
      white-space: nowrap;
      /* for the boosted caption, which hangs above the cell */
      position: relative;
      /* The pill this used to be is gone. The cell below carries the
         frame now, the same way the lobby's battery does, and a pill
         around a battery would be two borders saying one thing. */
      padding: 0;
      border: 0;
      background: none;
      box-shadow: none;
    }
    /* ---- the cell: the lobby's battery at chip size ----

       Same parts in the same order, so the two are read as one
       instrument seen in two places rather than two designs. Smaller,
       because this one is above the board for the whole game while
       the lobby's only appears when a round cannot be started. */
    .energy-battery {
      display: inline-flex;
      align-items: center;
      flex: none;
    }
    .energy-cell {
      position: relative;
      display: inline-flex;
      align-items: center;
      min-width: 74px;
      height: 24px;
      padding: 0 7px;
      border-radius: 7px;
      border: 2px solid var(--energy-accent);
      background: rgba(6, 11, 21, 0.72);
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.45);
      overflow: hidden;
      transition: border-color 220ms ease;
    }
    .energy-cell-fill {
      position: absolute;
      inset: 0 auto 0 0;
      width: 0%;
      border-radius: 5px 0 0 5px;
      background: linear-gradient(180deg, var(--energy-accent), var(--energy-accent-wash));
      opacity: .5;
      transition: width .32s cubic-bezier(.2, .85, .3, 1), background 220ms ease;
    }
    .energy-cell-read {
      position: relative;
      display: inline-flex;
      align-items: baseline;
      justify-content: center;
      gap: 5px;
      width: 100%;
    }
    /* ---- the faster refill, said on the bolt ----

       Not on the cell. The cell is already carrying the reading in
       the ramp's colour, and the ramp passes through green on its
       way to blue -- so a green cell would be ambiguous between
       "plenty left" and "fills quickly". The cell says how much, the
       bolt says how fast, and the two never have to be untangled.

       It is the glow rather than the green that does the work. The
       ramp is never animated, so a bolt breathing on a two second
       cycle is the one thing on the readout that cannot be mistaken
       for a level. */
    @keyframes energyBoostPulse {
      0%, 100% {
        opacity: 1;
        filter: drop-shadow(0 0 3px rgba(105, 214, 154, 0.55));
      }
      50% {
        opacity: .75;
        filter: drop-shadow(0 0 7px rgba(105, 214, 154, 0.9));
      }
    }
    .energy-chip.is-refill-boosted .energy-bolt,
    .opponent-overlay-subtext-btn.is-refill-boosted .btn-bolt,
    .find-match-energy.is-refill-boosted .find-match-energy-bolt {
      color: #69d69a;
      animation: energyBoostPulse 2.2s ease-in-out infinite;
    }
    @media (prefers-reduced-motion: reduce) {
      /* the green and the halo stay; only the breathing stops */
      .energy-chip.is-refill-boosted .energy-bolt,
      .opponent-overlay-subtext-btn.is-refill-boosted .btn-bolt,
      .find-match-energy.is-refill-boosted .find-match-energy-bolt {
        animation: none;
        filter: drop-shadow(0 0 5px rgba(105, 214, 154, 0.75));
      }
    }
    /* ---- what the green bolt means, on hover ----

       The bolt is the mark; this is the sentence behind it, for a
       player who wants to know why theirs is green. A panel rather
       than a title attribute: the native tooltip waits a second and
       draws in the operating system's colours, on the most carefully
       themed strip of the page.

       Only reachable while the flag is on, so a plain account cannot
       hover up an explanation of something it does not have, and
       absolute so a hover never moves the stars beside it.

       Anchored by its right edge, not its left. The chip lives at the
       right end of a row as wide as the board, so a panel growing
       leftwards from there stays over the board however narrow the
       screen gets; growing rightwards it would run off the edge.

       Below the chip, not above, although above is where a caption
       belongs. The row floats over the top of the board and how much
       clear space is left above it is set by the window height -- at
       1080 the chip sits at y 154, at 800 at y 14, and by 720 the chip
       itself is already clipped. A panel hung above it would be cut
       off on any short window. Below it there is always the board. It
       costs a moment of the playfield's top rows while the pointer
       rests on the chip, which is a fair trade for a panel that can
       always be read. */
    .energy-boost-tip {
      position: absolute;
      right: 0;
      top: calc(100% + 8px);
      z-index: 5;
      display: grid;
      gap: 3px;
      /* A plain width, not max-content. Everything here is border-box, so
         a max-content width sizes the *border* box to what the sentence
         needs and then the padding is taken out of the inside -- and the
         column, already sized to the full sentence, hangs out over the
         right edge. The sentence wraps instead. */
      width: min(212px, 70vw);
      padding: 8px 10px;
      border-radius: 10px;
      text-align: left;
      background: linear-gradient(180deg, rgba(14, 20, 34, 0.98), rgba(8, 12, 20, 0.99));
      border: 1px solid rgba(105, 214, 154, 0.26);
      box-shadow: 0 12px 28px rgba(0, 0, 0, 0.38),
                  0 0 0 1px rgba(105, 214, 154, 0.06) inset;
      opacity: 0;
      pointer-events: none;
      /* drops into place, since it now arrives from above */
      transform: translateY(-4px);
      transition: opacity .15s ease, transform .15s ease;
    }
    .energy-boost-tip-title {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: #69d69a;
    }
    .energy-boost-tip-body {
      font-size: 11px;
      font-weight: 600;
      line-height: 1.45;
      color: #eaf6ff;
      /* two lines of roughly equal length rather than a full one and a
         stub; ignored by browsers that do not know it */
      text-wrap: balance;
    }
    /* the cursor is the only hint that there is anything to hover */
    .energy-chip.is-refill-boosted { cursor: help; }
    .energy-chip.is-refill-boosted:hover .energy-boost-tip {
      opacity: 1;
      transform: translateY(0);
    }
    /* the terminal, which is what makes the shape a battery */
    .energy-cell-nub {
      flex: none;
      width: 4px;
      height: 10px;
      margin-left: 2px;
      border-radius: 0 2px 2px 0;
      background: var(--energy-accent);
      opacity: .85;
      transition: background 220ms ease;
    }
    /* Colour follows the level, not the account -- see energyLevelOf.
       The .is-boosted class is still set on the chip because it says
       which account this is, but it no longer decides the colour: a
       boosted player on 3 of 80 needs the same red a plain one gets,
       and blue is now what being above 50 looks like. */
    /* The chip warms up as it drains. Both of these come after
       .is-boosted so they win over it: which account you hold matters
       less than whether you can start a round, and a boosted player
       on 2 energy needs the same warning a plain one does.

       The whole chip moves, not just the digits -- the accent drives
       the bolt, the border and the wash through the one variable, so
       it reads at a glance without anything flashing. */
    /* The three band rules that used to live here -- .is-charged,
       .is-warn, .is-low -- are gone. They were steps between four
       fixed colours; the ramp gives every reading its own, and the
       classes stay only as states the rest of the page keys off. */
    .energy-bolt {
      width: 12px;
      height: 12px;
      flex: none;
      display: block;
      align-self: center;
      color: var(--energy-accent);
    }
    /* currentColor, not --energy-accent directly. The rule above already
       sets color from the accent, so the default is unchanged -- but a
       path painted straight from the variable ignores any rule that
       recolours the bolt, which is how the refill-boost green was being
       silently overpainted by the ramp. */
    .energy-bolt path {
      fill: currentColor;
      transition: fill 220ms ease;
    }
    .energy-values {
      display: inline-flex;
      align-items: baseline;
      font-variant-numeric: tabular-nums;
      line-height: 1;
    }
    .energy-current {
      font-size: 15px;
      font-weight: 800;
      color: var(--energy-accent);
      transition: color 220ms ease;
    }
    .energy-max {
      font-size: 10px;
      font-weight: 700;
      color: var(--text-muted);
      letter-spacing: .04em;
    }
    /* the countdown to the next point. Empty at full, and the rule collapses
       the gap so the chip does not carry a hole where it used to be. */
    .energy-timer {
      font-size: 10px;
      font-weight: 600;
      color: var(--text-muted);
      font-variant-numeric: tabular-nums;
      letter-spacing: .04em;
      /* The divider went with the pill. There is no enclosing frame
         to divide any more, and the gap between the cell and this
         does the separating. */
    }
    .energy-timer:empty {
      display: none;
    }
    /* frame, background and radius come from the PANEL FRAME section below */
    .board-wrap {
      position: relative;
      width: fit-content;
      margin: 0 auto;
    }
    .opponent-board-wrap {
      position: relative;
    }
    .opponent-board-wrap .board-idle-label {
      inset: 0;
      padding: 18px;
      background:
        radial-gradient(circle at 50% 18%, rgba(88, 166, 255, 0.16), transparent 34%),
        radial-gradient(circle at 50% 54%, rgba(77, 112, 255, 0.08), transparent 52%),
        linear-gradient(180deg, rgba(7, 12, 23, 0.82), rgba(6, 10, 18, 0.95));
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }
    .opponent-lobby-shell {
      position: absolute;
      inset: 0;
      display: grid;
      place-items: center;
      pointer-events: none;
    }
    .opponent-lobby-shell.is-hidden {
      opacity: 0;
      visibility: hidden;
    }
    .opponent-lobby-glow,
    .opponent-lobby-grid {
      position: absolute;
      inset: 0;
      pointer-events: none;
    }
    .opponent-lobby-glow {
      background:
        radial-gradient(circle at 50% 24%, rgba(95, 212, 255, 0.14), transparent 34%),
        radial-gradient(circle at 50% 72%, rgba(152, 116, 255, 0.08), transparent 42%);
      opacity: 0.92;
    }
    .opponent-lobby-grid {
      background-image:
        linear-gradient(rgba(95, 212, 255, 0.05) 1px, transparent 1px),
        linear-gradient(90deg, rgba(95, 212, 255, 0.05) 1px, transparent 1px);
      background-size: 28px 28px;
      mask-image: linear-gradient(180deg, rgba(0,0,0,0.95), rgba(0,0,0,0.32));
      opacity: 0.45;
    }
    .opponent-lobby-card {
      position: relative;
      width: min(100%, 278px);
      display: grid;
      gap: 16px;
      justify-items: center;
      padding: 22px 20px;
      border-radius: 18px;
      background: linear-gradient(180deg, rgba(13, 19, 33, 0.90), rgba(7, 11, 20, 0.97));
      border: 1px solid var(--panel-edge);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        0 14px 34px rgba(0, 0, 0, 0.34);
      pointer-events: none;
      transition: border-color var(--tr-transition-standard);
    }
    .opponent-lobby-card.is-searching {
      border-color: var(--panel-edge-hi);
    }
    /* one column: the two stat rows, the button and the card edge all line up */
    .opponent-lobby-stats {
      display: grid;
      gap: 8px;
      width: 100%;
      padding-bottom: 16px;
      border-bottom: 1px solid var(--panel-edge);
    }
    .opponent-lobby-stat {
      display: grid;
      grid-template-columns: auto 1fr auto;
      align-items: center;
      gap: 10px;
      width: 100%;
      min-height: 34px;
      padding: 0 12px;
      border-radius: 10px;
      background: rgba(255, 255, 255, 0.022);
      border: 1px solid var(--panel-edge);
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .14em;
      text-transform: uppercase;
      white-space: nowrap;
    }
    .opponent-lobby-stat .counter-label { line-height: 1; }
    .opponent-lobby-stat .counter-value {
      margin-left: 0;
      justify-self: end;
      font-size: 14px;
      font-weight: 900;
      letter-spacing: 0;
      color: var(--tr-text-primary);
    }
    .opponent-lobby-dot {
      width: 7px;
      height: 7px;
      border-radius: 999px;
      flex: none;
    }
    .opponent-lobby-dot-total { background: #ffd84a; color: #ffd84a; box-shadow: 0 0 6px rgba(255, 216, 74, 0.45); }
    .opponent-lobby-dot-online { background: #24f06a; color: #24f06a; box-shadow: 0 0 6px rgba(36, 240, 106, 0.45); }
    .opponent-lobby-stat-total { color: #ffd84a; }
    .opponent-lobby-stat-online { color: #78e5b2; }
    .player-counts-flyout {
      gap: 5px;
      pointer-events: none;
    }
    .player-count-chip {
      margin-bottom: 0;
      width: max-content;
      min-width: 0;
      pointer-events: none;
    }
    .player-count-chip .counter-value {
      min-width: 18px;
      text-align: left;
    }
    .opponent-lobby-center {
      display: grid;
      justify-items: center;
      gap: 8px;
      text-align: center;
      width: 100%;
    }
    .opponent-lobby-arena {
      margin-bottom: 4px;
      font-size: 15px;
      line-height: 1;
      color: var(--tr-cyan);
      opacity: .7;
    }
    .opponent-lobby-title {
      margin: 0;
      font-size: 18px;
      font-weight: 900;
      letter-spacing: .20em;
      color: var(--tr-text-primary);
      text-transform: uppercase;
    }
    .opponent-lobby-subtitle {
      margin: 0;
      max-width: 26ch;
      font-size: 12px;
      line-height: 1.45;
      color: var(--text-secondary);
    }
    /* the widest breath in the card sits just above the action */
    .opponent-lobby-center .opponent-overlay-subtext-btn { margin-top: 10px; }
    /* empty while idle, so it must not reserve height or a grid gap */
    .opponent-lobby-status-row {
      display: none;
      justify-items: center;
      gap: 8px;
    }
    .opponent-lobby-card.is-searching .opponent-lobby-status-row { display: grid; }
    .opponent-lobby-timer {
      font-size: 12px;
      font-weight: 800;
      letter-spacing: .08em;
      color: var(--text-secondary);
    }
    .opponent-room-spinner {
      display: none;
    }
    .match-status-overlay {
      position: fixed;
      top: calc(env(safe-area-inset-top) + 12px);
      left: 50%;
      transform: translateX(-50%);
      z-index: 9999;
      display: none;
      min-width: 220px;
      padding: 10px 14px;
      border-radius: 16px;
      text-align: center;
      background: var(--tr-panel-gradient);
      border: 1px solid var(--tr-border-subtle);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
      pointer-events: none;
    }
    .match-status-overlay.waiting {
      background: var(--tr-panel-gradient);
      border-color: rgba(255,216,120,0.42);
      box-shadow: var(--tr-inset-highlight), 0 0 24px rgba(255,216,120,0.1);
    }
    .match-status-overlay.game {
      background: var(--tr-panel-gradient);
      border-color: rgba(120,229,178,0.42);
      box-shadow: var(--tr-inset-highlight), 0 0 24px rgba(120,229,178,0.1);
    }
    .match-status-overlay.show { display: grid; gap: 4px; }
    .match-status-label {
      font-size: 10px;
      font-weight: 900;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--text-secondary);
    }
    .match-status-value {
      font-size: clamp(28px, 5vw, 38px);
      font-weight: 900;
      color: var(--text-primary);
      line-height: 1;
      white-space: nowrap;
    }
    .board-idle-label {
      position: absolute;
      inset: 0;
      display: grid;
      place-items: center;
      text-align: center;
      pointer-events: none;
      z-index: 3;
      padding: 16px;
      background: linear-gradient(180deg, rgba(7, 11, 18, 0.10), rgba(7, 11, 18, 0.24));
      justify-items: center;
    }
    .opponent-board-wrap.match-finding .board-idle-label {
      gap: 4px;
      align-content: center;
      justify-items: center;
      justify-content: center;
    }
    .opponent-board-wrap.match-finding .board-idle-title {
      font-size: 18px;
      font-weight: 900;
      letter-spacing: .12em;
      color: #f2f7ff;
      text-shadow: 0 0 12px rgba(109, 162, 255, 0.2);
      text-align: center;
    }
    .opponent-board-wrap.match-finding .board-idle-subtitle {
      font-size: 30px;
      font-weight: 900;
      letter-spacing: .04em;
      color: rgba(160, 168, 180, 0.8);
      text-align: center;
      -webkit-text-stroke: 0.4px rgba(40, 46, 56, 0.45);
      text-shadow: 0 0 6px rgba(0, 0, 0, 0.18);
    }
    .opponent-board-wrap.opponent-game-active .board-idle-label {
      display: none;
      background: transparent;
    }
    .opponent-board-wrap.opponent-game-active::after {
      display: none;
    }
    .opponent-board-wrap.opponent-game-active #opponentBoard {
      filter: none;
      opacity: 1;
    }
    .opponent-room-spinner {
      width: 22px;
      height: 22px;
      border-radius: 50%;
      border: 3px solid rgba(242, 247, 255, 0.16);
      border-top-color: rgba(109, 162, 255, 0.95);
      box-shadow: 0 0 10px rgba(109, 162, 255, 0.22);
      animation: opponentRoomSpin .8s linear infinite;
    }
    @keyframes opponentRoomSpin {
      to { transform: rotate(360deg); }
    }
    .opponent-overlay-subtext-btn {
      position: relative;
      z-index: 11;
      width: 100%;
      min-width: 0;
      /* a row rather than a line of text, because the locked state
         puts the energy bolt in front of the words */
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      padding: 13px 18px;
      border-radius: 10px;
      border: 1px solid rgba(109, 162, 255, 0.28);
      background: var(--tr-primary-gradient);
      color: #f8fbff;
      font-size: 13px;
      font-weight: 900;
      letter-spacing: .14em;
      text-transform: uppercase;
      box-shadow:
        inset 0 1px rgba(255, 255, 255, 0.14),
        0 10px 22px rgba(0, 0, 0, 0.32);
      pointer-events: auto;
      cursor: pointer;
      text-align: center;
      transition: transform .18s ease, filter .18s ease, box-shadow .18s ease, background .18s ease;
    }
    .opponent-overlay-subtext-btn:not([data-state="cancel"]) {
      animation: opponentFindMatchPulse 1.8s ease-in-out infinite;
    }
    /* =========================================================
       NOT ENOUGH $TETRA

       The entry price is enforced on the server, which refuses the
       movement write and says nothing -- so the button worked, the
       match opened and the board never moved. The lobby states the
       price instead and offers the one thing that fixes it.

       The locked button keeps its size and loses its pull: no
       fill, no pulse, nothing inviting a press.
       ========================================================= */
    .opponent-overlay-subtext-btn.is-locked,
    .opponent-overlay-subtext-btn:disabled {
      background: rgba(18, 25, 38, 0.92);
      border-color: rgba(255, 255, 255, 0.1);
      color: var(--tr-text-muted, #8c9aab);
      box-shadow: none;
      cursor: not-allowed;
      animation: none;
      transform: none;
    }
    /* The bolt in the locked button. Sized in em so it tracks the
       label rather than the breakpoints, and it takes the button's
       own muted colour from the .is-locked rule above through
       currentColor. */
    .opponent-overlay-subtext-btn .btn-bolt {
      width: 1.15em;
      height: 1.15em;
      flex: none;
      display: block;
    }
    /* ---- the button, coloured by what is left ----

       The control the player is aiming at is also the readout: blue
       above the ordinary ceiling, the house violet across the
       ordinary range, warming through orange as it gets short and
       going red when a round can no longer be started.

       Only the accents move -- the border, the glow and the price
       chip. The fill stays the primary gradient so the button still
       reads as the thing to press; a button that changed colour
       outright four times would stop looking like one control. */
    /* ---- the fill, coloured by what is left ----

       The whole control, not only its edge. The tint is mixed down
       against the panel's own dark before it is used, and that is
       not decoration: the ramp runs through yellow, and a fill of
       raw #ffc860 under white uppercase is a button nobody can read.
       Mixed this far down, the red, yellow and blue ends all land in
       the same narrow lightness band, so the label keeps roughly the
       same contrast wherever the reading happens to be.

       The fallback is the middle stop of the house violet, so a
       button with no reading behind it -- signed out, or before the
       document arrives -- looks the way it always did. */
    .opponent-overlay-subtext-btn {
      --fm-fill: var(--energy-tint, #7550e8);
      --fm-accent: color-mix(in srgb, var(--energy-tint, #6da2ff) 62%, transparent);
    }
    .opponent-overlay-subtext-btn:not(:disabled) {
      background:
        radial-gradient(circle at 22% 0%, rgba(255, 255, 255, 0.12), transparent 54%),
        linear-gradient(135deg,
          color-mix(in srgb, var(--fm-fill) 56%, #1a1030) 0%,
          color-mix(in srgb, var(--fm-fill) 46%, #120b24) 52%,
          color-mix(in srgb, var(--fm-fill) 32%, #0b0718) 100%);
      border-color: var(--fm-accent);
      box-shadow:
        inset 0 1px rgba(255, 255, 255, 0.14),
        0 10px 22px rgba(0, 0, 0, 0.32),
        0 0 0 1px var(--fm-accent) inset;
    }
    /* The price, on the control that charges it. A chip rather than
       more label: the words are the action, this is the cost. */
    .fm-cost {
      display: inline-flex;
      align-items: center;
      gap: 2px;
      flex: none;
      padding: 2px 7px 2px 5px;
      border-radius: 999px;
      border: 1px solid var(--fm-accent);
      background: rgba(0, 0, 0, 0.26);
      font-size: 0.78em;
      font-weight: 800;
      letter-spacing: .02em;
      font-variant-numeric: tabular-nums;
    }
    .fm-cost .btn-bolt { width: 0.95em; height: 0.95em; }
    .fm-label { flex: none; }
    /* Out of energy is a wait, not a purchase, so the button says so
       in the colour the chip uses for the same state rather than in
       the flat grey of a control that is simply unavailable. */
    .opponent-overlay-subtext-btn.is-out-of-energy {
      border-color: rgba(255, 111, 114, 0.28);
      color: #ff9ea0;
      /* "Not enough energy" is three times the width of "Find Match"
         and the lobby card is only as wide as the board, so the type
         gives a little rather than the label wrapping onto a second
         line and making the button twice as tall as the one it
         replaced. */
      font-size: 12px;
      letter-spacing: .07em;
    }
    .find-match-requirement {
      margin: 10px 0 0;
      font-size: 11.5px;
      line-height: 1.45;
      letter-spacing: .04em;
      color: #e0b872;
      text-align: center;
    }
    /* the same red, so the line and the button agree about which
       problem they are describing */
    .find-match-requirement.is-low { color: #ff9ea0; }
    /* ---- the cell ----

       A battery rather than a sentence, and deliberately the largest
       thing in the card: somebody who cannot start a round is here to
       find out how far off they are, and a strip of charge answers
       that before the eye reaches any words. The reading sits inside
       the cell rather than beside it, so the figure and the fill
       cannot be read apart.

       Its colour comes from the same four bands as the chip and the
       button, through --fm-cell. */
    .fm-battery {
      --fm-cell: var(--energy-tint, #ffc860);
      --fm-cell-soft: color-mix(in srgb, var(--fm-cell) 16%, transparent);
      margin-top: 12px;
      display: flex;
      align-items: stretch;
      justify-content: center;
      width: 100%;
    }
    .fm-battery[hidden] { display: none; }
    .fm-battery-shell {
      position: relative;
      flex: 1 1 auto;
      min-width: 0;
      height: 46px;
      border-radius: 10px;
      /* the casing: thick enough to read as a cell rather than as
         another progress bar in a page that has several */
      border: 2px solid var(--fm-cell);
      background: rgba(6, 11, 21, 0.72);
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.45);
      overflow: hidden;
    }
    .fm-battery-fill {
      position: absolute;
      inset: 0 auto 0 0;
      width: 0%;
      border-radius: 7px 0 0 7px;
      background: linear-gradient(180deg, var(--fm-cell), var(--fm-cell-soft));
      opacity: .55;
      transition: width .32s cubic-bezier(.2, .85, .3, 1), background .22s ease;
    }
    .fm-battery-read {
      position: relative;
      height: 100%;
      display: flex;
      align-items: baseline;
      justify-content: center;
      gap: 1px;
      font-variant-numeric: tabular-nums;
      line-height: 46px;
    }
    .fm-battery-now {
      font-size: 26px;
      font-weight: 900;
      letter-spacing: .01em;
      color: #f4f9ff;
      text-shadow: 0 1px 4px rgba(0, 0, 0, 0.75);
    }
    .fm-battery-cap {
      font-size: 13px;
      font-weight: 800;
      color: rgba(226, 238, 252, 0.62);
      text-shadow: 0 1px 4px rgba(0, 0, 0, 0.75);
    }
    /* the terminal, which is what makes the shape a battery rather
       than a rounded rectangle */
    .fm-battery-nub {
      flex: none;
      align-self: center;
      width: 5px;
      height: 16px;
      margin-left: 3px;
      border-radius: 0 3px 3px 0;
      background: var(--fm-cell);
      opacity: .85;
    }
    /* The refill clock, under the button and on the same frame as the
       rest of the lobby's readouts. Set in the tabular face because
       it changes every second and the digits must not shuffle the row
       sideways as they do. */
    .find-match-energy {
      margin-top: 9px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      padding: 5px 11px;
      border-radius: 999px;
      border: 1px solid rgba(255, 111, 114, 0.22);
      background: rgba(255, 111, 114, 0.07);
      color: #ff9ea0;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: .04em;
    }
    .find-match-energy[hidden] { display: none; }
    .find-match-energy-bolt {
      width: 12px;
      height: 12px;
      flex: none;
      display: block;
    }
    .find-match-energy-label {
      color: rgba(255, 158, 160, 0.72);
      font-weight: 600;
    }
    .find-match-energy-count {
      font-variant-numeric: tabular-nums;
      font-weight: 800;
      /* four glyphs of a monospaced clock, so 9:59 ticking to 10:00
         does not widen the pill */
      min-width: 4ch;
      text-align: left;
    }
    .buy-tetra-btn {
      display: block;
      /* The lobby shell is pointer-events:none so a click goes through to
         the board behind it, and every control inside has to opt back in.
         Find Match does, on its own rule further up; this never did, so
         the one thing offered to a player who cannot afford a round
         rendered, highlighted on hover -- and did nothing when pressed. */
      pointer-events: auto;
      position: relative;
      /* the same layer the button beside it sits on, above the shell's
         glow and grid fills */
      z-index: 11;
      margin-top: 10px;
      padding: 11px 18px;
      border-radius: 10px;
      border: 1px solid rgba(120, 208, 161, 0.4);
      background: rgba(120, 208, 161, 0.12);
      color: #a7e9c6;
      font-size: 12.5px;
      font-weight: 900;
      letter-spacing: .12em;
      text-transform: uppercase;
      text-align: center;
      text-decoration: none;
      transition: background .16s ease, border-color .16s ease, color .16s ease;
    }
    .buy-tetra-btn:hover {
      background: rgba(120, 208, 161, 0.2);
      border-color: rgba(120, 208, 161, 0.6);
      color: #d6f7e6;
    }
    .find-match-requirement[hidden],
    .buy-tetra-btn[hidden] { display: none; }
    @keyframes opponentFindMatchPulse {
      0%, 100% { transform: translateY(0) scale(1); }
      50% { transform: translateY(-1px) scale(1.02); }
    }
    .opponent-overlay-subtext-btn[data-state="cancel"] {
      background: linear-gradient(135deg, #33405f 0%, #1a2234 100%);
      border-color: rgba(140, 164, 212, 0.24);
    }
    .opponent-board-wrap #opponentBoard {
      pointer-events: none;
    }
    .opponent-board-wrap.match-finding::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: 2;
      background: radial-gradient(circle at center, rgba(109, 162, 255, 0.12), rgba(0, 0, 0, 0.18));
    }
    .opponent-overlay-subtext-btn:not([data-state="cancel"]):hover {
      transform: translateY(-2px);
      filter: brightness(1.08);
      box-shadow:
        inset 0 1px rgba(255, 255, 255, 0.16),
        0 14px 28px rgba(0, 0, 0, 0.36);
    }
    .opponent-overlay-subtext-btn[data-state="cancel"]:hover {
      transform: translateY(-2px);
      filter: brightness(1.08);
    }
    .opponent-overlay-subtext-btn:focus-visible {
      outline: 2px solid #8fc6ff;
      outline-offset: 3px;
    }
    @media (prefers-reduced-motion: reduce) {
      .opponent-overlay-subtext-btn,
      .opponent-overlay-subtext-btn:not([data-state="cancel"]),
      .opponent-room-spinner {
        animation: none !important;
        transition: none !important;
      }
    }

    /* =========================================================
       GUEST ROOMS -- a private room on the opponent board

       "Invite a Guest" sits under Find Match as the quieter of the
       two: an outline in the house cyan rather than a second filled
       button competing with the one that costs energy. While a room
       is open, the lobby's own contents give way to the room --
       the link to send, who has joined, and Start Battle.

       The lobby card is pointer-events:none so a click reaches the
       board behind it; every control here opts back in, as Find
       Match and Buy $TETRA do.
       ========================================================= */
    .invite-guest-btn {
      position: relative;
      z-index: 11;
      pointer-events: auto;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      width: 100%;
      margin-top: 8px;
      padding: 10px 16px;
      border-radius: 10px;
      border: 1px solid rgba(95, 212, 255, 0.26);
      background: rgba(95, 212, 255, 0.05);
      color: #bfe9ff;
      font-size: 11.5px;
      font-weight: 900;
      letter-spacing: .14em;
      text-transform: uppercase;
      cursor: pointer;
      transition: background var(--tr-transition-fast), border-color var(--tr-transition-fast),
        color var(--tr-transition-fast), transform var(--tr-transition-fast);
    }
    .invite-guest-btn svg {
      width: 16px;
      height: 16px;
      flex: none;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.8;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    .invite-guest-btn:hover { background: rgba(95, 212, 255, 0.11); border-color: rgba(95, 212, 255, 0.44); color: #e2f6ff; transform: translateY(-1px); }
    .invite-guest-btn:disabled { opacity: .55; cursor: default; transform: none; }
    .invite-guest-btn:focus-visible { outline: 2px solid #8fc6ff; outline-offset: 3px; }
    .opponent-lobby-card.is-searching .invite-guest-btn { display: none; }
    .invite-guest-note {
      margin: 8px 0 0;
      font-size: 11.5px;
      line-height: 1.45;
      letter-spacing: .02em;
      color: #e0b872;
      text-align: center;
    }
    .invite-guest-note[hidden] { display: none; }

    .opponent-lobby-card.is-guest-room .opponent-lobby-stats,
    .opponent-lobby-card.is-guest-room .opponent-lobby-center,
    .opponent-lobby-card.is-guest-room .opponent-lobby-status-row { display: none; }
    /* minmax(0, ...) all the way down: an auto track grows to its widest
       child's min-content, and the unbroken link would push every row
       past the card's edge */
    .opponent-lobby-card.is-guest-room {
      border-color: rgba(120, 229, 178, 0.18);
      grid-template-columns: minmax(0, 1fr);
      justify-items: stretch;
    }
    .guest-room {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      gap: 12px;
      width: 100%;
      min-width: 0;
      text-align: center;
    }
    .guest-room > * { min-width: 0; }
    .guest-room[hidden] { display: none; }
    .guest-room-head {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      color: #9fe7c3;
    }
    .guest-room-lock, .room-chat-lock {
      width: 15px;
      height: 15px;
      flex: none;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.9;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    .guest-room-title {
      font-size: 15px;
      font-weight: 900;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: var(--tr-text-primary);
    }
    .guest-room-status {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      min-height: 18px;
      font-size: 12px;
      font-weight: 700;
      letter-spacing: .02em;
      color: var(--text-secondary);
    }
    .guest-room-status span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .guest-room-dot {
      width: 7px;
      height: 7px;
      flex: none;
      border-radius: 50%;
      background: var(--tr-text-muted);
    }
    .guest-room-status[data-tone="wait"] .guest-room-dot { background: var(--tr-yellow); animation: guestRoomPulse 1.6s ease-in-out infinite; }
    .guest-room-status[data-tone="ok"] .guest-room-dot { background: var(--tr-green); }
    .guest-room-status[data-tone="warn"] .guest-room-dot { background: var(--tr-red); }
    .guest-room-status[data-tone="warn"] { color: #ffc2cb; }
    @keyframes guestRoomPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
    .guest-room-guest {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 9px 10px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      border-left: 2px solid rgba(255, 216, 120, 0.5);
      background: rgba(255, 216, 120, 0.04);
      text-align: left;
      transition: border-color var(--tr-transition-standard), background var(--tr-transition-standard);
    }
    .guest-room-guest[hidden] { display: none; }
    .guest-room-guest[data-state="ready"] { border-left-color: rgba(120, 229, 178, 0.6); background: rgba(120, 229, 178, 0.06); }
    .guest-room-guest[data-state="away"] { border-left-color: rgba(255, 111, 130, 0.5); background: rgba(255, 111, 130, 0.04); }
    .guest-room-avatar, .room-chat-avatar {
      display: grid;
      place-items: center;
      flex: none;
      color: #0b1120;
      font-weight: 900;
      background: linear-gradient(150deg, #8cefaa, #2cc65d);
    }
    .guest-room-avatar { width: 30px; height: 30px; border-radius: 9px; font-size: 13px; }
    .guest-room-guest-meta { display: grid; gap: 2px; min-width: 0; }
    .guest-room-guest-name {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-size: 13px;
      font-weight: 800;
      color: var(--tr-text-primary);
    }
    .guest-room-guest-state {
      font-size: 9.5px;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: #ffe3ab;
    }
    .guest-room-guest[data-state="ready"] .guest-room-guest-state { color: #9fe7c3; }
    .guest-room-guest[data-state="away"] .guest-room-guest-state { color: #ffb3bd; }
    .guest-room-link {
      display: flex;
      align-items: center;
      gap: 6px;
      padding: 5px 5px 5px 10px;
      border-radius: 10px;
      border: 1px solid var(--panel-edge);
      background: rgba(6, 11, 21, 0.6);
      pointer-events: auto;
    }
    .guest-room-link-text {
      flex: 1 1 auto;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      text-align: left;
      color: #c6ecff;
      user-select: all;
      -webkit-user-select: all;
    }
    .guest-room-copy {
      flex: none;
      height: 26px;
      padding: 0 10px;
      border-radius: 8px;
      border: 1px solid var(--panel-edge-hi);
      background: rgba(255, 255, 255, 0.04);
      color: var(--tr-text-secondary);
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      cursor: pointer;
      pointer-events: auto;
      transition: background var(--tr-transition-fast), color var(--tr-transition-fast), border-color var(--tr-transition-fast);
    }
    .guest-room-copy:hover { background: rgba(255, 255, 255, 0.08); color: var(--tr-text-primary); }
    .guest-room-copy.is-done { color: #9fe7c3; border-color: rgba(120, 229, 178, 0.45); }
    .guest-room-note[hidden] { display: none; }
    .guest-room-note {
      margin: 0;
      font-size: 11px;
      line-height: 1.5;
      color: var(--tr-text-muted);
    }
    .guest-room-start {
      position: relative;
      z-index: 11;
      width: 100%;
      min-height: 46px;
      padding: 0 14px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      border-radius: 10px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      background: rgba(18, 25, 38, 0.92);
      color: var(--tr-text-muted);
      font-size: 13px;
      font-weight: 900;
      letter-spacing: .14em;
      text-transform: uppercase;
      pointer-events: auto;
      cursor: default;
      transition: background var(--tr-transition-standard), border-color var(--tr-transition-standard),
        color var(--tr-transition-standard), transform var(--tr-transition-fast), filter var(--tr-transition-fast);
    }
    .guest-room-start.is-ready {
      border-color: rgba(183, 153, 255, 0.5);
      background: var(--tr-primary-gradient);
      color: #f8fbff;
      box-shadow: inset 0 1px rgba(255, 255, 255, 0.14), 0 10px 22px rgba(0, 0, 0, 0.32);
      cursor: pointer;
      animation: opponentFindMatchPulse 1.8s ease-in-out infinite;
    }
    .guest-room-start.is-ready:hover { filter: brightness(1.08); transform: translateY(-1px); }
    .guest-room-start:focus-visible { outline: 2px solid #8fc6ff; outline-offset: 3px; }
    /* Chat is one word and takes its own width; the invite gets the rest,
       which is what keeps "Invite to TetraRank" whole on a 278px card */
    .guest-room-actions { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px; }
    .guest-room-action { padding: 0 11px; }
    .guest-room-action {
      position: relative;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      min-width: 0;
      min-height: 34px;
      padding: 0 8px;
      border-radius: 9px;
      border: 1px solid var(--panel-edge-hi);
      background: rgba(255, 255, 255, 0.03);
      color: var(--tr-text-secondary);
      font-size: 9.5px;
      font-weight: 800;
      letter-spacing: .08em;
      text-transform: uppercase;
      cursor: pointer;
      pointer-events: auto;
      transition: background var(--tr-transition-fast), color var(--tr-transition-fast), border-color var(--tr-transition-fast);
    }
    .guest-room-action span:not(.guest-room-badge) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .guest-room-action:hover:not(:disabled) { background: rgba(255, 255, 255, 0.07); color: var(--tr-text-primary); }
    .guest-room-action:disabled { opacity: .5; cursor: default; }
    .guest-room-action svg {
      width: 14px;
      height: 14px;
      flex: none;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.8;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    .guest-room-badge {
      position: absolute;
      top: -7px;
      right: -7px;
      min-width: 18px;
      height: 18px;
      padding: 0 5px;
      display: grid;
      place-items: center;
      border-radius: 999px;
      background: linear-gradient(180deg, #ff6f82, #c42842);
      color: #fff;
      font-size: 10px;
      font-weight: 900;
      letter-spacing: 0;
    }
    .guest-room-badge[hidden] { display: none; }
    .guest-room-close {
      justify-self: center;
      padding: 4px 8px;
      border: 0;
      background: none;
      color: var(--tr-text-muted);
      font-size: 11px;
      font-weight: 700;
      letter-spacing: .06em;
      text-decoration: underline;
      text-underline-offset: 3px;
      cursor: pointer;
      pointer-events: auto;
    }
    .guest-room-close:hover:not(:disabled) { color: var(--tr-text-secondary); }
    .guest-room-close.is-armed { color: #ffb3bd; }
    .guest-room-close:disabled { opacity: .45; cursor: default; }
    /* a guest has no rank and no stars to paint */
    #opponentStars.is-guest { visibility: hidden; }

    /* the result of a room's round: the same screen, no reward row */
    .mr-friendly { display: none; }
    .match-result.is-friendly .mr-friendly {
      display: block;
      margin-top: -6px;
      font-size: 10.5px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      text-align: center;
      color: #9fe7c3;
    }
    .match-result.is-friendly .mr-reward { display: none; }

    /* the room's chat: the game chat's frame, a quieter close button */
    .room-chat-title { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
    .room-chat-lock { color: var(--tr-green); }
    .room-chat-with {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      letter-spacing: .04em;
      text-transform: none;
      color: var(--tr-text-muted);
    }
    .room-chat-close {
      width: 30px;
      height: 30px;
      flex: none;
      display: grid;
      place-items: center;
      padding: 0;
      border-radius: 50%;
      border: 1px solid var(--panel-edge-hi);
      background: rgba(255, 255, 255, 0.03);
      color: var(--tr-text-secondary);
      cursor: pointer;
      transition: background var(--tr-transition-fast), color var(--tr-transition-fast);
    }
    .room-chat-close:hover { background: rgba(255, 255, 255, 0.08); color: var(--tr-text-primary); }
    .room-chat-close svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
    /* doubled class names: the game chat's own rules come later in this
       file at the same weight, and would otherwise win */
    .game-chat-messages.room-chat-messages { height: 380px; }
    .room-chat-avatar { grid-column: 1; width: 22px; height: 22px; border-radius: 7px; font-size: 10px; }
    .game-chat-tag.room-chat-guest-tag { border-color: rgba(120, 229, 178, 0.4); background: rgba(120, 229, 178, 0.12); color: #bdf2d8; }
    /* a guest has no country on record, so no flag box beside the name */
    .match-result.is-friendly #mrOppFlag { display: none; }
    .room-chat-notice {
      padding: 8px 10px;
      border-radius: 10px;
      border: 1px solid rgba(255, 200, 96, 0.28);
      background: rgba(255, 200, 96, 0.07);
      font-size: 12px;
      color: #ffe2ab;
    }
    .room-chat-notice[hidden] { display: none; }
    @media (max-height: 760px) {
      .game-chat-messages.room-chat-messages { height: min(380px, 44vh); }
      .guest-room { gap: 9px; }
      .guest-room-note { display: none; }
    }
    @media (prefers-reduced-motion: reduce) {
      .guest-room-start.is-ready, .guest-room-status[data-tone="wait"] .guest-room-dot { animation: none; }
    }
    #board {
      width: 300px;
      height: 600px;
      flex: none;
    }
    /* The board was the one thing on this screen that never shrank.

       Every chip, the dock, the CA strip and the chat launcher step
       down at (max-height: 900px) and again at 700px, but the
       playfield stayed 300x600 -- so at 150% browser zoom, which
       gives a 1080p screen a 625px-tall viewport, a 626px board hung
       51px below the fold on a page that does not scroll.

       Sized from the viewport with a 600px ceiling, so at every
       height it already fitted nothing moves at all. The canvas
       keeps its 300x600 backing store and the game keeps computing
       in those coordinates; only the box it is painted into
       changes. */
    #board,
    #opponentBoard {
      /* Three ceilings: the design size, the room above and below,
         and the room across.

         The width term is the same overflow one step further out.
         Two boards sit side by side, each with a HOLD and a NEXT
         column, and that row measures 2W + 508px; below about a
         1180px viewport it ran off the right of the page with no
         breakpoint between there and the 1000px reflow to catch it.
         1366 at 125% zoom lands exactly in that gap, and so does
         1600 at 150%. Solving for W and doubling it for the height
         gives 100vw - 548px. */
      height: min(600px, calc(100vh - 150px), calc(100vw - 548px));
      width: auto;
      aspect-ratio: 1 / 2;
    }
    canvas {
      display: block;
      background: linear-gradient(180deg, #0d1320, #0a1019);
      image-rendering: pixelated;
      border: 1px solid rgba(55, 168, 255, 0.22);
      box-shadow: 0 0 0 1px rgba(125, 51, 255, 0.14) inset;
    }
    .title {
      font-size: 30px;
      font-weight: 900;
      margin: 0 0 8px;
      letter-spacing: .12em;
      text-shadow: 0 0 10px rgba(37, 183, 255, 0.35);
    }
    
    @media (max-width: 1300px), (max-height: 900px) {
      /* these tops carry !important because the base dock rule further up sets
         them that way; without it they were silently dead and the stack kept its
         unscaled spacing while the chips themselves shrank */
      .account-chip { transform: scale(0.8); transform-origin: top left; top: 12px !important; left: 12px; }
      /* 12 + 62 * 0.8 */
      .holdings-chip { transform: scale(0.8); transform-origin: top left; top: 62px !important; left: 12px; }
      /* 62 + (44 - 2) * 0.8 */
      .earnings-chip { transform: scale(0.8); transform-origin: top left; top: 96px !important; left: 12px; }
      .scoreboard-panel { top: 12px; right: 12px; width: 250px; }
      .scoreboard-panel .scoreboard-title { padding: 8px 10px; font-size: 13px; }
      .scoreboard-panel .scoreboard-subtitle { font-size: 11px; }
      .scoreboard-panel .scoreboard-list { gap: 8px; }
      .scoreboard-panel .scoreboard-row { padding: 8px 10px; border-radius: 14px; grid-template-columns: 30px 22px 1fr auto; }
      .scoreboard-panel .scoreboard-name { font-size: 12px; }
      .scoreboard-panel .scoreboard-address { font-size: 9px; }
      .scoreboard-panel .scoreboard-score-player { font-size: 15px; min-width: 44px; }
      .scoreboard-panel .scoreboard-score-earner { font-size: 10px; min-width: 44px; }
      .scoreboard-panel .scoreboard-score img { width: 10px; height: 10px; }
      .scoreboard-panel .scoreboard-toggle,
      .scoreboard-panel .scoreboard-find { padding: 6px 8px; font-size: 16px; }
      .scoreboard-panel .scoreboard-mode-toggle { padding: 5px 10px; font-size: 8px; min-width: 92px; }
      .scoreboard-panel .scoreboard-rank { width: 24px; height: 24px; font-size: 11px; }
      .controls-btn { transform: scale(0.8); transform-origin: bottom left; left: 12px; bottom: 12px; }
    }    
    @media (max-width: 1200px), (max-height: 700px) {
      .account-chip { transform: scale(0.6); transform-origin: top left; top: 10px !important; left: 10px; }
      /* 10 + 62 * 0.6 */
      .holdings-chip { transform: scale(0.6); transform-origin: top left; top: 47px !important; left: 10px; }
      /* 47 + (44 - 2) * 0.6 */
      .earnings-chip { transform: scale(0.6); transform-origin: top left; top: 72px !important; left: 10px; }
      .scoreboard-panel { top: 10px; right: 10px; width: 230px; }
      .scoreboard-panel .scoreboard-title { padding: 7px 9px; font-size: 12px; }
      .scoreboard-panel .scoreboard-subtitle { font-size: 10px; }
      .scoreboard-panel .scoreboard-list { gap: 7px; }
      .scoreboard-panel .scoreboard-row { padding: 7px 9px; border-radius: 12px; grid-template-columns: 28px 20px 1fr auto; }
      .scoreboard-panel .scoreboard-name { font-size: 11px; }
      .scoreboard-panel .scoreboard-address { font-size: 8px; }
      .scoreboard-panel .scoreboard-score-player { font-size: 15px; min-width: 40px; }
      .scoreboard-panel .scoreboard-score-earner { font-size: 10px; min-width: 40px; }
      .scoreboard-panel .scoreboard-score img { width: 9px; height: 9px; }
      .scoreboard-panel .scoreboard-toggle,
      .scoreboard-panel .scoreboard-find { padding: 5px 7px; font-size: 15px; }
      .scoreboard-panel .scoreboard-mode-toggle { padding: 4px 8px; font-size: 8px; min-width: 82px; }
      .scoreboard-panel .scoreboard-rank { width: 22px; height: 22px; font-size: 15px; }
      .controls-btn { transform: scale(0.6); transform-origin: bottom left; left: 10px; bottom: 10px; }
    }
    .stat { margin: 12px 0; }
    .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .12em; }
    .value { font-size: 28px; font-weight: 800; margin-top: 4px; }
    .mini {
      margin-top: 14px;
      width: 100%;
      aspect-ratio: 1 / 1;
      background: linear-gradient(180deg, rgba(11, 17, 28, 0.95), rgba(7, 11, 18, 0.95));
      border: 1px solid rgba(37, 183, 255, 0.18);
      border-radius: 16px;
      box-shadow: 0 0 0 1px rgba(125, 51, 255, 0.12) inset;
    }
    .hold-row {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-top: 10px;
    }
    .play-shell {
      display: flex;
      justify-content: center;
      align-items: flex-start;
      min-height: 100%;
    }
    .piece-stack {
      --stack-gap: 8px;
      display: grid;
      grid-template-columns: 1fr;
      gap: var(--stack-gap);
      align-content: start;
      width: 108px;
      margin: 0;
      padding: 0;
      transform: none;
    }
    .piece-card {
      background: linear-gradient(180deg, rgba(18, 25, 37, 0.98), rgba(10, 15, 24, 0.99));
      border: 1px solid var(--border-medium);
      border-radius: 20px;
      padding: 10px;
      box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.025) inset,
        0 10px 24px rgba(0, 0, 0, 0.28);
    }
    /* frame comes from the PANEL FRAME section below */
    .piece-card .mini {
      width: 84px;
      height: 84px;
      margin: 0 auto;
      aspect-ratio: 1 / 1;
    }
    .next-stack {
      display: grid;
      gap: var(--stack-gap);
    }
    .next-stack .piece-card {
      padding: 8px;
    }
    /* Queue shift: each card starts one slot lower and rides up into place, so
       consuming a piece reads as the stack moving rather than snapping. The
       slot height is the card's own height (100%) plus the stack gap, which
       keeps it correct at every breakpoint. */
    .next-stack .piece-card.shift {
      animation: nextShiftUp 240ms cubic-bezier(.2, .85, .3, 1) both;
    }
    .next-stack .piece-card.shift:nth-child(2) { animation-delay: 20ms; }
    .next-stack .piece-card.shift:nth-child(3) { animation-delay: 40ms; }
    .next-stack .piece-card.shift:nth-child(4) { animation-delay: 60ms; }
    @keyframes nextShiftUp {
      from { transform: translateY(calc(100% + var(--stack-gap))); opacity: .3; }
      to { transform: translateY(0); opacity: 1; }
    }
    /* Hold swap: the card turns over to reveal the piece that just went in. */
    .piece-card.swap {
      animation: holdSwapFlip 300ms cubic-bezier(.25, .8, .3, 1) both;
    }
    @keyframes holdSwapFlip {
      0% { transform: perspective(360px) rotateY(0deg) scale(1); }
      44% { transform: perspective(360px) rotateY(-74deg) scale(.86); }
      45% { transform: perspective(360px) rotateY(74deg) scale(.86); }
      100% { transform: perspective(360px) rotateY(0deg) scale(1); }
    }
    @media (prefers-reduced-motion: reduce) {
      .next-stack .piece-card.shift,
      .piece-card.swap { animation: none; }
    }
    .next-stack .piece-card .mini {
      width: 72px;
      height: 72px;
    }
    .piece-card .label { margin-bottom: 10px; }
    .piece-heading {
      position: relative;
      width: 100%;
      margin: 0 0 4px;
      padding-bottom: 2px;
      color: #bcc6d5;
      text-align: center;
      font-family: Arial, Helvetica, sans-serif;
      font-size: 18px;
      font-weight: 800;
      line-height: 1;
      letter-spacing: 0.22em;
      text-transform: uppercase;
      white-space: nowrap;
      animation: none;
      text-shadow: none;
      filter: none;
    }
    .hold-heading,
    .next-heading {
      background: none;
      -webkit-text-fill-color: currentColor;
      filter: none;
      animation: none;
    }
    @media (prefers-reduced-motion: reduce) {
      .piece-heading { animation: none; }
    }

    /* =========================================================
       PANEL FRAME
       One quiet frame for the playfields, the HOLD/NEXT cards and the
       KO / LINES SENT / RANK modules. Depth comes from a hairline edge, a
       soft inner bevel and a diffuse drop shadow -- never from a glow.
       State changes ease the edge up a step; they do not light it.
       Declared after the component rules above so it owns border and
       box-shadow for all of them.
       ========================================================= */
    .board-wrap,
    .piece-card,
    .piece-card .mini,
    .score-chip-main,
    .score-chip-rankbox,
    .score-chip-roundbox {
      position: relative;
      border: 1px solid var(--panel-edge);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 8px 20px rgba(0, 0, 0, 0.28);
      transition:
        border-color var(--tr-transition-standard),
        box-shadow var(--tr-transition-standard);
    }

    /* ---- playfields ---- */
    .board-wrap {
      /* read by the garbage meter and the block overlay, which sit in and
         over exactly this padding; the portrait rules below narrow both */
      --frame-pad: 12px;
      padding: 12px;
      border-radius: 14px;
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.99), rgba(9, 13, 21, 1));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        inset 0 0 0 1px rgba(0, 0, 0, 0.32),
        0 14px 34px rgba(0, 0, 0, 0.32);
    }

    /* ---- HOLD / NEXT cards ---- */
    .piece-card {
      padding: 9px;
      border-radius: 10px;
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.99), rgba(9, 13, 22, 1));
    }
    /* the canvas sits a little deeper than the card that holds it */
    .piece-card .mini {
      border-radius: 6px;
      background: linear-gradient(180deg, rgba(8, 12, 20, 1), rgba(5, 9, 15, 1));
      box-shadow:
        inset 0 0 0 1px rgba(0, 0, 0, 0.40),
        inset 0 1px 6px rgba(0, 0, 0, 0.45);
    }

    /* ---- score modules: KO / LINES SENT / RANK ---- */
    .score-chip {
      gap: 8px;
      padding: 9px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(15, 21, 33, 0.99), rgba(8, 12, 19, 1));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        0 10px 24px rgba(0, 0, 0, 0.30);
    }
    .score-chip-main,
    .score-chip-rankbox,
    .score-chip-roundbox {
      border-radius: 10px;
      background: linear-gradient(180deg, rgba(255, 255, 255, 0.030), rgba(255, 255, 255, 0.010));
    }
    /* Each module owns a colour: an accent bar along its top edge, a faint
       wash beneath it, an accent label and a tinted value. The outline itself
       stays neutral -- the colour identifies the stat, it does not outline it. */
    .score-chip-roundbox {            /* KO */
      --module-accent: #ffc860;
      --module-value: #ffe3ab;
      --module-wash: rgba(255, 200, 96, 0.075);
      --panel-accent: rgba(255, 200, 96, 0.42);
    }
    .score-chip-main {                /* LINES SENT */
      --module-accent: #5fd4ff;
      --module-value: #c6ecff;
      --module-wash: rgba(95, 212, 255, 0.075);
      --panel-accent: rgba(95, 212, 255, 0.42);
    }
    .score-chip-rankbox {             /* RANK */
      --module-accent: #b79cff;
      --module-value: #dcd0ff;
      --module-wash: rgba(152, 116, 255, 0.085);
      --panel-accent: rgba(152, 116, 255, 0.44);
    }
    .score-chip-main,
    .score-chip-rankbox,
    .score-chip-roundbox {
      background:
        radial-gradient(120% 86% at 50% 0%, var(--module-wash), transparent 64%),
        linear-gradient(180deg, rgba(255, 255, 255, 0.030), rgba(255, 255, 255, 0.010));
    }
    /* inset to the corner radius rather than clipping the box, so the stat-pop
       scale on the value is never cut off */
    .score-chip-main::before,
    .score-chip-rankbox::before,
    .score-chip-roundbox::before {
      content: "";
      position: absolute;
      top: 0;
      left: 10px;
      right: 10px;
      height: 2px;
      border-radius: 0 0 2px 2px;
      background: linear-gradient(90deg, transparent, var(--module-accent), transparent);
      opacity: .8;
      pointer-events: none;
    }
    .score-chip-main .label,
    .score-chip-rankbox .label,
    .score-chip-roundbox .label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--module-accent);
      opacity: .92;
    }
    .score-chip-roundbox .value,
    .score-chip-main .value {
      font-weight: 900;
      color: var(--module-value);
    }
    .score-chip-rank {
      font-size: 32px;
      letter-spacing: .06em;
      color: var(--module-value);
      text-shadow: none;
    }
    /* a value that just changed lifts its own edge, then eases back */
    .score-chip-main:has(.value.stat-pop),
    .score-chip-roundbox:has(.value.stat-pop) {
      border-color: var(--panel-accent);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.05),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 8px 20px rgba(0, 0, 0, 0.28);
    }

    /* ---- HOLD / NEXT headings ---- */
    .piece-heading {
      display: flex;
      align-items: center;
      gap: 9px;
      margin: 0 0 6px;
      padding: 0;
      font-size: 12px;
      font-weight: 700;
      letter-spacing: .22em;
      color: var(--tr-text-muted);
      text-shadow: none;
    }
    .piece-heading::before,
    .piece-heading::after {
      content: "";
      position: static;
      flex: 1 1 auto;
      width: auto;
      height: 1px;
      transform: none;
      border-radius: 0;
      background: linear-gradient(90deg, transparent, var(--panel-edge-hi));
    }
    .piece-heading::after { background: linear-gradient(90deg, var(--panel-edge-hi), transparent); }
    .hold-heading,
    .next-heading { color: var(--tr-text-muted); }

    /* Stacked help buttons beside the board: scoring guide above "how to
       play". This was left: -35%, which on a ~324px board put them about
       110px out and left a 70px gap to the board edge -- they read as
       floating loose in the column rather than belonging to the board. A
       fixed offset tucks them against it: 44px button + 12px gap. */
    .controls-flyout {
      position: absolute;
      left: -56px;
      bottom: 10px;
      z-index: 1;
      display: grid;
      justify-items: center;
      gap: 10px;
    }
    .touch-controls {
      left: 130%;
      bottom: -40%;
      transform: translateX(-100%);
      display: none;
      gap: 6px;
      pointer-events: auto;
      user-select: none;
      touch-action: manipulation;
      position: fixed;
    }
    .touch-controls .touch-row {
      display: flex;
      justify-content: center;
      gap: 6px;
    }
    .touch-controls .touch-btn {
      min-width: 45px;
      height: 45px;
      padding: 0 10px;
      border-radius: 12px;
      border: 1px solid rgba(55, 168, 255, 0.18);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.94), rgba(9, 14, 24, 0.98));
      color: #dff4ff;
      font-size: 6px;
      font-weight: 900;
      letter-spacing: .04em;
      box-shadow:
        0 0 0 1px rgba(125, 51, 255, 0.12) inset,
        0 10px 22px rgba(0, 0, 0, 0.28),
        0 0 24px rgba(37, 183, 255, 0.08);
    }
    .touch-controls .touch-btn:active { transform: scale(0.97); filter: brightness(1.08); }
      .touch-controls .touch-btn.primary { background: linear-gradient(180deg, rgba(125, 60, 255, 0.38), rgba(67, 18, 168, 0.55)); }
      .touch-controls .touch-btn.wide { min-width: 65px; }
      .touch-controls .touch-btn.down-btn {
        position: absolute;
        left: -30%;
        top: 0%;
        font-size: 12px;
        background: linear-gradient(180deg, rgba(20, 84, 126, 0.98), rgba(12, 48, 84, 0.98));
      }
    /* the 100px PNG button used to be sized here; both help buttons are now
       styled together in the board help buttons block above. This hover keeps
       the same lift the rest of the chrome uses -- no scale, no glow. */
    .controls-icon:hover {
      translate: 0 -2px;
      filter: brightness(1.06);
    }
    /* (Two earlier copies of the card and image sizing sat here, fighting the
       real rules further down with a different width, a cyan border and a
       violet inset ring. Removed; the single set lives with .controls-card.) */
    .controls-btn::before,
    .profile-toggle::before,
    .controls-close::before,
    .confirm-btn::before,
    .close-btn::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(120deg, transparent 0%, rgba(255,255,255,0.18) 50%, transparent 100%);
      transform: translateX(-120%);
      transition: transform .55s ease;
      pointer-events: none;
    }
    .controls-btn:hover,
    .profile-toggle:hover,
    .controls-close:hover,
    .confirm-btn:hover,
    .close-btn:hover {
      transform: translateY(-2px) scale(1.01);
      filter: brightness(1.08);
      box-shadow:
        0 0 0 1px rgba(255,255,255,0.1) inset,
        0 14px 28px rgba(83, 33, 216, 0.42),
        0 0 36px rgba(125, 60, 255, 0.26);
    }
    .controls-btn:hover::before,
    .profile-toggle:hover::before,
    .controls-close:hover::before,
    .confirm-btn:hover::before,
    .close-btn:hover::before {
      transform: translateX(120%);
    }
    .controls-btn:active,
    .profile-toggle:active,
    .controls-close:active,
    .confirm-btn:active,
    .close-btn:active {
      transform: translateY(0) scale(0.99);
    }
    .controls-panel {
      position: fixed;
      inset: 0;
      background: rgba(2, 5, 11, 0.76);
      backdrop-filter: blur(7px) saturate(0.9);
      -webkit-backdrop-filter: blur(7px) saturate(0.9);
      display: grid;
      place-items: center;
      z-index: 50;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
    }
    .controls-panel.show { opacity: 1; pointer-events: auto; }
    /* A titled frame around one square reference image, not a bare square
       filling the viewport. Both sheets are 1254x1254, so the image keeps a 1:1
       box and the card sizes to it; the max-width caps the image by the space
       left under the title so a short window shrinks it instead of clipping.
       The surface, border, shadow and top hairline come from the shared
       panel-frame selector lists near the top of this file. */
    .controls-card {
      width: min(94vw, 720px);
      max-width: 94vw;
      max-height: 94vh;
      min-height: 0;
      display: grid;
      grid-template-rows: auto 1fr;
      justify-items: center;
      gap: 14px;
      border-radius: 22px;
      padding: 26px 22px 22px;
    }
    .controls-card-title {
      margin: 0;
      font-size: 12px;
      font-weight: 700;
      letter-spacing: .22em;
      text-transform: uppercase;
      text-align: center;
      color: rgba(178, 202, 226, 0.55);
      text-shadow: none;
    }
    /* Quiet ghost circle, matching the other panels. The hover and active
       overrides are scoped through .controls-panel so they outrank the shared
       .controls-close:hover lift further down the file. */
    .controls-card > .controls-close {
      position: absolute;
      top: 14px;
      right: 14px;
      width: 30px;
      height: 30px;
      padding: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 16px;
      line-height: 1;
      cursor: pointer;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.98), rgba(9, 13, 22, 0.99));
      color: rgba(207, 228, 245, 0.62);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .controls-card > .controls-close::before { content: none; }
    .controls-panel .controls-card > .controls-close:hover {
      transform: none;
      filter: none;
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }
    .controls-panel .controls-card > .controls-close:active { transform: none; }
    /* ---- the Tetra Store --------------------------------------------
       Opened from the side menu. The title carries the storefront the
       menu tile uses, in the amber the tile is keyed to. */
    .controls-card-title .store-card-icon {
      width: 17px;
      height: 17px;
      vertical-align: -3px;
      margin-right: 8px;
      color: rgba(255, 216, 120, 0.72);
    }
    /* Landscape, not a column. This opens on a desktop beside a
       board, and a tall narrow dialog left the perks in a single
       scrolling strip with empty screen either side. Wide enough for
       four cards across, so the whole shop is visible at once and
       nothing has to be scrolled past to be compared. */
    .perks-card { width: min(94vw, 960px); grid-template-rows: auto 1fr; }
    .perks-sheet {
      width: 100%;
      min-height: 0;
      align-self: stretch;
      overflow-y: auto;
      scrollbar-width: thin;
      scrollbar-color: rgba(151, 184, 222, 0.22) transparent;
    }
    .perks-sheet::-webkit-scrollbar { width: 8px; }
    .perks-sheet::-webkit-scrollbar-track { background: transparent; }
    .perks-sheet::-webkit-scrollbar-thumb {
      background: rgba(151, 184, 222, 0.18);
      border-radius: 99px;
    }
    /* The balance leads, because it is what every price below is
       measured against -- a price means nothing until you know what
       you already have towards it. */
    .perks-wallet {
      display: grid;
      gap: 3px;
      align-content: start;
      padding: 12px 14px;
      border-radius: 13px;
      border: 1px solid rgba(255, 200, 96, 0.24);
      background: rgba(255, 200, 96, 0.06);
    }
    .perks-wallet-label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: rgba(240, 207, 132, 0.8);
    }
    .perks-wallet-value {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 24px;
      font-weight: 800;
      line-height: 1;
      color: #f0cf84;
    }
    .perks-wallet-unit { font-size: 11px; color: rgba(240, 207, 132, 0.7); }
    .perks-wallet-note {
      margin-top: 3px;
      font-size: 10px;
      line-height: 1.5;
      color: var(--tr-text-muted);
    }
    /* The burn notice. Warm rather than alarming: this is a fact
       about the product, not a warning that something has gone
       wrong, so it is stated in the shop's own colour and left to
       read as ordinary copy. */
    /* The burn notice and the balance sit side by side across the top
       rather than stacked. Both are read once before anything is
       bought, and stacking them pushed the shop itself below the
       fold on a laptop. */
    .perks-head {
      display: grid;
      grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
      align-items: stretch;
      gap: 10px;
      margin-bottom: 12px;
    }
    .perks-burn {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      align-items: start;
      align-content: start;
      gap: 11px;
      padding: 12px 14px;
      border-radius: 13px;
      border: 1px solid rgba(232, 138, 96, 0.26);
      background:
        linear-gradient(180deg, rgba(232, 138, 96, 0.09), rgba(232, 138, 96, 0.03));
    }
    .perks-burn-flame { width: 22px; height: 22px; color: #e0906a; flex: none; }
    .perks-burn-text {
      font-size: 10.5px;
      line-height: 1.55;
      color: var(--tr-text-muted);
    }
    .perks-burn-text strong {
      display: block;
      margin-bottom: 2px;
      font-size: 11.5px;
      font-weight: 800;
      letter-spacing: .01em;
      color: #e8a882;
    }

    /* How prices are worked out. A quiet strip above the shelves,
       shut until asked: it answers a question most players never ask
       and a few care about a great deal. */
    .perks-explain {
      margin-bottom: 11px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.022);
    }
    .perks-explain-summary {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 10px 13px;
      font-size: 10.5px;
      font-weight: 800;
      letter-spacing: .04em;
      color: var(--tr-text-secondary);
      cursor: pointer;
      list-style: none;
      user-select: none;
    }
    .perks-explain-summary::-webkit-details-marker { display: none; }
    .perks-explain-summary:hover { color: #cfe0f2; }
    .perks-explain-icon { width: 14px; height: 14px; flex: none; opacity: .7; }
    .perks-explain-chevron {
      width: 15px; height: 15px; margin-left: auto; flex: none; opacity: .6;
      transition: transform var(--tr-transition-fast);
    }
    .perks-explain[open] .perks-explain-chevron { transform: rotate(180deg); }
    .perks-explain-body {
      padding: 0 13px 12px;
      font-size: 10.5px;
      line-height: 1.55;
      color: var(--tr-text-muted);
    }
    .perks-explain-body p { margin: 0 0 8px; }
    .perks-explain-body strong { color: #cfe0f2; font-weight: 800; }
    .perks-explain-steps {
      margin: 0 0 8px;
      padding: 0;
      list-style: none;
      display: grid;
      gap: 7px;
    }
    .perks-explain-steps li {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      align-items: start;
      gap: 8px;
    }
    .pe-step {
      width: 17px;
      height: 17px;
      display: grid;
      place-items: center;
      border-radius: 50%;
      border: 1px solid rgba(151, 184, 222, 0.28);
      background: rgba(151, 184, 222, 0.08);
      font-size: 9px;
      font-weight: 800;
      color: rgba(207, 224, 242, 0.85);
    }
    .perks-explain-why {
      padding-top: 8px;
      border-top: 1px solid var(--panel-edge);
    }
    /* The player's own figures, so the formula above can be checked
       against the shelf rather than taken on trust. */
    .perks-explain-live {
      margin: 0;
      padding: 8px 10px;
      border-radius: 9px;
      background: rgba(9, 14, 24, 0.45);
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 10px;
      color: rgba(159, 192, 228, 0.85);
    }
    .perks-explain-live[hidden] { display: none; }

    /* The shop floor. Two columns so each perk gets a card with room
       for an icon, which is what makes it read as merchandise rather
       than as a settings list. */
    .perks-list {
      margin: 0;
      padding: 0;
      list-style: none;
      display: grid;
      /* auto-fit with a floor rather than a fixed count, so the same
         rule gives four across in the full-width dialog and folds to
         three, two and one as the viewport narrows -- no breakpoint
         has to know how many perks there are. */
      grid-template-columns: repeat(auto-fit, minmax(178px, 1fr));
      gap: 10px;
    }
    .perk-card {
      position: relative;
      display: grid;
      /* icon, name, note, term, button. The NOTE takes the slack, so
         a two-line description and a one-line one still put their
         buttons on the same baseline across the row. */
      grid-template-rows: auto auto 1fr auto auto;
      gap: 2px;
      padding: 14px 13px 12px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background:
        linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012));
      transition: border-color var(--tr-transition-fast),
        background var(--tr-transition-fast);
    }
    .perk-card:hover { border-color: rgba(151, 184, 222, 0.26); }
    /* The bundle. It keeps the same shape as every other card and is
       marked out by colour alone: eight perks divide evenly into the
       grid, and giving one of them a full-width row would punch a
       hole in the layout to say something a warmer border already
       says. */
    .perk-card.is-feature {
      border-color: rgba(255, 200, 96, 0.22);
      background:
        linear-gradient(180deg, rgba(255, 200, 96, 0.075), rgba(255, 200, 96, 0.02));
    }
    .perk-card.is-feature .perk-term { color: rgba(240, 207, 132, 0.75); }
    .perk-card.is-owned { border-color: rgba(105, 214, 154, 0.26); }

    .perk-icon {
      width: 46px;
      height: 46px;
      margin-bottom: 7px;
      display: grid;
      place-items: center;
      border-radius: 13px;
      border: 1px solid var(--panel-edge);
      background: rgba(9, 14, 24, 0.45);
      color: #9fc0e4;
    }
    .perk-icon svg { width: 27px; height: 27px; }
    /* One icon, three weights. The lead shape is the perk itself,
       "on" marks what it gives you and "off" what it does not --
       so Next Piece 2 and Next Piece 4 are the same drawing with a
       different amount lit. */
    .perk-icon .pi-lead {
      stroke: currentColor; stroke-width: 1.7; fill: rgba(159, 192, 228, 0.1);
    }
    .perk-icon .pi-on { stroke: currentColor; stroke-width: 1.7; fill: none; }
    .perk-icon rect.pi-on { fill: rgba(159, 192, 228, 0.22); stroke: none; }
    .perk-icon .pi-off {
      stroke: currentColor; stroke-width: 1.4; fill: none; opacity: .3;
    }
    .perk-icon rect.pi-off { fill: rgba(159, 192, 228, 0.07); stroke: none; }
    .perk-card.is-owned .perk-icon {
      border-color: rgba(105, 214, 154, 0.26);
      color: #8fe0b0;
    }
    .perk-card.is-feature .perk-icon {
      color: #e8c98a;
      border-color: rgba(255, 200, 96, 0.22);
      background: rgba(255, 200, 96, 0.05);
    }

    .perk-name {
      font-size: 12px;
      font-weight: 800;
      letter-spacing: .02em;
      color: #e4eef8;
    }
    .perk-note {
      font-size: 10.5px;
      line-height: 1.45;
      color: var(--tr-text-muted);
    }
    .perk-term {
      margin-top: 3px;
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .11em;
      text-transform: uppercase;
      color: rgba(151, 184, 222, 0.55);
    }
    .perk-state {
      position: absolute;
      top: 10px;
      right: 10px;
      padding: 3px 7px;
      border-radius: 999px;
      border: 1px solid rgba(105, 214, 154, 0.28);
      background: rgba(105, 214, 154, 0.09);
      font-size: 8.5px;
      font-weight: 800;
      letter-spacing: .06em;
      white-space: nowrap;
      color: #8fe0b0;
    }

    .perk-buy {
      margin-top: 10px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      padding: 9px 11px;
      border-radius: 11px;
      border: 1px solid rgba(232, 138, 96, 0.3);
      background: rgba(232, 138, 96, 0.1);
      color: #e8a882;
      font-size: 11px;
      font-weight: 800;
      white-space: nowrap;
      cursor: pointer;
      transition: background var(--tr-transition-fast),
        border-color var(--tr-transition-fast);
    }
    .perk-buy .perk-flame { width: 13px; height: 13px; flex: none; }
    .perk-buy-price {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11.5px;
      color: #f0c49c;
    }
    .perk-buy:hover:not(:disabled) {
      background: rgba(232, 138, 96, 0.18);
      border-color: rgba(232, 138, 96, 0.5);
    }
    .perk-buy:disabled { opacity: .4; cursor: default; }
    .perk-buy.is-empty {
      border-color: var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      color: var(--tr-text-muted);
      font-weight: 700;
    }
    /* The price and the "i" that explains it, on one line. The row is
       the tooltip's anchor, so the tooltip spans exactly the card's
       width and can never run out past the scrolling shelf's edge. */
    .perk-buy-row {
      position: relative;
      margin-top: 10px;
      display: flex;
      gap: 6px;
    }
    .perk-buy-row .perk-buy { margin-top: 0; flex: 1 1 auto; min-width: 0; }
    /* price − balance = to burn, under the button when the balance helps */
    .perk-balance-sum {
      margin-top: 7px;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      line-height: 1.4;
      color: var(--tr-text-muted);
    }
    .perk-balance-sum s { color: var(--tr-text-secondary); text-decoration-color: rgba(255, 111, 130, 0.7); }
    .perk-balance-used { color: var(--tr-yellow); }
    .perk-balance-sum b { color: var(--tr-green); font-weight: 700; }
    .perk-info {
      flex: none;
      width: 34px;
      padding: 0;
      display: grid;
      place-items: center;
      border-radius: 11px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      color: rgba(159, 192, 228, 0.6);
      cursor: pointer;
      transition: color var(--tr-transition-fast),
        border-color var(--tr-transition-fast),
        background var(--tr-transition-fast);
    }
    .perk-info svg { width: 15px; height: 15px; }
    .perk-info:hover {
      color: #cfe0f2;
      border-color: rgba(151, 184, 222, 0.32);
      background: rgba(151, 184, 222, 0.07);
    }
    .perk-info:focus-visible {
      outline: 2px solid var(--tr-violet-bright);
      outline-offset: 2px;
    }
    .perk-info-tip {
      position: absolute;
      left: 0;
      right: 0;
      bottom: calc(100% + 8px);
      z-index: 3;
      display: grid;
      gap: 3px;
      padding: 9px 11px;
      border-radius: 11px;
      border: 1px solid rgba(151, 184, 222, 0.2);
      background: linear-gradient(180deg, rgba(14, 20, 34, 0.98), rgba(8, 12, 20, 0.99));
      box-shadow: 0 12px 26px rgba(0, 0, 0, 0.38);
      font-size: 10px;
      line-height: 1.45;
      color: var(--tr-text-muted);
      opacity: 0;
      transform: translateY(4px);
      pointer-events: none;
      transition: opacity .15s ease, transform .15s ease;
    }
    .perk-info-tip strong {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11.5px;
      font-weight: 700;
      color: #f0c49c;
    }
    .perk-info-tip em {
      font-style: normal;
      font-size: 8.5px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-cyan);
    }
    /* The copy inside the card is only the content: it is never shown
       there. The store's shelf scrolls and clips anything that crosses
       its edge, so the tooltip itself is #perkTipFloat -- one floating
       layer over the whole store, placed by script beside the "i" and
       kept inside the screen. Taking it out of the row also means it
       can never push the Burn button about. */
    .perk-buy-row > .perk-info-tip { display: none !important; }
    .perk-tip-float {
      position: fixed;
      left: 0;
      top: 0;
      right: auto;
      bottom: auto;
      width: 240px;
      z-index: 60;
      opacity: 0;
      transform: translateY(4px);
      pointer-events: none;
    }
    .perk-tip-float.is-below { transform: translateY(-4px); }
    .perk-tip-float.is-on { opacity: 1; transform: none; }
    .perk-tip-float.has-mascot { --mz: 40px; padding-left: 58px !important; min-height: 60px; }
    .perk-tip-float .mz-tip-img { position: absolute; }

    /* One perk's price worked through, laid over the store card. The
       card is the positioning box and clips it to its own corners, so
       it reads as a detail of the store rather than a second dialog. */
    .perk-price-info {
      position: absolute;
      inset: 0;
      z-index: 6;
      display: grid;
      place-items: center;
      padding: 18px;
      background: rgba(3, 6, 12, 0.72);
      backdrop-filter: blur(3px);
      -webkit-backdrop-filter: blur(3px);
      opacity: 0;
      pointer-events: none;
      transition: opacity .18s ease;
    }
    .perk-price-info.show { opacity: 1; pointer-events: auto; }
    .perk-price-info-card {
      position: relative;
      width: min(100%, 440px);
      max-height: 100%;
      overflow-y: auto;
      display: grid;
      gap: 16px;
      padding: 22px 22px 20px;
      border-radius: 18px;
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-panel-gradient);
      box-shadow: var(--tr-inset-highlight), 0 18px 40px rgba(0, 0, 0, 0.45);
      transform: translateY(6px) scale(.98);
      transition: transform .2s cubic-bezier(.2, .85, .3, 1);
      scrollbar-width: thin;
      scrollbar-color: rgba(151, 184, 222, 0.22) transparent;
    }
    .perk-price-info.show .perk-price-info-card { transform: none; }
    .perk-price-info-close {
      position: absolute;
      top: 12px;
      right: 12px;
      width: 28px;
      height: 28px;
      padding: 0;
      display: grid;
      place-items: center;
      border-radius: 50%;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.98), rgba(9, 13, 22, 0.99));
      color: rgba(207, 228, 245, 0.62);
      font-size: 15px;
      line-height: 1;
      cursor: pointer;
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .perk-price-info-close:hover {
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }
    .perk-price-info-close:focus-visible {
      outline: 2px solid var(--tr-violet-bright);
      outline-offset: 2px;
    }
    .ppi-head {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      align-items: center;
      gap: 13px;
      padding-right: 32px;
    }
    .ppi-icon { margin-bottom: 0; }
    .ppi-heading { display: grid; gap: 2px; min-width: 0; }
    .ppi-kicker {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--tr-cyan);
    }
    .ppi-title {
      margin: 0;
      font-size: 15px;
      font-weight: 800;
      letter-spacing: .02em;
      color: #e4eef8;
    }
    .ppi-lead {
      margin: 0;
      font-size: 11px;
      line-height: 1.5;
      color: var(--tr-text-muted);
    }
    .ppi-steps {
      margin: 0;
      padding: 13px 14px;
      list-style: none;
      display: grid;
      gap: 11px;
      border-radius: 13px;
      border: 1px solid var(--panel-edge);
      background: rgba(9, 14, 24, 0.45);
    }
    .ppi-step {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr) auto;
      align-items: start;
      gap: 10px;
    }
    .ppi-step .pe-step { margin-top: 1px; }
    .ppi-step-text { display: grid; gap: 1px; min-width: 0; }
    .ppi-step-label {
      font-size: 11px;
      font-weight: 700;
      color: #cfe0f2;
    }
    .ppi-step-sub {
      font-size: 9.5px;
      line-height: 1.4;
      color: var(--tr-text-muted);
    }
    .ppi-step-value {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      text-align: right;
      white-space: nowrap;
      color: rgba(159, 192, 228, 0.9);
    }
    .ppi-step.is-final .ppi-step-label { color: #f0c49c; }
    .ppi-step.is-final .ppi-step-value { color: #f0c49c; font-weight: 700; }
    .ppi-total {
      display: flex;
      align-items: baseline;
      flex-wrap: wrap;
      gap: 6px 12px;
      padding: 12px 14px;
      border-radius: 13px;
      border: 1px solid rgba(232, 138, 96, 0.26);
      background: rgba(232, 138, 96, 0.07);
    }
    .ppi-total-label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: rgba(232, 168, 130, 0.85);
    }
    .ppi-total-value {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 22px;
      font-weight: 800;
      line-height: 1;
      color: #f0c49c;
    }
    .ppi-total-unit { font-size: 11px; color: rgba(240, 196, 156, 0.7); }
    .ppi-total-usd {
      margin-left: auto;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      color: var(--tr-text-muted);
    }
    .ppi-total-usd:empty { display: none; }
    .ppi-pay { display: grid; gap: 8px; padding: 0 2px; }
    .ppi-pay:empty { display: none; }
    .ppi-pay-row {
      display: flex;
      justify-content: space-between;
      align-items: baseline;
      gap: 12px;
      font-size: 11px;
      color: var(--tr-text-secondary);
    }
    .ppi-pay-row b {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-weight: 700;
      text-align: right;
      color: #e4eef8;
    }
    .ppi-pay-row.is-balance b { color: #f0cf84; }
    .ppi-pay-row.is-save b { color: #8fe0b0; }
    .ppi-note {
      margin: 0;
      padding-top: 12px;
      border-top: 1px solid var(--panel-edge);
      font-size: 10px;
      line-height: 1.55;
      color: var(--tr-text-muted);
    }
    /* Narrow enough that the head cannot hold two columns, and the
       feature row cannot hold its button beside the text. */
    @media (max-width: 640px) {
      .perks-head { grid-template-columns: minmax(0, 1fr); }
    }
    @media (max-width: 420px) {
      .perks-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
      .perk-card { padding: 12px 11px 11px; }
      .perk-icon { width: 40px; height: 40px; }
      .perk-icon svg { width: 24px; height: 24px; }
      /* two cards across leave about 150px each: the button and the
         "i" give up a little so the price still fits on one line */
      .perk-buy-row { gap: 5px; }
      .perk-buy-row .perk-buy { gap: 4px; padding: 9px 8px; }
      .perk-info { width: 29px; }
      .perk-price-info { padding: 10px; }
      .perk-price-info-card { gap: 13px; padding: 18px 15px 15px; }
      .ppi-steps { padding: 11px 12px; gap: 9px; }
      .ppi-total-value { font-size: 19px; }
    }
    .perks-status {
      margin: 12px 0 0;
      padding: 11px 12px;
      border-radius: 11px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      font-size: 11px;
      line-height: 1.5;
      color: var(--tr-text-secondary);
    }
    .perks-status[hidden] { display: none; }
    .perks-status.is-good {
      border-color: rgba(105, 214, 154, 0.3);
      color: #8fe0b0;
    }
    .perks-status.is-bad {
      border-color: rgba(255, 111, 130, 0.3);
      color: #e8a0a8;
    }
    .perks-status.is-wait {
      border-color: rgba(255, 200, 96, 0.28);
      color: #f0cf84;
    }
    .perks-status.has-sheet { display: grid; gap: 8px; }
    .perk-pay-line { font-weight: 700; }
    .perk-pay-address {
      padding: 9px 10px;
      border-radius: 9px;
      border: 1px solid var(--panel-edge);
      background: rgba(9, 14, 24, 0.7);
      color: var(--tr-text-primary);
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 10px;
      word-break: break-all;
      text-align: left;
      cursor: pointer;
    }
    .perk-pay-address.copied {
      border-color: rgba(105, 214, 154, 0.4);
      color: #8fe0b0;
    }
    /* The amount, sitting above the address and read the same way.
       Larger, because it is the figure that has to be got right. */
    .perk-pay-amount {
      padding: 9px 10px;
      border-radius: 9px;
      border: 1px solid var(--panel-edge);
      background: rgba(9, 14, 24, 0.7);
      color: var(--tr-text-primary);
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 13px;
      font-weight: 700;
      letter-spacing: 0.02em;
      text-align: left;
      cursor: pointer;
    }
    .perk-pay-amount.copied {
      border-color: rgba(105, 214, 154, 0.4);
      color: #8fe0b0;
    }
    .perk-pay-confirm {
      padding: 9px 12px;
      border-radius: 10px;
      border: 1px solid rgba(255, 200, 96, 0.34);
      background: rgba(255, 200, 96, 0.12);
      color: #f0cf84;
      font-size: 11px;
      font-weight: 800;
      cursor: pointer;
    }
    /* A next slot that has not been bought. The padlock is drawn on
       the canvas; this takes the card's fill down so it reads as shut
       rather than as empty. Scoped to the player's own column -- the
       opponent's previews are their board, not a shop window, and
       they never carry this class anyway. */
    .next-column:not(.opponent-next-column) .piece-card.is-locked {
      opacity: .5;
      border-style: dashed;
      background: rgba(255, 255, 255, 0.015);
    }

    /* ---- the controls sheet, built rather than drawn ------------------
       This was controls.png, and it went the same way the scoring sheet
       below it did, for the same reasons: a list of keys is text, and as
       text it reflows, scales, reads out and sits next to the keydown
       listener that has to agree with it.

       Same restraint too. The mock-up both were drawn from glows -- every
       keycap lit from behind, every glyph haloed. Nothing else in this
       interface does that, so the accent here is carried by the keycap
       edge and the icon, and the row itself is the same quiet surface as
       everywhere else.
       ------------------------------------------------------------------ */
    /* Between the image card's 720px and the scoring sheet's 560px: three
       columns of content, but the middle one is a 40px glyph. */
    .key-card { width: min(94vw, 600px); grid-template-rows: auto 1fr; }
    .controls-card-title .key-card-pad {
      width: 17px;
      height: 17px;
      vertical-align: -3px;
      margin-right: 8px;
      fill: currentColor;
      color: rgba(149, 190, 255, 0.7);
    }
    .key-sheet {
      width: 100%;
      min-height: 0;
      align-self: stretch;
      overflow-y: auto;
      scrollbar-width: thin;
      scrollbar-color: rgba(151, 184, 222, 0.22) transparent;
    }
    .key-sheet::-webkit-scrollbar { width: 8px; }
    .key-sheet::-webkit-scrollbar-track { background: transparent; }
    .key-sheet::-webkit-scrollbar-thumb {
      background: rgba(151, 184, 222, 0.18);
      border-radius: 99px;
    }
    .key-rules { margin: 0; padding: 0; list-style: none; }
    /* keys, glyph, wording. The keys column is fixed width and ruled off
       down the right: a control is looked up by the key you are holding,
       so the caps have to line up down the sheet instead of each row
       starting wherever its own caps happen to end. */
    .key-rule {
      display: grid;
      grid-template-columns: 140px 40px 1fr;
      align-items: center;
      gap: 14px;
      padding: 11px 6px;
      border-top: 1px solid var(--panel-edge);
    }
    .key-rule:first-child { border-top: 0; }
    .key-rule-keys {
      align-self: stretch;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 7px;
      padding-right: 14px;
      border-right: 1px solid var(--panel-edge);
    }
    /* Taller than the inline <kbd> in the welcome panel, and scoped so it
       does not drag that one up with it: there a key is a footnote inside
       a sentence, here it is the subject of the row. */
    .key-sheet .key-cap {
      display: grid;
      place-items: center;
      min-width: 38px;
      height: 38px;
      padding: 0 9px;
      border-radius: 10px;
      border: 1px solid var(--rule-edge);
      border-bottom-width: 2px;
      background: linear-gradient(180deg, var(--rule-soft), rgba(0, 0, 0, 0.18));
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 15px;
      font-weight: 800;
      line-height: 1;
      color: #e4eef8;
    }
    /* Space and Shift are wide keys on a real keyboard and the row has
       nothing else in it, so they take the whole column. */
    .key-sheet .key-cap.is-wide {
      flex: 1;
      font-size: 11px;
      letter-spacing: .16em;
      text-transform: uppercase;
    }
    .key-rule-or {
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: rgba(178, 202, 226, 0.4);
    }
    /* Bare glyph, not the tinted tile the scoring rows use for theirs.
       The keycaps beside it are already tinted boxes and two of them a
       row is one too many. */
    .key-rule-icon {
      width: 40px;
      height: 40px;
      display: grid;
      place-items: center;
      color: var(--rule-accent);
    }
    .key-rule-icon svg { width: 25px; height: 25px; fill: currentColor; display: block; }
    .key-rule-text { min-width: 0; display: grid; gap: 4px; }
    .key-rule-name {
      font-size: 14px;
      font-weight: 800;
      letter-spacing: .04em;
      text-transform: uppercase;
      color: #e4eef8;
    }
    .key-rule-note {
      font-size: 12px;
      line-height: 1.5;
      color: rgba(178, 202, 226, 0.62);
    }
    /* A phone keeps all three columns, just narrower. Dropping the glyph
       would be the easy saving and the wrong one -- it is the only part
       of the row that is readable at a glance. */
    @media (max-width: 560px) {
      .key-rule {
        grid-template-columns: 104px 30px 1fr;
        gap: 10px;
        padding: 10px 2px;
      }
      .key-rule-keys { gap: 5px; padding-right: 10px; }
      .key-sheet .key-cap {
        min-width: 30px;
        height: 32px;
        padding: 0 6px;
        border-radius: 8px;
        font-size: 13px;
      }
      .key-sheet .key-cap.is-wide { font-size: 9px; letter-spacing: .1em; }
      .key-rule-or { font-size: 9px; }
      .key-rule-icon { width: 30px; height: 30px; }
      .key-rule-icon svg { width: 21px; height: 21px; }
      .key-rule-name { font-size: 12px; }
      .key-rule-note { font-size: 11px; }
    }
    /* ---- the scoring sheet, built rather than drawn -------------------
       It replaced a 1254x1254 PNG. Two things follow from that and are
       worth stating, because both are easy to undo by accident.

       It is a LIST, so the card cannot keep the image's 1:1 box: the
       sheet stretches to the card and scrolls inside it when the window
       is short, rather than being scaled down until the numbers are
       unreadable.

       It is in the game's own palette, not the reference sheet's. The
       mock-up this was built from glows: every tile has a lit border and
       every figure a halo. That is not how the rest of this interface
       looks and it is not what the panels next to it do, so the accent
       is carried by the icon and the figure alone, and the tile is the
       same quiet surface as everywhere else.
       ------------------------------------------------------------------ */
    /* Narrower than the image card. 720px was the width a square
       reference sheet wanted; five rows of text at that width give
       very short lines with a lot of empty middle. */
    .score-card { width: min(94vw, 560px); grid-template-rows: auto 1fr; }
    .controls-card-title .score-card-star {
      width: 15px;
      height: 15px;
      vertical-align: -2px;
      margin-right: 8px;
      color: rgba(183, 156, 255, 0.72);
    }
    .score-sheet {
      width: 100%;
      min-height: 0;
      align-self: stretch;
      overflow-y: auto;
      scrollbar-width: thin;
      scrollbar-color: rgba(151, 184, 222, 0.22) transparent;
    }
    .score-sheet::-webkit-scrollbar { width: 8px; }
    .score-sheet::-webkit-scrollbar-track { background: transparent; }
    .score-sheet::-webkit-scrollbar-thumb {
      background: rgba(151, 184, 222, 0.18);
      border-radius: 99px;
    }
    .score-rules { margin: 0; padding: 0; list-style: none; }
    /* icon, wording, figure. The figure column is fixed so "+1" and
       "+100" end on the same right edge down the sheet -- ragged
       figures are what makes a table of numbers look like a list of
       unrelated facts. */
    .score-rule {
      display: grid;
      grid-template-columns: 52px 1fr 78px;
      align-items: center;
      gap: 16px;
      padding: 15px 6px;
      border-top: 1px solid var(--panel-edge);
    }
    .score-rule:first-child { border-top: 0; }
    .score-rule-icon {
      width: 52px;
      height: 52px;
      display: grid;
      place-items: center;
      border-radius: 13px;
      border: 1px solid var(--rule-edge);
      background: linear-gradient(180deg, var(--rule-soft), rgba(0, 0, 0, 0.16));
      color: var(--rule-accent);
    }
    .score-rule-icon svg { width: 27px; height: 27px; fill: currentColor; display: block; }
    .score-rule-text { min-width: 0; display: grid; gap: 4px; }
    .score-rule-name {
      font-size: 14px;
      font-weight: 800;
      letter-spacing: .04em;
      text-transform: uppercase;
      color: #e4eef8;
    }
    .score-rule-note {
      font-size: 12px;
      line-height: 1.5;
      color: rgba(178, 202, 226, 0.62);
    }
    /* the combo ladder, which is the one rule a sentence does not carry
       as well as seeing it climb */
    .score-rule-chain {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 6px;
      margin-top: 2px;
      font-size: 11px;
      color: rgba(178, 202, 226, 0.5);
    }
    .score-rule-chain em {
      font-style: normal;
      font-weight: 700;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      padding: 2px 6px;
      border-radius: 6px;
      border: 1px solid var(--rule-edge);
      color: var(--rule-accent);
    }
    .score-rule-value {
      justify-self: end;
      font-size: 26px;
      font-weight: 800;
      line-height: 1;
      letter-spacing: -.01em;
      white-space: nowrap;
      color: var(--rule-accent);
    }
    .score-rule-inf { font-size: 30px; vertical-align: -2px; }
    /* One accent a row, and every one of them is a colour this interface
       already uses: the board cyan, the RANK module's violet, the star
       gold, the online green and the loss red. Three values each rather
       than one and a colour function -- nothing else in this stylesheet
       mixes colours at runtime, and the wash wants tuning per hue
       anyway, since gold at a flat eleven per cent reads brighter than
       violet does.

       The controls sheet shares these five, one hue per family of key, so
       the two sheets cannot drift apart: rotate is the board cyan, move
       the violet, both drops the gold, hold the green. Red stays out of
       it -- in this interface red means a loss. */
    .score-rule[data-tone="line"],
    .key-rule[data-tone="rotate"] {
      --rule-accent: #5fd4ff;
      --rule-edge: rgba(95, 212, 255, 0.26);
      --rule-soft: rgba(95, 212, 255, 0.10);
    }
    .score-rule[data-tone="combo"],
    .key-rule[data-tone="move"] {
      --rule-accent: #b79cff;
      --rule-edge: rgba(183, 156, 255, 0.28);
      --rule-soft: rgba(183, 156, 255, 0.11);
    }
    .score-rule[data-tone="tspin"],
    .key-rule[data-tone="drop"] {
      --rule-accent: #ffc860;
      --rule-edge: rgba(255, 200, 96, 0.24);
      --rule-soft: rgba(255, 200, 96, 0.085);
    }
    .score-rule[data-tone="perfect"],
    .key-rule[data-tone="hold"] {
      --rule-accent: #69d69a;
      --rule-edge: rgba(105, 214, 154, 0.26);
      --rule-soft: rgba(105, 214, 154, 0.10);
    }
    .score-rule[data-tone="death"] {
      --rule-accent: #ff6f82;
      --rule-edge: rgba(255, 111, 130, 0.26);
      --rule-soft: rgba(255, 111, 130, 0.10);
    }
    /* the same ice blue the block itself is drawn in on the board */
    .score-rule[data-tone="block"] {
      --rule-accent: #a9d4ff;
      --rule-edge: rgba(169, 212, 255, 0.26);
      --rule-soft: rgba(169, 212, 255, 0.10);
    }
    /* ---- Handling, under the keys ----
       Stacked rows rather than a grid: a name and its reading on one
       line with the reading pushed right, the note under it, the
       slider full width beneath. Accents follow the key rows above --
       violet for sideways, amber for the drop -- on the reading and
       the slider's fill, never as a glow. */
    .handling {
      margin: 6px 0 14px;
      padding: 16px 16px 6px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.022);
    }
    .handling-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
    }
    .handling-title {
      margin: 0;
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--tr-cyan);
    }
    .handling-reset {
      padding: 6px 11px;
      border-radius: 9px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      color: var(--tr-text-secondary);
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: border-color var(--tr-transition-fast), color var(--tr-transition-fast);
    }
    .handling-reset:hover { border-color: rgba(151, 184, 222, 0.32); color: var(--tr-text-primary); }
    .handling-reset:focus-visible { outline: 2px solid var(--tr-violet-bright); outline-offset: 2px; }
    .handling-lead {
      margin: 8px 0 6px;
      font-size: 12px;
      line-height: 1.5;
      color: rgba(178, 202, 226, 0.62);
    }
    .handling-row {
      --h-accent: #b79cff;
      --h-soft: rgba(183, 156, 255, 0.28);
      display: grid;
      gap: 6px;
      padding: 14px 0 12px;
      border-top: 1px solid var(--panel-edge);
    }
    .handling-lead + .handling-row { border-top: 0; }
    .handling-row[data-tone="drop"] {
      --h-accent: #ffc860;
      --h-soft: rgba(255, 200, 96, 0.26);
    }
    .handling-line {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 12px;
    }
    .handling-name {
      font-size: 13px;
      font-weight: 800;
      letter-spacing: .04em;
      text-transform: uppercase;
      color: #e4eef8;
    }
    .handling-name abbr {
      margin-left: 6px;
      font-size: 10px;
      letter-spacing: .12em;
      text-decoration: none;
      color: rgba(178, 202, 226, 0.5);
      cursor: help;
    }
    .handling-value {
      flex: none;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 13px;
      font-weight: 700;
      color: var(--h-accent);
    }
    .handling-note {
      margin: 0;
      font-size: 12px;
      line-height: 1.5;
      color: rgba(178, 202, 226, 0.62);
    }
    /* The slider: a thin track filled to the thumb in the row's accent
       (--fill is set from script), and a plain round thumb. */
    .handling-slider {
      --fill: 50%;
      width: 100%;
      height: 22px;
      margin: 2px 0 0;
      background: transparent;
      -webkit-appearance: none;
      appearance: none;
      cursor: pointer;
    }
    .handling-slider:focus-visible { outline: 2px solid var(--tr-violet-bright); outline-offset: 4px; border-radius: 6px; }
    .handling-slider::-webkit-slider-runnable-track {
      height: 5px;
      border-radius: 99px;
      background: linear-gradient(90deg, var(--h-accent) 0 var(--fill), rgba(151, 184, 222, 0.16) var(--fill) 100%);
    }
    .handling-slider::-moz-range-track {
      height: 5px;
      border-radius: 99px;
      background: rgba(151, 184, 222, 0.16);
    }
    .handling-slider::-moz-range-progress {
      height: 5px;
      border-radius: 99px;
      background: var(--h-accent);
    }
    .handling-slider::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 16px;
      height: 16px;
      margin-top: -5.5px;
      border-radius: 50%;
      border: 2px solid var(--h-accent);
      background: #0d1422;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
    }
    .handling-slider::-moz-range-thumb {
      width: 13px;
      height: 13px;
      border-radius: 50%;
      border: 2px solid var(--h-accent);
      background: #0d1422;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
    }
    .score-sheet-foot,
    .key-sheet-foot {
      margin: 4px 0 0;
      padding-top: 15px;
      border-top: 1px solid var(--panel-edge);
      text-align: center;
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: rgba(178, 202, 226, 0.38);
    }
    /* A phone holds the icon and the wording; the figure moves under the
       wording rather than squeezing both into a third of the width. */
    @media (max-width: 560px) {
      .score-rule {
        grid-template-columns: 42px 1fr;
        gap: 12px;
        padding: 13px 2px;
      }
      .score-rule-icon { width: 42px; height: 42px; border-radius: 11px; }
      .score-rule-icon svg { width: 22px; height: 22px; }
      .score-rule-value {
        grid-column: 2;
        justify-self: start;
        font-size: 22px;
        margin-top: 2px;
      }
    }
    .controls-col {
      display: grid;
      gap: 12px;
      align-content: start;
      width: 100%;
    }
    .controls-col.scoring {
      grid-column: 3;
      justify-self: stretch;
    }
    .controls-col.controls-main {
      grid-column: 2;
    }
    .controls-col .section-title {
      margin-top: 0;
      text-align: center;
      font-size: 16px;
      background-color: #4312a8;
    }
    .controls-list div {
      width: 100%;
      padding: 10px 12px;
      border-radius: 14px;
      border: 1px solid rgba(55, 168, 255, 0.08);
      background: rgba(255,255,255,.03);
    }
    .controls-item {
      cursor: default;
      transition: transform .18s ease, background .18s ease, border-color .18s ease;
      min-height: 48px;
      display: grid;
      place-items: center;
    }
    .controls-item:hover {
      transform: translateY(-2px);
      background: rgba(255,255,255,.05);
      border-color: rgba(55, 168, 255, 0.22);
    }
    .controls-item.selected {
      background: rgba(37, 183, 255, 0.12);
      border-color: rgba(37, 183, 255, 0.35);
      box-shadow: 0 0 0 1px rgba(37, 183, 255, 0.12) inset, 0 0 18px rgba(37, 183, 255, 0.18);
    }
    .section-title {
      width: 100%;
      text-align: left;
      font-size: 11px;
      color: var(--cyan);
      letter-spacing: .16em;
      text-transform: uppercase;
      margin: 4px 0 2px;
    }
    .controls-close {
      position: relative;
      overflow: hidden;
      border: 0;
      border-radius: 18px;
      padding: 14px 18px;
      background: linear-gradient(180deg, #7d3cff 0%, #5d1fd8 50%, #4312a8 100%);
      color: #f7f3ff;
      font-weight: 900;
      cursor: pointer;
      box-shadow:
        0 0 0 1px rgba(255,255,255,0.08) inset,
        0 10px 22px rgba(83, 33, 216, 0.35),
        0 0 28px rgba(125, 60, 255, 0.18);
    }
    .sidebar-stack {
      display: grid;
      gap: 10px;
    }
    .panel-title {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 10px;
      margin-bottom: 8px;
    }
    .panel-title small {
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: .14em;
    }
    .compact-stat { display: none; }
    .help {
      margin-top: 16px;
      font-size: 13px;
      line-height: 1.6;
      color: var(--muted);
    }
    .overlay {
      position: absolute;
      inset: 24px;
      display: grid;
      place-items: center;
      background: rgba(0, 0, 0, .36);
      border-radius: 10px;
      text-align: center;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
    }
    .overlay.show { opacity: 1; pointer-events: auto; }
    /* the opponent board's copy of the countdown and of the end-of-round
       state: display only, never clickable. It needs a z-index because the
       wrap's ::after wash sits at 2 and the idle label at 3, and an
       auto-z-index overlay would paint underneath both of them. It can never
       swallow a click on the lobby button below it -- pointer-events stay off
       even when shown. */
    .opponent-overlay,
    .opponent-overlay.show {
      pointer-events: none;
      z-index: 4;
    }
    /* The countdown on a watched or replayed seat: display only, above
       the seat's own layers (meter 3, block 21, perfect clear 24, K.O.
       26) and inside its frame. */
    .spectate-countdown,
    .spectate-countdown.show {
      pointer-events: none;
      z-index: 27;
    }
    .spectate-countdown.pvp-countdown { inset: 10px; }
    .overlay.pvp-countdown {
      inset: 18px;
      padding: 14px 12px;
      display: grid;
      place-items: center;
      align-content: center;
      gap: 6px;
      overflow: hidden;
      box-sizing: border-box;
      border-radius: 18px;
      background:
        radial-gradient(circle at 50% 42%, rgba(37, 183, 255, 0.12), transparent 44%),
        linear-gradient(180deg, rgba(5, 10, 19, 0.72), rgba(4, 7, 14, 0.88));
      border: 1px solid rgba(92, 200, 255, 0.26);
      box-shadow:
        inset 0 0 0 1px rgba(255, 255, 255, 0.025),
        inset 0 0 35px rgba(37, 183, 255, 0.05),
        0 0 28px rgba(37, 183, 255, 0.09);
      backdrop-filter: blur(2px);
      -webkit-backdrop-filter: blur(2px);
      isolation: isolate;
    }
    .overlay.pvp-countdown > div {
      width: 100%;
      max-width: 100%;
      min-width: 0;
      box-sizing: border-box;
      text-align: center;
    }
    .overlay.pvp-countdown::before {
      content: "";
      position: absolute;
      inset: 0;
      z-index: -1;
      pointer-events: none;
      background: linear-gradient(90deg, transparent, rgba(84, 202, 255, 0.13), transparent);
      transform: translateX(-120%);
      animation: pvpCountdownScan 2.2s ease-in-out infinite;
    }
    .overlay.pvp-countdown::after {
      content: "";
      position: absolute;
      top: 18%;
      bottom: 18%;
      left: 50%;
      width: 1px;
      z-index: -1;
      pointer-events: none;
      background: linear-gradient(180deg, transparent, rgba(125, 60, 255, 0.22), transparent);
      box-shadow:
        -90px 0 30px rgba(37, 183, 255, 0.05),
        90px 0 30px rgba(125, 60, 255, 0.05);
      opacity: 0.7;
    }
    .overlay.pvp-countdown h2 {
      position: relative;
      margin: 0;
      max-width: 100%;
      padding: 0 8px;
      box-sizing: border-box;
      font-size: clamp(12px, 2.4vw, 15px);
      font-weight: 900;
      line-height: 1.25;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      white-space: normal;
      text-align: center;
      color: #9edfff;
      text-shadow:
        0 0 10px rgba(37, 183, 255, 0.42),
        0 0 24px rgba(37, 183, 255, 0.18);
    }
    .overlay.pvp-countdown h2::before,
    .overlay.pvp-countdown h2::after {
      content: "";
      position: absolute;
      top: 50%;
      width: 28px;
      height: 1px;
      background: linear-gradient(90deg, transparent, rgba(95, 207, 255, 0.75));
    }
    .overlay.pvp-countdown h2::before { right: calc(100% + 6px); }
    .overlay.pvp-countdown h2::after { left: calc(100% + 6px); transform: scaleX(-1); }
    .overlay.pvp-countdown p {
      margin: 6px 0 0;
      min-width: 0;
      max-width: 100%;
      font-variant-numeric: tabular-nums;
      font-size: clamp(28px, 7vw, 48px);
      font-weight: 950;
      line-height: 0.92;
      letter-spacing: 0.02em;
      white-space: nowrap;
      color: #f4fbff;
      background: linear-gradient(180deg, #ffffff 0%, #cdeeff 55%, #7fcfff 100%);
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
      text-shadow: 0 0 14px rgba(37, 183, 255, 0.24);
      filter: drop-shadow(0 0 8px rgba(37, 183, 255, 0.24));
    }
    .overlay.pvp-countdown.countdown-final {
      border-color: rgba(155, 112, 255, 0.48);
      box-shadow:
        inset 0 0 0 1px rgba(255, 255, 255, 0.035),
        inset 0 0 45px rgba(125, 60, 255, 0.08),
        0 0 34px rgba(125, 60, 255, 0.17);
    }
    .overlay.pvp-countdown.countdown-final p { animation: pvpCountdownPulse 0.72s ease-in-out infinite; }
    .overlay.pvp-countdown.countdown-go p {
      color: #8dffd0;
      -webkit-text-fill-color: #8dffd0;
      text-shadow: 0 0 12px rgba(105, 214, 154, 0.8), 0 0 34px rgba(105, 214, 154, 0.48);
      animation: pvpCountdownGo 0.3s ease-out both;
    }
    .overlay.pvp-countdown.cue-ready p {
      color: #78d9ff;
      -webkit-text-fill-color: #78d9ff;
      font-size: clamp(18px, 4.6vw, 28px);
      text-shadow:
        0 0 12px rgba(37, 183, 255, 0.75),
        0 0 30px rgba(37, 183, 255, 0.35);
    }
    .overlay.pvp-countdown.cue-set p {
      color: #b892ff;
      -webkit-text-fill-color: #b892ff;
      font-size: clamp(18px, 4.6vw, 28px);
      text-shadow:
        0 0 12px rgba(125, 60, 255, 0.8),
        0 0 32px rgba(125, 60, 255, 0.42);
    }
    .overlay.pvp-countdown.cue-go p {
      color: #8dffd0;
      -webkit-text-fill-color: #8dffd0;
      font-size: clamp(16px, 4.2vw, 24px);
      text-shadow:
        0 0 14px rgba(105, 214, 154, 0.85),
        0 0 38px rgba(105, 214, 154, 0.5);
    }
    .overlay.pvp-countdown.cue-pop p {
      animation: pvpCuePop 0.42s ease-out;
    }
    @keyframes pvpCountdownScan {
      0% { transform: translateX(-120%); opacity: 0; }
      30% { opacity: 1; }
      70% { opacity: 1; }
      100% { transform: translateX(120%); opacity: 0; }
    }
    @keyframes pvpCountdownPulse {
      0%, 100% { transform: scale(1); filter: drop-shadow(0 0 7px rgba(125, 60, 255, 0.26)); }
      50% { transform: scale(1.08); filter: drop-shadow(0 0 17px rgba(125, 60, 255, 0.58)); }
    }
    @keyframes pvpCountdownGo {
      0% { transform: scale(0.72); opacity: 0; }
      65% { transform: scale(1.12); opacity: 1; }
      100% { transform: scale(1); opacity: 1; }
    }
    @keyframes pvpCuePop {
      0% { transform: scale(0.76); opacity: 0.35; }
      55% { transform: scale(1.12); opacity: 1; }
      100% { transform: scale(1); opacity: 1; }
    }
    @media (orientation: portrait) {
    .music-popover {
      position: absolute;
      bottom: calc(100% + 10px);
      display: grid;
      gap: 10px;
      padding: 12px;
      border-radius: 16px;
      background: var(--tr-panel-gradient);
      border: 1px solid var(--tr-border-subtle);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
      opacity: 0;
      pointer-events: none;
      transition: opacity .18s ease, transform .18s ease;
      min-width: 220px;
    }
    .music-flyout.open .music-popover {
      opacity: 1;
      pointer-events: auto;
      transform: translate3d(var(--music-flyout-x), var(--music-flyout-y), 0) scale(1);
    }

    .music-flyout.right-anchor .music-popover {
      transform-origin: bottom right;
    }
    .music-flyout.right-anchor .music-popover::before {
      left: auto;
      right: 14px;
    }
    .music-flyout.left-anchor .music-popover {
      transform-origin: bottom left;
    }
    .music-flyout.left-anchor .music-popover::before {
      left: 14px;
      right: auto;
    }


      /* the portrait board is ~224-332px wide, so these have to stay narrow */
      .combo-popup{
        top: 12px;
        padding: 4px 9px;
        font-size: 9px;
        letter-spacing: .06em;
      }
      .combo-popup[data-tier="3"] { font-size: 10px; }
      .tspin-popup{
        top: 38px;
        padding: 5px 11px;
        border-radius: 10px;
        font-size: 9px;
        letter-spacing: .14em;
      }
      .perfect-popup{
        top: 66px;
        padding: 5px 11px;
        border-radius: 10px;
        font-size: 9px;
        letter-spacing: .08em;
      }

      .piece-stack{
        margin-top: 0px;
      }

      #controlsVisibilityImage{
        width: 50px;
        height: 50px;
      }

      .controls-visibility-toggle{
        height: 50px;
        width: 50px;
        left: -35%;
        bottom: 0%;
        position: fixed;
      }

      .music-button{
        width: 35px;
        height: 35px;
        font-size: 15px;
        z-index: 220;
        transform: translateY(-20%);
      }

      .account-chip{
        width: 150px;

      }
      .account-flag{
        width: 30px; 
        height: 30px;
      }

      .account-chip .account-name{
        font-size: 10px;
      }

      .account-chip .account-address{
        font-size: 8px;
        width: 80px;
      }
      

    @keyframes rewardPulse {
      0%, 100% { box-shadow:
        0 0 0 1px rgba(125, 51, 255, 0.12) inset,
        0 18px 50px rgba(0, 0, 0, 0.45),
        0 0 32px rgba(37, 183, 255, 0.08);
        transform: translateX(-50%) scale(0.6);
      }
    }
    
      #rewardPoolIcon{
        width: 15px;
        height: 15px;
      }

    .reward-panel{
      position: fixed !important;
      width: 300px;
      left: 50%;
      top: 10%;
      height: 30px;
      transform: translate(-50%);
    }

    .winners-panel{
      width: 100%;
      background: rgba(2, 5, 11, 0.76);
      backdrop-filter: blur(7px) saturate(0.9);
      -webkit-backdrop-filter: blur(7px) saturate(0.9);
    }
    .winners-card{
      width: 95%;
      position: absolute;
    }

      .overlay.show { pointer-events: none; }
    }
    .overlay h2 { margin: 0 0 10px; font-size: 36px; }
    .overlay p { margin: 0; color: #dbe4f0; }
    @media (orientation: portrait) {
      .overlay.calculating {
        inset: 6px;
        padding: 12px 8px;
        border-radius: 10px;
      }
      .overlay.calculating > div { gap: 7px; }
      .overlay.calculating h2 { letter-spacing: .14em; }
      .overlay.calculating h2::before,
      .overlay.calculating h2::after { width: 12px; }
      .overlay.calculating h2::before { right: calc(100% + 6px); }
      .overlay.calculating h2::after { left: calc(100% + 6px); }
      .overlay.calculating .overlay-spinner { width: 30px; height: 30px; }
      .overlay.pvp-countdown {
        inset: 5px;
        padding: 10px 6px;
        gap: 5px;
        border-radius: 7px;
      }
      .overlay.pvp-countdown h2 {
        font-size: clamp(7px, 2.7vw, 10px);
        letter-spacing: 0.13em;
        white-space: nowrap;
      }
      .overlay.pvp-countdown h2::before,
      .overlay.pvp-countdown h2::after {
        width: 12px;
      }
      .overlay.pvp-countdown h2::before { right: calc(100% + 6px); }
      .overlay.pvp-countdown h2::after { left: calc(100% + 6px); }
      .overlay.pvp-countdown p {
        min-width: 0;
        margin-top: 3px;
        font-size: clamp(22px, 8vw, 34px);
        letter-spacing: 0.02em;
      }
      .overlay.pvp-countdown.cue-ready p,
      .overlay.pvp-countdown.cue-set p,
      .overlay.pvp-countdown.cue-go p {
        font-size: clamp(18px, 7vw, 26px);
      }
    }
    .overlay.manual-pause {
      inset: 18px;
      padding: 24px 18px;
      overflow: hidden;
      border-radius: 18px;
      background:
        radial-gradient(circle at 50% 38%, rgba(125, 60, 255, 0.15), transparent 44%),
        linear-gradient(180deg, rgba(7, 10, 19, 0.78), rgba(4, 7, 14, 0.93));
      border: 1px solid rgba(157, 122, 255, 0.34);
      box-shadow:
        inset 0 0 0 1px rgba(255,255,255,0.025),
        inset 0 0 40px rgba(125, 60, 255, 0.07),
        0 0 32px rgba(125, 60, 255, 0.14);
      backdrop-filter: blur(3px);
      -webkit-backdrop-filter: blur(3px);
      isolation: isolate;
    }
    .overlay.manual-pause.pause-closing {
      pointer-events: none !important;
    }
    .overlay.manual-pause.pause-closing .pause-resume-btn {
      pointer-events: none;
    }
    .overlay.manual-pause::before {
      content: "";
      position: absolute;
      inset: 0;
      z-index: -1;
      pointer-events: none;
      background: repeating-linear-gradient(0deg, transparent 0, transparent 7px, rgba(255,255,255,0.012) 8px);
      mask-image: linear-gradient(180deg, transparent, #000 20%, #000 80%, transparent);
    }
    .overlay.manual-pause::after {
      content: "\23F8";
      position: absolute;
      top: 15%;
      left: 50%;
      z-index: -1;
      transform: translateX(-50%);
      font-size: 160px;
      font-weight: 950;
      line-height: 1;
      color: rgba(157, 122, 255, 0.035);
      text-shadow: 0 0 35px rgba(125, 60, 255, 0.08);
      pointer-events: none;
    }
    /* The start screen wears the pause card, with a play mark behind the
       title instead of the pause bars -- it is waiting to begin, not
       holding a game that was stopped. */
    .overlay.manual-pause.initial-start::after { content: "\25B6"; }
    .overlay.manual-pause > div {
      position: relative;
      z-index: 1;
      display: grid;
      justify-items: center;
      gap: 10px;
      width: 100%;
    }
    .overlay.manual-pause h2 {
      margin: 0;
      font-size: clamp(16px, 4.2vw, 26px);
      font-weight: 950;
      line-height: 1;
      letter-spacing: 0.08em;
      white-space: nowrap;
      color: #f4f1ff;
      text-shadow: 0 0 10px rgba(171, 139, 255, 0.48), 0 0 30px rgba(125, 60, 255, 0.25);
    }
    .overlay.manual-pause p {
      margin: 0;
      font-size: clamp(8px, 1.9vw, 11px);
      font-weight: 800;
      line-height: 1.4;
      letter-spacing: 0.11em;
      text-transform: uppercase;
      color: rgba(204, 213, 230, 0.78);
    }
    @media (orientation: portrait) {
      .overlay.manual-pause { inset: 5px; padding: 10px 6px; border-radius: 7px; }
      .overlay.manual-pause::after { top: 20%; font-size: 60px; }
      .overlay.manual-pause h2 { font-size: clamp(12px, 4.8vw, 18px); letter-spacing: 0.04em; }
      .overlay.manual-pause p { font-size: clamp(6px, 2vw, 8px); letter-spacing: 0.05em; }
    }
    @media (prefers-reduced-motion: reduce) {
      .overlay.pvp-countdown::before,
      .overlay.pvp-countdown.countdown-final p,
      .overlay.pvp-countdown.countdown-go p,
      .overlay.pvp-countdown.cue-pop p { animation: none; }
    }
    @media (prefers-reduced-motion: reduce) {
      .overlay.pvp-countdown::before,
      .overlay.pvp-countdown.countdown-final p,
      .overlay.pvp-countdown.countdown-go p,
      .overlay.pvp-countdown.cue-pop p {
        animation: none;
      }
    }
    @media (prefers-reduced-motion: reduce) {
      .overlay.pvp-countdown::before,
      .overlay.pvp-countdown.countdown-final p,
      .overlay.pvp-countdown.countdown-go p {
        animation: none;
      }
    }
    .profile-toggle { display: none; }
    .profile-panel {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,.62);
      display: grid;
      place-items: center;
      z-index: 110;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
    }
    .profile-panel.show { opacity: 1; pointer-events: auto; }
    /* The way in, on the same frame as everything else.
     *
     * This card used to bring its own surface: a blue-tinted edge, a
     * violet inner line and a 40px cyan glow around the outside,
     * none of which the rest of the game has. It is in the shared
     * panel-frame list near the top of this file like every other
     * card, so the background, the hairline border, the shadow and
     * the cyan-to-violet top rule all come from there now. What is
     * left here is its size.
     *
     * It is the first thing a new player sees, which is the argument
     * for it looking like the game rather than like a form in front
     * of the game. */
    .profile-card {
      width: min(92vw, 440px);
      max-height: min(92vh, 820px);
      overflow-y: auto;
      border-radius: 24px;
      padding: 26px 22px 22px;
    }
    .profile-card::-webkit-scrollbar { width: 7px; }
    .profile-card::-webkit-scrollbar-track { background: transparent; }
    .profile-card::-webkit-scrollbar-thumb {
      border-radius: 999px;
      background: rgba(151, 184, 222, 0.18);
    }
    /* The house heading: small, spaced and cyan, the same as the
       profile and player cards. The old one was 24px with a violet
       text-shadow, which is the only glowing type left in the game. */
    .profile-card h2 {
      margin: 0 0 4px;
      font-size: 13px;
      font-weight: 900;
      letter-spacing: .22em;
      text-transform: uppercase;
      text-align: center;
      text-shadow: none;
      color: var(--tr-cyan);
    }
    /* Both kept clear of the close button by padding rather than by
       shifting left, so the heading stays centred on the card. The
       lead was running underneath the × and losing a word to it. */
    .profile-card h2,
    .profile-lead { padding: 0 42px; }
    .profile-lead {
      margin: 0 0 18px;
      text-align: center;
      font-size: 11.5px;
      line-height: 1.5;
      color: rgba(151, 184, 222, 0.66);
    }
    /* The form's one action, so it takes the width of the form.
       A 150px button centred under four full-width fields reads as
       an afterthought rather than as the way forward. */
    .profile-card .profile-actions { margin-top: 20px; }
    .profile-card .confirm-btn {
      width: 100%;
      min-width: 0;
      padding: 13px 16px;
      font-size: 13px;
    }
    /* The fields, on the house edge rather than a blue one, and with
       a focus state -- the old rule set `outline: none` and put
       nothing in its place, so tabbing through this form showed you
       nothing at all. */
    .profile-card .field input,
    .profile-card .field select {
      border-color: var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      transition: border-color var(--tr-transition-standard), background var(--tr-transition-standard);
    }
    .profile-card .field input:hover,
    .profile-card .field select:hover { border-color: rgba(151, 184, 222, 0.28); }
    .profile-card .field input:focus,
    .profile-card .field select:focus {
      border-color: var(--tr-border-cyan);
      background: rgba(95, 212, 255, 0.045);
    }
    /* The close button is styled once, further down, next to the
       country row -- see the note there. A block here was the losing
       half of a pair. */
    /* Confirm keeps its weight -- it is the one action on the panel
       -- but loses the sweep, the lift and the glow. A button that
       moves when you point at it reads as arcade furniture; this one
       just warms up. */
    .profile-card .confirm-btn {
      background: var(--tr-control-gradient);
      border: 1px solid var(--tr-border-cyan);
      box-shadow: var(--tr-inset-highlight);
      letter-spacing: .12em;
      text-transform: uppercase;
      transition: background var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .profile-card .confirm-btn::before { content: none; }
    .profile-card .confirm-btn:hover {
      transform: none;
      filter: none;
      background: var(--tr-control-hover-gradient);
      border-color: rgba(95, 212, 255, 0.5);
      box-shadow: var(--tr-inset-highlight);
    }
    .profile-card .confirm-btn:active { transform: none; }
    /* The panel's two ways out, stacked in the order a newcomer should
       weigh them: sign in first, Continue as Guest as the quieter
       second, with one line saying what a guest gives up. */
    .profile-card .profile-actions {
      flex-direction: column;
      align-items: stretch;
      gap: 10px;
    }
    .profile-card .guest-btn {
      width: 100%;
      padding: 12px 16px;
      border: 1px solid var(--panel-edge);
      border-radius: 12px;
      background: rgba(255, 255, 255, 0.03);
      color: var(--tr-text-secondary);
      font-size: 12px;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
      cursor: pointer;
      transition:
        border-color var(--tr-transition-standard),
        color var(--tr-transition-standard),
        background var(--tr-transition-standard);
    }
    .profile-card .guest-btn:hover {
      border-color: rgba(151, 184, 222, 0.32);
      background: rgba(151, 184, 222, 0.06);
      color: var(--tr-text-primary);
    }
    .profile-card .guest-btn:focus-visible {
      outline: 2px solid var(--tr-violet-bright);
      outline-offset: 2px;
    }
    .guest-note {
      margin: 2px 0 0;
      font-size: 11px;
      line-height: 1.5;
      text-align: center;
      color: var(--tr-text-muted);
    }
    .field { margin-bottom: 14px; }
    .field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; text-transform: uppercase; letter-spacing: .12em; }
    .field input, .field select {
      width: 100%;
      border: 1px solid rgba(55, 168, 255, 0.18);
      background: rgba(255,255,255,.04);
      color: var(--text);
      border-radius: 12px;
      padding: 12px 14px;
      font-size: 15px;
      outline: none;
    }
    /* :user-invalid, not :invalid.
       An empty required field is invalid from the moment it exists,
       so :invalid painted every box red the instant the panel opened
       -- telling a new player they had got four things wrong before
       they had typed anything. :user-invalid waits until the field
       has actually been filled in badly or left blank on a submit.
       An engine that does not know it simply never turns red, which
       is the better of the two failures. */
    .field.required input:invalid:not(:placeholder-shown),
    .field.required select:invalid:not(:placeholder-shown) {
      border-color: rgba(255, 96, 96, 0.75);
      box-shadow: 0 0 0 3px rgba(255, 96, 96, 0.14);
    }
    /* A separate rule, not another line on the one above: a selector
       list containing one thing the engine does not understand is
       dropped whole, which would take the working half with it. This
       half adds the case the placeholder trick misses -- a field
       left blank and then submitted. */
    .field.required input:user-invalid,
    .field.required select:user-invalid {
      border-color: rgba(255, 96, 96, 0.75);
      box-shadow: 0 0 0 3px rgba(255, 96, 96, 0.14);
    }
    .field select {
      appearance: none;
      background-color: rgba(10, 16, 28, 0.98);
      background-image: linear-gradient(45deg, transparent 50%, var(--cyan) 50%), linear-gradient(135deg, var(--cyan) 50%, transparent 50%);
      background-position: calc(100% - 18px) calc(50% - 2px), calc(100% - 12px) calc(50% - 2px);
      background-size: 6px 6px, 6px 6px;
      background-repeat: no-repeat;
      padding-right: 38px;
    }
    .field select option {
      background: #0c1321;
      color: #edf4ff;
    }
    .field select::-webkit-scrollbar {
      width: 10px;
    }
    .field select::-webkit-scrollbar-track {
      background: #0a101b;
    }
    .field select::-webkit-scrollbar-thumb {
      background: linear-gradient(180deg, #33b5ff, #7d33ff);
      border-radius: 999px;
      border: 2px solid #0a101b;
    }
    .field select::-webkit-scrollbar-thumb:hover {
      background: linear-gradient(180deg, #59c8ff, #9a5cff);
    }
    .field select:focus {
      border-color: rgba(37, 183, 255, 0.45);
      box-shadow: 0 0 0 3px rgba(37, 183, 255, 0.12);
    }
    .field input[type="password"] {
      background: rgba(255,255,255,.04);
      border-color: rgba(55, 168, 255, 0.18);
      color: var(--text);
    }
    .field input[type="password"]:focus {
      border-color: rgba(37, 183, 255, 0.45);
      box-shadow: 0 0 0 3px rgba(37, 183, 255, 0.12);
    }
    .sol-address-row {
      display: flex;
      gap: 8px;
      align-items: center;
    }
    .sol-address-row input { flex: 1; min-width: 0; }
    .sol-warning {
      position: relative;
      flex: 0 0 auto;
      width: 30px;
      height: 30px;
      border-radius: 8px;
      border: 1px solid rgba(255, 193, 7, 0.22);
      background: rgba(255, 193, 7, 0.08);
      color: #ffd86b;
      display: grid;
      place-items: center;
      cursor: help;
      box-shadow: 0 0 0 1px rgba(125, 51, 255, 0.08) inset;
    }
    .sol-warning::before { content: '!'; font-weight: 900; font-size: 16px; line-height: 1; }
    .sol-warning-tooltip {
      position: absolute;
      right: 0;
      top: calc(100% + 8px);
      width: min(280px, 72vw);
      padding: 10px 12px;
      border-radius: 12px;
      background: linear-gradient(180deg, rgba(14, 20, 34, 0.98), rgba(8, 12, 20, 0.99));
      border: 1px solid rgba(55, 168, 255, 0.18);
      color: #eaf6ff;
      font-size: 11px;
      line-height: 1.45;
      box-shadow: 0 12px 28px rgba(0,0,0,.35), 0 0 0 1px rgba(125, 51, 255, 0.08) inset;
      opacity: 0;
      pointer-events: none;
      transform: translateY(-4px);
      transition: opacity .15s ease, transform .15s ease;
      z-index: 5;
    }
    .sol-warning:hover .sol-warning-tooltip {
      opacity: 1;
      transform: translateY(0);
    }
    .password-row { display: flex; gap: 8px; align-items: center; }
    .password-row input { flex: 1; min-width: 0; }
    .change-password-btn {
      border: 1px solid rgba(55, 168, 255, 0.18);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.96), rgba(9, 14, 24, 0.98));
      color: #dff4ff;
      border-radius: 10px;
      padding: 10px 12px;
      font-size: 11px;
      font-weight: 900;
      white-space: nowrap;
      cursor: pointer;
      box-shadow: 0 0 0 1px rgba(125, 51, 255, 0.12) inset;
    }
    .change-password-btn:hover { filter: brightness(1.08); }
    .change-password-btn.hidden { display: none; }
    /* ===== CHANGE PASSWORD PANEL =====
       The card is already listed in the shared panel-frame selectors near the top of this
       file, so it inherits the standard surface, border, shadow and top hairline. Nothing
       here re-declares a background, border colour or glow — only geometry and contents. */
    .change-password-panel {
      position: fixed;
      inset: 0;
      background: rgba(2, 5, 11, 0.7);
      backdrop-filter: blur(7px) saturate(0.9);
      -webkit-backdrop-filter: blur(7px) saturate(0.9);
      display: grid;
      place-items: center;
      z-index: 250;
      opacity: 0;
      pointer-events: none;
      transition: opacity .18s ease;
    }
    .change-password-panel.show { opacity: 1; pointer-events: auto; }
    .change-password-card {
      width: min(92vw, 400px);
      border-radius: 22px;
      padding: 30px 26px 26px;
    }
    /* Quiet ghost circle, matching the user profile panel. */
    .change-password-card .close-btn {
      position: absolute;
      top: 14px;
      right: 14px;
      width: 30px;
      height: 30px;
      padding: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 16px;
      line-height: 1;
      cursor: pointer;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.98), rgba(9, 13, 22, 0.99));
      color: rgba(207, 228, 245, 0.62);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .change-password-card .close-btn::before { content: none; }
    .change-password-card .close-btn:hover {
      transform: none;
      filter: none;
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }
    .change-password-card .close-btn:active { transform: none; }
    .change-password-title {
      margin: 0;
      font-size: 12px;
      font-weight: 700;
      letter-spacing: .22em;
      text-transform: uppercase;
      text-align: center;
      color: rgba(178, 202, 226, 0.55);
      text-shadow: none;
    }
    .change-password-note {
      margin: 9px 0 22px;
      text-align: center;
      font-size: 12px;
      line-height: 1.5;
      color: rgba(154, 178, 204, 0.55);
    }
    .change-password-card .field { margin-bottom: 14px; }
    .change-password-card .field label {
      font-size: 10px;
      letter-spacing: .16em;
      color: rgba(154, 178, 204, 0.58);
      margin-bottom: 7px;
    }
    .change-password-card .field input {
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(12, 17, 28, 0.9), rgba(8, 12, 20, 0.95));
      border-radius: 12px;
      padding: 12px 14px;
      font-size: 14px;
      letter-spacing: .02em;
      transition: border-color var(--tr-transition-standard), box-shadow var(--tr-transition-standard);
    }
    .change-password-card .field input::placeholder { color: rgba(154, 178, 204, 0.32); }
    /* An empty required input matches :invalid the moment the panel opens, which painted
       both boxes in the error colour before anything was typed. Flag a field only once it
       actually holds something. */
    .change-password-card .field.required input:invalid {
      border-color: var(--panel-edge);
      box-shadow: none;
    }
    .change-password-card .field.required input:invalid:not(:placeholder-shown) {
      border-color: rgba(255, 111, 130, 0.38);
      box-shadow: none;
    }
    .change-password-card .field.required input:invalid:focus,
    .change-password-card .field input:focus {
      border-color: var(--panel-edge-hi);
      box-shadow: inset 0 0 0 1px rgba(95, 212, 255, 0.07);
    }
    .change-password-actions {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      margin-top: 22px;
    }
    .cp-cancel-btn,
    .cp-confirm-btn {
      padding: 12px 16px;
      border-radius: 12px;
      cursor: pointer;
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      transition: border-color var(--tr-transition-standard), color var(--tr-transition-standard);
    }
    .cp-cancel-btn {
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.98), rgba(9, 14, 24, 0.99));
      color: rgba(197, 214, 232, 0.68);
    }
    .cp-cancel-btn:hover { border-color: rgba(255, 111, 130, 0.3); color: #ffd2d6; }
    .cp-confirm-btn {
      border: 1px solid rgba(152, 116, 255, 0.28);
      background:
        radial-gradient(120% 150% at 50% 0%, rgba(152, 116, 255, 0.13), transparent 70%),
        linear-gradient(180deg, rgba(25, 29, 50, 0.99), rgba(12, 15, 27, 1));
      color: #e4dcff;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 8px 20px rgba(0, 0, 0, 0.3);
    }
    .cp-confirm-btn:hover { border-color: rgba(152, 116, 255, 0.5); color: #f3eeff; }
    .cp-confirm-btn:active { transform: translateY(1px); }
    .cp-confirm-btn.loading { opacity: .85; pointer-events: none; }
    /* The same one-scroller rule the welcome panel needed, for the
       same reason: on a short screen a capped card that scrolls
       inside a fixed panel gives a phone two things to drag, and the
       form is tall enough to hit that in landscape. Here the panel
       scrolls and the card grows. */
    @media (max-height: 620px) {
      .profile-panel {
        place-items: start center;
        padding: 8px;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior: contain;
      }
      .profile-card {
        max-height: none;
        overflow: visible;
      }
    }
    @media (orientation: portrait) {
      .profile-card {
        width: min(94vw, 360px);
        padding: 18px 14px 16px;
        border-radius: 18px;
      }
      /* Sized for the old 24px heading. The heading is a small
         spaced label now, so 18px here would undo it in portrait --
         the one orientation most players arrive in. */
      .profile-card h2 {
        font-size: 12px;
        margin: 0 0 4px;
      }
      .profile-lead { margin-bottom: 14px; font-size: 11px; }
      .field { margin-bottom: 10px; }
      .field label {
        font-size: 10px;
        margin-bottom: 4px;
      }
      .field input,
      .field select {
        padding: 9px 10px;
        font-size: 12px;
        border-radius: 10px;
      }
      .password-row { gap: 6px; }
      .change-password-btn {
        padding: 9px 10px;
        font-size: 9px;
        border-radius: 9px;
      }
      .country-row { gap: 8px; }
      .flag-box {
        width: 34px;
        height: 34px;
      }
      .profile-actions {
        margin-top: 14px;
      }
      .confirm-btn {
        min-width: 120px;
        padding: 10px 14px;
        font-size: 12px;
        border-radius: 10px;
      }
      /* Dead until now: the base rule for this button sits outside
         any media query and later in the file, so it won here too.
         Marked !important rather than moved, because moving it would
         put a fourth block in the running. */
      .profile-card > .close-btn {
        width: 30px !important;
        height: 30px !important;
        top: 10px !important;
        right: 10px !important;
        font-size: 16px !important;
      }
      .profile-card h2,
      .profile-lead { padding: 0 34px; }
    }
    /* (A second copy of the change-password panel rules used to sit here and silently
       override the ones above; removed so the block above is the single source.) */
    /* THE close button for this card, and the only one.
       A `.profile-card .close-btn` block further up was being beaten
       by this rule -- same specificity, later in the file -- which is
       why the × stayed a red circle through two attempts at changing
       it. That block is gone; this is the single source. The style is
       the quiet ghost circle the profile, player and replay cards
       all use. */
    .profile-card > .close-btn {
      position: absolute;
      top: 14px;
      right: 14px;
      z-index: 2;
      width: 34px;
      height: 34px;
      padding: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 18px;
      line-height: 1;
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--panel-edge);
      color: rgba(207, 228, 245, 0.7);
      box-shadow: none;
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .profile-card > .close-btn::before { content: none; }
    .profile-card > .close-btn:hover {
      transform: none;
      filter: none;
      box-shadow: none;
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }
    .profile-card > .close-btn:active { transform: none; }
    .country-row { display: flex; gap: 10px; align-items: center; }
    /* A flag is a rectangle. This was a 44px circle, which crops the
       hoist off every ensign and makes a Nordic cross unreadable --
       and it was the one place in the game that drew a flag that
       way. The players list and the profile card both use a rounded
       3:2 box; so does this now. */
    .flag-box {
      width: 46px;
      height: 31px;
      border-radius: 7px;
      display: grid;
      place-items: center;
      background: rgba(255,255,255,.05);
      border: 1px solid var(--panel-edge);
      overflow: hidden;
      flex: 0 0 auto;
    }
    .flag-box img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }
    .profile-actions { display: flex; gap: 10px; justify-content: center; margin-top: 18px; width: 100%; }
    .profile-actions button {
      border: 0;
      border-radius: 12px;
      padding: 12px 16px;
      font-weight: 800;
      cursor: pointer;
    }
    .confirm-btn {
      min-width: 150px;
      position: relative;
      overflow: hidden;
      background: linear-gradient(180deg, #7d3cff 0%, #5d1fd8 50%, #4312a8 100%);
      color: #f7f3ff;
      box-shadow:
        0 0 0 1px rgba(255,255,255,0.08) inset,
        0 10px 22px rgba(83, 33, 216, 0.35),
        0 0 28px rgba(125, 60, 255, 0.18);
    }
    .close-btn {
      position: relative;
      overflow: hidden;
      background: linear-gradient(180deg, #7d3cff 0%, #5d1fd8 50%, #4312a8 100%);
      color: #f7f3ff;
      border: 0;
    }

    .reward-distribution-flyout {
      position: relative;
      z-index: 1;
      display: grid;
    }
    .reward-distribution-stats {
      margin-bottom: 5px;
      padding: 8px 12px;
      border-radius: 14px;
      background: linear-gradient(180deg, rgba(14, 20, 34, 0.96), rgba(8, 12, 20, 0.98));
      border: 1px solid rgba(55, 168, 255, 0.18);
      color: #dff4ff;
      font-size: 11px;
      font-weight: 900;
      letter-spacing: .12em;
      text-transform: uppercase;
      box-shadow:
        0 0 0 1px rgba(125, 51, 255, 0.12) inset,
        0 12px 30px rgba(0, 0, 0, 0.32),
        0 0 24px rgba(37, 183, 255, 0.08);
    }
    
    .online-players-dot {
      display: inline-block;
      width: 10px;
      height: 10px;
      margin-right: 10px;
      border-radius: 50%;
      background: #24f06a;
      box-shadow: 0 0 10px rgba(36, 240, 106, 0.8);
      vertical-align: middle;
    }
    .total-players-dot {
      display: inline-block;
      width: 10px;
      height: 10px;
      margin-right: 10px;
      border-radius: 50%;
      background: #ffd84a;
      box-shadow: 0 0 10px rgba(255, 216, 74, 0.8);
      vertical-align: middle;
    }
    .reward-distribution-stats + .reward-distribution-stats { margin-top: 0px; }
    .reward-distribution-btn {
      display: inline-flex;
      align-items: center;
      gap: 9px;
      min-height: 44px;
      padding: 0 16px;
      border: 1px solid var(--tr-border-subtle);
      border-radius: 14px;
      background:
        radial-gradient(circle at 20% 0%, rgba(95, 212, 255, 0.08), transparent 46%),
        var(--tr-control-gradient);
      color: #dff4ff;
      font-family: inherit;
      font-size: 12px;
      font-weight: 900;
      letter-spacing: .12em;
      text-transform: uppercase;
      white-space: nowrap;
      cursor: pointer;
      box-shadow:
        var(--tr-inset-highlight),
        0 10px 28px rgba(0, 0, 0, 0.4),
        0 0 20px rgba(95, 212, 255, 0.055);
    }
    .reward-distribution-btn-icon {
      display: grid;
      place-items: center;
      flex: 0 0 auto;
      width: 22px;
      height: 22px;
      border-radius: 50%;
      border: 1px solid rgba(95, 212, 255, 0.28);
      background: rgba(37, 183, 255, 0.10);
      color: #9fe6ff;
      font-size: 12px;
      line-height: 1;
    }
    /* =========================================================
       PERFECT CLEAR -- full-screen celebration
       Fires for your own clear (gold) and, reflected through the LiveBoards
       PERFECT_CLEAR score event, for the opponent's (violet). Purely
       decorative: pointer-events stay off so it never blocks play.
       ========================================================= */
    .pc-fx {
      position: fixed;
      inset: 0;
      z-index: 1500;
      display: grid;
      place-items: center;
      overflow: hidden;
      pointer-events: none;
      opacity: 0;
      visibility: hidden;
      --pc-hot: #ffd86a;
      --pc-warm: #ff8f2e;
      --pc-soft: rgba(255, 200, 90, 0.42);
    }
    .pc-fx.is-opponent {
      --pc-hot: #b79cff;
      --pc-warm: #5f7cff;
      --pc-soft: rgba(152, 116, 255, 0.42);
    }
    .pc-fx.show { visibility: visible; animation: pcFxFade 1.9s ease-out forwards; }
    @keyframes pcFxFade {
      0% { opacity: 0; }
      6% { opacity: 1; }
      74% { opacity: 1; }
      100% { opacity: 0; visibility: hidden; }
    }
    /* centre bloom */
    .pc-fx::before {
      content: "";
      position: absolute;
      inset: 0;
      background: radial-gradient(circle at 50% 50%, var(--pc-soft), transparent 62%);
      opacity: 0;
    }
    .pc-fx.show::before { animation: pcFxBloom 1.9s ease-out forwards; }
    @keyframes pcFxBloom {
      0% { opacity: 0; transform: scale(.35); }
      18% { opacity: 1; transform: scale(1.04); }
      100% { opacity: 0; transform: scale(1.5); }
    }
    /* edge vignette so the wordmark reads over a busy board */
    .pc-fx::after {
      content: "";
      position: absolute;
      inset: 0;
      background: radial-gradient(circle at 50% 50%, transparent 28%, rgba(3, 6, 12, 0.58) 100%);
      opacity: 0;
    }
    .pc-fx.show::after { animation: pcFxTint 1.9s ease-out forwards; }
    @keyframes pcFxTint {
      0% { opacity: 0; }
      14% { opacity: 1; }
      70% { opacity: 1; }
      100% { opacity: 0; }
    }
    .pc-fx-rays {
      position: absolute;
      width: 190vmax;
      height: 190vmax;
      border-radius: 50%;
      background: repeating-conic-gradient(from 0deg, var(--pc-soft) 0deg 2.6deg, transparent 2.6deg 12deg);
      -webkit-mask: radial-gradient(circle, #000 0%, transparent 60%);
      mask: radial-gradient(circle, #000 0%, transparent 60%);
      opacity: 0;
    }
    .pc-fx.show .pc-fx-rays { animation: pcFxRays 1.9s cubic-bezier(.15, .8, .3, 1) forwards; }
    @keyframes pcFxRays {
      0% { opacity: 0; transform: rotate(0deg) scale(.18); }
      16% { opacity: .8; }
      100% { opacity: 0; transform: rotate(40deg) scale(1); }
    }
    .pc-fx-ring {
      position: absolute;
      width: 28vmin;
      height: 28vmin;
      border-radius: 50%;
      border: 6px solid var(--pc-hot);
      opacity: 0;
    }
    .pc-fx.show .pc-fx-ring { animation: pcFxRing 1.15s cubic-bezier(.12, .75, .25, 1) forwards; }
    @keyframes pcFxRing {
      0% { opacity: .9; transform: scale(.14); border-width: 6px; }
      100% { opacity: 0; transform: scale(5.4); border-width: 1px; }
    }
    .pc-fx-bar {
      position: absolute;
      left: 0;
      right: 0;
      height: 2px;
      background: linear-gradient(90deg, transparent, var(--pc-hot), transparent);
      opacity: 0;
    }
    .pc-fx-bar-top { top: calc(50% - 13vmin); }
    .pc-fx-bar-bottom { bottom: calc(50% - 13vmin); }
    .pc-fx.show .pc-fx-bar { animation: pcFxBar 1.9s ease-out forwards; }
    @keyframes pcFxBar {
      0% { opacity: 0; transform: scaleX(0); }
      20% { opacity: 1; transform: scaleX(1); }
      74% { opacity: 1; transform: scaleX(1); }
      100% { opacity: 0; transform: scaleX(1.06); }
    }
    .pc-fx-copy {
      position: relative;
      display: grid;
      justify-items: center;
      gap: 8px;
      max-width: 92vw;
      padding: 0 16px;
      text-align: center;
    }
    .pc-fx-who {
      font-size: clamp(10px, 1.8vw, 15px);
      font-weight: 900;
      letter-spacing: .42em;
      text-transform: uppercase;
      color: var(--pc-hot);
      opacity: 0;
    }
    .pc-fx.show .pc-fx-who { animation: pcFxWho 1.9s ease-out forwards; }
    @keyframes pcFxWho {
      0% { opacity: 0; transform: translateY(10px); }
      22% { opacity: .9; transform: translateY(0); }
      74% { opacity: .9; }
      100% { opacity: 0; }
    }
    .pc-fx-title {
      font-size: clamp(30px, 8vw, 104px);
      font-weight: 900;
      line-height: 1;
      letter-spacing: .10em;
      text-transform: uppercase;
      background: linear-gradient(180deg, #ffffff 0%, var(--pc-hot) 52%, var(--pc-warm) 100%);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      -webkit-text-fill-color: transparent;
      filter: drop-shadow(0 0 22px var(--pc-soft)) drop-shadow(0 5px 0 rgba(0, 0, 0, 0.32));
      opacity: 0;
    }
    .pc-fx.show .pc-fx-title { animation: pcFxTitle 1.9s cubic-bezier(.16, .9, .2, 1) forwards; }
    @keyframes pcFxTitle {
      0% { opacity: 0; transform: scale(2.4) rotate(-5deg); letter-spacing: .46em; }
      12% { opacity: 1; transform: scale(.93) rotate(1.5deg); letter-spacing: .10em; }
      21% { transform: scale(1.05) rotate(0deg); }
      31% { transform: scale(1); }
      76% { opacity: 1; transform: scale(1); }
      100% { opacity: 0; transform: scale(1.13); }
    }
    .pc-fx-sub {
      font-size: clamp(16px, 3.2vw, 34px);
      font-weight: 900;
      letter-spacing: .18em;
      color: #fff;
      text-shadow: 0 0 16px var(--pc-soft);
      opacity: 0;
    }
    .pc-fx.show .pc-fx-sub { animation: pcFxSub 1.9s cubic-bezier(.16, .9, .2, 1) forwards; }
    @keyframes pcFxSub {
      0% { opacity: 0; transform: translateY(16px) scale(.7); }
      26% { opacity: 1; transform: translateY(0) scale(1.06); }
      36% { transform: scale(1); }
      76% { opacity: 1; }
      100% { opacity: 0; transform: scale(1.08); }
    }
    @media (prefers-reduced-motion: reduce) {
      .pc-fx.show .pc-fx-rays,
      .pc-fx.show .pc-fx-ring,
      .pc-fx.show .pc-fx-bar,
      .pc-fx.show::before { animation: none; opacity: 0; }
      .pc-fx.show .pc-fx-title,
      .pc-fx.show .pc-fx-sub,
      .pc-fx.show .pc-fx-who { animation: pcFxFade 1.9s ease-out forwards; }
    }
    .winners-panel {
      position: fixed;
      inset: 0;
      display: grid;
      place-items: center;
      background: rgba(0, 0, 0, 0.68);
      z-index: 80;
      opacity: 0;
      pointer-events: none;
      transition: opacity .18s ease;
    }
    .winners-panel.show { opacity: 1; pointer-events: auto; }
    .winners-card {
      width: min(92vw, 860px);
      max-height: min(88vh, 900px);
      overflow: hidden;
      background: var(--tr-panel-gradient);
      border: 1px solid var(--tr-border-subtle);
      border-radius: 24px;
      padding: 18px;
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
      display: grid;
      grid-template-rows: auto 1fr;
      gap: 12px;
      position: relative;
    }
    .winners-title {
      text-align: center;
      width: 100%;
      font-size: 18px;
      font-weight: 900;
      letter-spacing: .14em;
      color: #dff4ff;
      text-transform: uppercase;
      padding: 10px 12px;
      border-radius: 14px;
    }
    .winners-list {
      display: grid;
      gap: 10px;
      min-height: 0;
      overflow-y: auto;
      overflow-x: hidden;
      padding-right: 2px;
      align-content: start;
    }
    .winner-row {
      display: grid;
      grid-template-columns: 54px 26px minmax(0, 1fr) auto auto;
      align-items: center;
      gap: 10px;
      padding: 12px 14px;
      border-radius: 16px;
      background: linear-gradient(180deg, rgba(255,255,255,0.025), rgba(255,255,255,0.009));
      border: 1px solid rgba(151,184,222,0.085);
    }
    .winner-row:hover {
      background: linear-gradient(180deg, rgba(95,212,255,0.07), rgba(152,116,255,0.045));
      border-color: rgba(95,212,255,0.24);
      box-shadow: inset 0 0 18px rgba(95,212,255,0.025);
    }
    .winner-rank {
      width: 42px;
      height: 42px;
      border-radius: 12px;
      display: grid;
      place-items: center;
      font-weight: 900;
      font-size: 12px;
      color: #fff;
      background: rgba(255,255,255,0.07);
      text-transform: uppercase;
    }
    .winner-flag {
      width: 26px;
      height: 26px;
      border-radius: 50%;
      overflow: hidden;
      background: rgba(255,255,255,0.06);
      display: grid;
      place-items: center;
      flex: 0 0 auto;
    }
    .winner-flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .winner-meta { min-width: 0; }
    .winner-name { font-weight: 900; font-size: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .winner-address { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .winner-reward { font-size: 18px; font-weight: 900; color: #dff4ff; white-space: nowrap; display: flex; align-items: center; gap: 6px; }
    .winner-reward img { width: 15px; height: 15px; display: block; }
    .winner-score {
      width: 112px;
      justify-self: start;
      display: grid;
      place-items: center;
      padding: 10px 8px;
      border-radius: 12px;
      background: linear-gradient(180deg, rgba(152,116,255,0.18), rgba(95,212,255,0.08));
      border: 1px solid rgba(152,116,255,0.38);
      font-size: 20px;
      font-weight: 900;
      color: #f7f3ff;
      letter-spacing: 0;
      box-shadow: 0 0 0 1px rgba(255,255,255,0.06) inset, 0 0 18px rgba(125, 60, 255, 0.16);
    }
    .winners-summary {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 10px;
      width: 40%;
      text-align: center;
      justify-self: center;
      margin: 0 auto;
      padding: 12px 14px;
      border-radius: 16px;
      background: linear-gradient(180deg, rgba(255,255,255,0.025), rgba(255,255,255,0.009));
      border: 1px solid rgba(151,184,222,0.085);
    }
    .winners-summary .summary-box {
      padding: 10px 12px;
      border-radius: 14px;
      background: rgba(255,255,255,0.04);
      border: 1px solid rgba(255,255,255,0.08);
    }
    .winners-summary .summary-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .14em; }
    .winners-summary .summary-value { text-align: center; font-size: 18px; font-weight: 900; color: #f7fbff; margin-top: 4px; }
    .winner-row.gold { border-color: rgba(255, 215, 0, 0.8); box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.28) inset, 0 0 18px rgba(255, 215, 0, 0.22); }
    .winner-row.silver { border-color: rgba(192, 192, 192, 0.8); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14) inset, 0 0 18px rgba(192, 192, 192, 0.18); }
    .winner-row.bronze { border-color: rgba(205, 127, 50, 0.8); box-shadow: 0 0 0 1px rgba(205, 127, 50, 0.22) inset, 0 0 18px rgba(205, 127, 50, 0.18); }
    .winner-row.rank-4,
    .winner-row.rank-5,
    .winner-row.rank-6 {
      border-color: rgba(97, 171, 255, 0.55);
      box-shadow: 0 0 0 1px rgba(97, 171, 255, 0.18) inset, 0 0 18px rgba(97, 171, 255, 0.16);
    }
    .winner-row.gold .winner-rank { background: linear-gradient(180deg, #ffd54a, #ffb300); color: #241500; }
    .winner-row.silver .winner-rank { background: linear-gradient(180deg, #f2f2f2, #bdbdbd); color: #1b1b1b; }
    .winner-row.bronze .winner-rank { background: linear-gradient(180deg, #e0a46a, #b86b2d); color: #2a1404; }
    .winner-close {
      position: absolute;
      top: 14px;
      right: 14px;
      width: 36px;
      height: 36px;
      border-radius: 50%;
      border: 0;
      background: linear-gradient(180deg, rgba(188, 48, 72, 0.96), rgba(114, 24, 45, 0.98));
      color: #fff4f6;
      font-size: 20px;
      display: grid;
      place-items: center;
      cursor: pointer;
      box-shadow:
        inset 0 1px rgba(255,255,255,0.1),
        0 8px 20px rgba(85, 14, 31, 0.34),
        0 0 16px rgba(255,111,130,0.12);
    }

    @media (max-width: 1300px), (max-height: 900px) {
      .bottom-left-dock { transform: scale(0.8); left: 12px; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
    }
    @media (max-width: 1200px), (max-height: 700px) {
      .bottom-left-dock { transform: scale(0.6); left: 10px; bottom: calc(10px + env(safe-area-inset-bottom, 0px)); }
    }
    @media (max-width: 1300px), (max-height: 900px) {
      .ca-strip { transform: translateX(-50%) scale(0.8); transform-origin: bottom center; bottom: calc(14px + env(safe-area-inset-bottom, 0px)); }
    }
    @media (max-width: 1200px), (max-height: 700px) {
      .ca-strip { transform: translateX(-50%) scale(0.6); transform-origin: bottom center; bottom: calc(10px + env(safe-area-inset-bottom, 0px)); }
    }
    .reward-distribution-panel {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,.62);
      display: grid;
      place-items: center;
      z-index: 1000;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
    }
    .reward-distribution-panel.show { opacity: 1; pointer-events: auto; }

    /* =========================================================
       SPECTATOR

       Somebody else's round, over the lobby. It borrows the game's
       own furniture deliberately -- the same board backdrop, the
       same HOLD and NEXT cards, the same quiet panel frame -- so
       that watching reads as the game rather than as a report about
       it. The one thing it adds is the violet "live" accent the
       players list uses for the same state, which is what ties the
       row you clicked to the panel it opened.

       Above the reward panel at 1000. It used to be described as
       sitting below a full-screen version-update gate at 9999, on
       the reasoning that an update was the one thing allowed to
       interrupt watching. That gate is gone -- being on an old build
       is now a strip at 9998 that interrupts nothing -- so this
       panel simply covers the lobby and nothing covers it.
       ========================================================= */
    .spectate-panel {
      position: fixed;
      inset: 0;
      z-index: 1200;
      display: grid;
      place-items: center;
      padding: 18px 16px;
      background: rgba(2, 5, 11, 0.8);
      backdrop-filter: blur(9px) saturate(0.92);
      -webkit-backdrop-filter: blur(9px) saturate(0.92);
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
    }
    .spectate-panel.show { opacity: 1; pointer-events: auto; }
    /* The player's own match timer is pinned at 9999 so it survives
       every other panel. It should not survive this one: while you
       are watching somebody else, a clock counting your own search
       down over the top of their board is two rounds on one screen. */
    body.is-spectating .match-status-overlay { display: none; }
    .spectate-card {
      position: relative;
      /* 100%, not 96vw. The panel's own side padding is the gutter,
         and a vw figure does not know about it -- at 360px wide,
         96vw came to 346 against a 328px content box and the card
         hung two pixels off the edge of the screen. A percentage
         resolves against that content box, so the gutter is honoured
         at every width and the scrollbar cannot skew it either. */
      width: min(100%, 1240px);
      max-height: min(94vh, 960px);
      overflow: hidden;
      display: grid;
      grid-template-rows: auto minmax(0, 1fr);
      gap: 14px;
      padding: 16px 18px 18px;
      border-radius: var(--tr-radius-large);
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-panel-violet-gradient);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
    }
    /* The one piece of pure decoration, and it is a single pixel --
       the same hairline the reward panel wears. */
    .spectate-card::before {
      content: '';
      position: absolute;
      top: 0;
      left: 16%;
      right: 16%;
      height: 1px;
      background: var(--tr-cyan-violet-gradient);
      opacity: .5;
    }

    /* ---- the header strip ---- */
    .spectate-head {
      display: grid;
      grid-template-columns: 1fr auto 1fr;
      align-items: center;
      gap: 14px;
    }
    .spectate-head-left {
      display: flex;
      align-items: center;
      gap: 10px;
      min-width: 0;
    }
    .spectate-live {
      display: inline-flex;
      align-items: center;
      gap: 7px;
      flex: none;
      padding: 5px 11px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid rgba(152, 116, 255, 0.34);
      background: var(--tr-violet-soft);
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--tr-violet-bright);
    }
    .spectate-live-dot {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--tr-violet-bright);
      animation: playersLivePulse 1.9s ease-in-out infinite;
    }
    .spectate-band {
      font-size: 11px;
      font-weight: 700;
      letter-spacing: .08em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .spectate-head-mid {
      display: flex;
      align-items: stretch;
      gap: 10px;
    }
    .spectate-meter {
      display: grid;
      justify-items: center;
      gap: 3px;
      min-width: 128px;
      padding: 7px 14px;
      border-radius: var(--tr-radius-small);
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-control-gradient);
      box-shadow: var(--tr-inset-highlight);
    }
    .spectate-meter-label {
      font-size: 9px;
      font-weight: 700;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .spectate-meter-value {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 15px;
      font-weight: 800;
      font-variant-numeric: tabular-nums;
      color: var(--tr-text-primary);
    }
    /* The money reads gold, as it does on the coin and in the reward
       panel, so the same figure is the same colour wherever it lands. */
    .spectate-meter-reward { border-color: rgba(255, 216, 120, 0.22); }
    .spectate-meter-reward .spectate-meter-value { color: var(--tr-yellow); }
    .spectate-sol { width: 13px; height: 13px; display: block; }
    /* Tabular figures, so a clock ticking from 1:00 to 0:59 does not
       shuffle the panel's whole header sideways. */
    .spectate-clock { font-variant-numeric: tabular-nums; }
    .spectate-back {
      justify-self: end;
      display: inline-flex;
      align-items: center;
      gap: 7px;
      padding: 9px 16px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-control-gradient);
      box-shadow: var(--tr-inset-highlight);
      color: var(--tr-text-secondary);
      font: inherit;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: .06em;
      cursor: pointer;
      transition:
        color var(--tr-transition-standard),
        border-color var(--tr-transition-standard),
        background var(--tr-transition-standard);
    }
    .spectate-back:hover {
      color: var(--tr-text-primary);
      border-color: var(--tr-border-cyan);
      background: var(--tr-control-hover-gradient);
    }

    /* ---- the two seats ---- */
    .spectate-boards {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 16px;
      min-height: 0;
      overflow: auto;
      overscroll-behavior: contain;
      padding: 2px;
    }
    .spectate-boards::-webkit-scrollbar { width: 8px; height: 8px; }
    .spectate-boards::-webkit-scrollbar-track { background: transparent; }
    .spectate-boards::-webkit-scrollbar-thumb {
      border-radius: 999px;
      background: rgba(151, 184, 222, 0.18);
    }
    .spectate-boards::-webkit-scrollbar-thumb:hover { background: rgba(151, 184, 222, 0.3); }
    .spectate-seat {
      display: grid;
      grid-template-rows: auto auto auto;
      align-content: start;
      justify-items: center;
      gap: 11px;
      min-width: 0;
      padding: 13px;
      border-radius: var(--tr-radius-medium);
      border: 1px solid var(--tr-border-subtle);
      background: linear-gradient(180deg, rgba(13, 20, 35, 0.72), rgba(6, 11, 21, 0.78));
    }
    /* The seat you asked for, marked by its edge rather than by
       anything inside it -- the two boards have to stay directly
       comparable. */
    .spectate-seat.is-watched {
      border-color: rgba(152, 116, 255, 0.3);
      background:
        radial-gradient(circle at 50% 0%, rgba(152, 116, 255, 0.09), transparent 44%),
        linear-gradient(180deg, rgba(14, 19, 35, 0.78), rgba(7, 10, 21, 0.82));
    }
    .spectate-player {
      width: 100%;
      display: grid;
      grid-template-columns: 30px minmax(0, 1fr) auto auto;
      align-items: center;
      gap: 10px;
    }
    /* Quiet Report control on each spectator / replay plate. */
    .tr-plate-report {
      flex: none;
      padding: 3px 9px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid rgba(255, 111, 130, 0.38);
      background: var(--tr-red-soft);
      color: var(--tr-red);
      font: inherit;
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .04em;
      line-height: 1.2;
      white-space: nowrap;
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }
    .tr-plate-report:hover,
    .tr-plate-report:focus-visible {
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.6);
      background: rgba(255, 111, 130, 0.24);
      outline: none;
    }
    .tr-plate-report[hidden] { display: none; }
    /* The report dialog must sit above the spectator (1200) and
       replay overlays when opened from a plate. */
    .report-player-panel { z-index: 1300; }
    .spectate-flag {
      width: 30px;
      height: 20px;
      border-radius: 4px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.04);
    }
    .spectate-flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .spectate-ident { min-width: 0; display: grid; gap: 2px; }
    .spectate-name {
      font-size: 14px;
      font-weight: 800;
      color: var(--tr-text-primary);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .spectate-rank {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 10px;
      letter-spacing: .04em;
      color: rgba(151, 184, 222, 0.6);
    }
    .spectate-seat-tag {
      flex: none;
      padding: 3px 9px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid var(--tr-border-subtle);
      font-size: 9px;
      font-weight: 700;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .spectate-seat.is-watched .spectate-seat-tag {
      border-color: rgba(152, 116, 255, 0.3);
      color: var(--tr-violet-bright);
    }
    .spectate-stage {
      display: grid;
      grid-template-columns: auto minmax(0, auto);
      align-items: start;
      gap: 10px;
    }
    .spectate-side {
      display: grid;
      justify-items: center;
      gap: 5px;
      align-content: start;
    }
    .spectate-side-label {
      font-size: 8px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    /* The HOLD and NEXT cards, matching the ones beside a real board
       -- same surface, same radius, just smaller. */
    .spectate-mini {
      width: var(--spectate-mini, 46px);
      height: var(--spectate-mini, 46px);
      border-radius: 7px;
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.99), rgba(9, 13, 22, 1));
    }
    .spectate-board-wrap {
      --frame-pad: 8px;
      position: relative;
      /* The seat's own layers carry z-indexes now (meter 3, block 21,
         perfect clear 24, K.O. 26). Without a stacking context of its own
         they rose above the "Match over" and idle cards laid over the
         seats, which have none; isolating keeps them inside the frame. */
      isolation: isolate;
      padding: 8px;
      border-radius: 12px;
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.99), rgba(9, 13, 21, 1));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        inset 0 0 0 1px rgba(0, 0, 0, 0.32),
        0 14px 34px rgba(0, 0, 0, 0.32);
    }
    /* The canvas keeps its 300x600 backing store and is scaled by CSS,
       exactly as the game's own board is on a narrow screen, so one
       renderer serves every size. */
    .spectate-board {
      display: block;
      width: var(--spectate-board, 240px);
      height: calc(var(--spectate-board, 240px) * 2);
    }
    .spectate-board-note {
      position: absolute;
      inset: 8px;
      display: grid;
      place-items: center;
      border-radius: 8px;
      background: rgba(6, 10, 18, 0.72);
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      transition: opacity var(--tr-transition-standard);
    }
    .spectate-board-note.is-hidden { opacity: 0; pointer-events: none; }
    .spectate-stats {
      width: 100%;
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 7px;
    }
    .spectate-stat {
      display: grid;
      justify-items: center;
      gap: 3px;
      padding: 7px 4px;
      border-radius: var(--tr-radius-small);
      border: 1px solid var(--tr-border-subtle);
      background: rgba(255, 255, 255, 0.022);
    }
    .spectate-stat-name {
      font-size: 8px;
      font-weight: 700;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      white-space: nowrap;
    }
    .spectate-stat-value {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 15px;
      font-weight: 800;
      font-variant-numeric: tabular-nums;
      color: var(--tr-text-primary);
    }
    /* KO decides the round and lines sent only breaks a tie on it, so
       the two counters that matter are the two that are coloured. */
    .spectate-stat:nth-child(1) .spectate-stat-value { color: var(--tr-red); }
    .spectate-stat:nth-child(2) .spectate-stat-value { color: var(--tr-cyan); }
    /* A combo running on the board being watched, and garbage
       leaving it. The bomb count is the only one of the four that
       means something is happening right now rather than something
       that has happened, so it is the one that moves. */
    .spectate-stat-value.is-hot { color: var(--tr-cyan-bright); }
    .spectate-stat-value.is-sending { animation: spectateBombSend 520ms ease-out; }
    @keyframes spectateBombSend {
      0% { transform: none; color: #f0b072; }
      28% { transform: translateY(-3px) scale(1.18); color: #ffcf9a; }
      100% { transform: none; }
    }
    /* The same K.O. stamp the match uses, sized for a watched board.
       Inset matches .spectate-board-wrap's padding so it covers the
       playfield exactly, as it does there. */
    .spectate-ko-flash { inset: 8px; border-radius: 8px; }
    .spectate-ko-flash .ko-flash-label { font-size: clamp(22px, 5.4vw, 40px); }
    /* Bombs thrown between two watched boards are drawn on the same
       fixed layer the match uses, and that layer sits at z-index 62
       -- under the spectator, which is 1200. Raised only while the
       panel is up, because in a match it must stay under the panels
       that open over the board. */
    body.is-spectating .bomb-flight-layer { z-index: 1400; }
    @media (prefers-reduced-motion: reduce) {
      .spectate-stat-value.is-sending { animation: none; }
    }

    /* ---- the result, and the way out ---- */
    .spectate-result {
      position: absolute;
      inset: 0;
      display: grid;
      place-items: center;
      background: rgba(3, 6, 13, 0.78);
      backdrop-filter: blur(5px);
      -webkit-backdrop-filter: blur(5px);
      opacity: 0;
      pointer-events: none;
      transition: opacity var(--tr-transition-standard);
    }
    .spectate-result.is-visible { opacity: 1; pointer-events: auto; }
    .spectate-result-card {
      display: grid;
      justify-items: center;
      gap: 9px;
      padding: 26px 40px;
      border-radius: var(--tr-radius-large);
      border: 1px solid var(--tr-border-violet);
      background: var(--tr-panel-violet-gradient);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
    }
    .spectate-result-label {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .spectate-result-winner {
      font-size: 24px;
      font-weight: 900;
      letter-spacing: .02em;
      color: var(--tr-text-primary);
      text-align: center;
    }
    .spectate-result-note {
      font-size: 11px;
      color: var(--tr-text-secondary);
    }

    /* ---- the final score ---- */
    /* Two columns of numbers with a proportional bar under each pair.
       Two figures side by side say who is higher but not by how much,
       and "42 to 13" and "42 to 40" are very different rounds -- the
       bar is what makes the margin readable from across a room, which
       is the audience live mode is for. */
    .spectate-score {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
      align-items: center;
      gap: 9px 16px;
      width: 100%;
      padding: 16px 18px;
      border-radius: var(--tr-radius-medium);
      border: 1px solid var(--tr-border-subtle);
      background: rgba(255, 255, 255, 0.022);
    }
    .spectate-result.no-score .spectate-score,
    .spectate-result.no-score .spectate-result-reward { display: none; }
    .spectate-score-name {
      min-width: 0;
      font-size: 12px;
      font-weight: 800;
      letter-spacing: .02em;
      text-align: center;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      color: var(--tr-text-secondary);
      border-radius: var(--tr-radius-pill);
      padding: 3px 8px;
      transition: color var(--tr-transition-standard), background var(--tr-transition-standard);
    }
    /* Each seat keeps the colour it has all round -- left cyan, right
       violet -- so the score table and the boards above it are
       obviously about the same two people. */
    .spectate-score-name.spectate-score-a { color: var(--tr-cyan); }
    .spectate-score-name.spectate-score-b { color: var(--tr-violet-bright); }
    .spectate-score-name.is-winner {
      color: var(--tr-text-primary);
      background: rgba(255, 255, 255, 0.05);
    }
    /* Where the coin lands. A short flare rather than a standing
       glow: it marks the arrival and then gets out of the way. */
    .spectate-score-name.is-paid {
      animation: spectatePaid 900ms cubic-bezier(.2, .8, .2, 1);
    }
    @keyframes spectatePaid {
      0% { background: rgba(255, 216, 120, 0); }
      22% { background: rgba(255, 216, 120, 0.3); color: #fff4d6; }
      100% { background: rgba(255, 255, 255, 0.05); }
    }
    .spectate-score-head {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .spectate-score-value {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      /* 63 against 1 should sit on the same axis */
      font-variant-numeric: tabular-nums;
      font-size: 26px;
      font-weight: 900;
      line-height: 1;
      text-align: center;
      color: var(--tr-text-secondary);
    }
    .spectate-score-value.is-ahead { color: var(--tr-text-primary); }
    .spectate-score-label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      white-space: nowrap;
      text-align: center;
      color: var(--tr-text-muted);
    }
    .spectate-score-bar {
      grid-column: 1 / -1;
      display: flex;
      height: 5px;
      border-radius: 999px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.05);
    }
    .spectate-score-fill {
      height: 100%;
      transition: width 520ms cubic-bezier(.2, .8, .2, 1);
    }
    .spectate-score-fill.is-a { background: linear-gradient(90deg, rgba(95, 212, 255, 0.5), var(--tr-cyan)); }
    .spectate-score-fill.is-b { background: linear-gradient(90deg, var(--tr-violet), rgba(152, 116, 255, 0.5)); }
    /* What the round paid, on its own line under the score. It is
       also where the coin takes off from, so it has to be a real box
       with a measurable rectangle rather than loose text. */
    .spectate-result-reward {
      display: inline-flex;
      align-items: center;
      gap: 12px;
      padding: 8px 16px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid rgba(255, 216, 120, 0.24);
      background: rgba(255, 216, 120, 0.06);
    }
    .spectate-result-reward-label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .spectate-result-reward-value {
      display: inline-flex;
      align-items: center;
      gap: 7px;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-variant-numeric: tabular-nums;
      font-size: 15px;
      font-weight: 800;
      color: var(--tr-yellow);
    }
    .spectate-result-coin { width: 15px; height: 15px; display: block; }
    /* The card leans towards whichever side won, so the result reads
       before a single number has been. */
    .spectate-result.won-a .spectate-result-card { border-color: rgba(95, 212, 255, 0.4); }
    .spectate-result.won-b .spectate-result-card { border-color: rgba(152, 116, 255, 0.44); }
    .spectate-result.won-a .spectate-result-winner { color: var(--tr-cyan-bright); }
    .spectate-result.won-b .spectate-result-winner { color: var(--tr-violet-bright); }
    /* The coin in flight. Fixed, because it crosses from the reward
       row to a name and both are measured in viewport coordinates. */
    .spectate-coin {
      position: fixed;
      left: 0;
      top: 0;
      width: 28px;
      height: 28px;
      z-index: 1400;
      opacity: 0;
      pointer-events: none;
    }

    /* ---- live mode with nothing on yet ---- */
    .spectate-idle {
      position: absolute;
      inset: 0;
      display: grid;
      place-items: center;
      background: rgba(3, 6, 13, 0.72);
      opacity: 0;
      pointer-events: none;
      transition: opacity var(--tr-transition-standard);
    }
    .spectate-idle.is-visible { opacity: 1; pointer-events: auto; }
    .spectate-idle-card {
      display: grid;
      justify-items: center;
      gap: 12px;
      padding: 30px 44px;
      border-radius: var(--tr-radius-large);
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-panel-gradient);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
    }
    .spectate-idle-pulse { display: flex; gap: 7px; }
    .spectate-idle-pulse span {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--tr-violet-bright);
      animation: spectateIdlePulse 1.25s ease-in-out infinite;
    }
    .spectate-idle-pulse span:nth-child(2) { animation-delay: .16s; }
    .spectate-idle-pulse span:nth-child(3) { animation-delay: .32s; }
    @keyframes spectateIdlePulse {
      0%, 100% { opacity: .22; }
      50% { opacity: 1; }
    }
    .spectate-idle-title {
      font-size: 16px;
      font-weight: 800;
      letter-spacing: .04em;
      color: var(--tr-text-primary);
    }
    .spectate-idle-note {
      font-size: 11px;
      color: var(--tr-text-muted);
    }

    /* Two boards will not fit side by side below about this width, so
       they stack and the panel scrolls -- the same answer the players
       list gives when it runs out of row. */
    @media (max-width: 1080px) {
      .spectate-boards { grid-template-columns: minmax(0, 1fr); }
      .spectate-head {
        grid-template-columns: minmax(0, 1fr);
        justify-items: center;
        gap: 10px;
      }
      .spectate-back { justify-self: center; }
    }
    @media (max-width: 720px), (max-height: 760px) {
      .spectate-panel { padding: 10px 8px; }
      .spectate-card { padding: 12px 12px 14px; gap: 10px; }
      .spectate-board { --spectate-board: 168px; }
      .spectate-mini { --spectate-mini: 34px; }
      .spectate-meter { min-width: 104px; padding: 6px 10px; }
      .spectate-meter-value { font-size: 13px; }
      .spectate-result-winner { font-size: 19px; }
      .spectate-result-card { padding: 20px 24px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .spectate-panel,
      .spectate-result,
      .spectate-board-note { transition: none; }
      .spectate-live-dot,
      .players-live-dot,
      .players-row.is-finding .players-status,
      .spectate-idle-pulse span,
      .spectate-score-name.is-paid,
      .live-hud-dot { animation: none; }
      .spectate-score-fill { transition: none; }
    }

    /* =========================================================
       LIVE MODE (F2)

       The lobby rearranged for an audience. Four things are on
       screen and everything else is gone: the two player counts, the
       chat, the contract address and the players list. None of them
       is a control -- they are all things a viewer reads -- and the
       things that ARE controls (the account chip, the holdings chip,
       the daily spin button, the chat's own input) are the things
       taken away.

       The spectator sits INSET between those four rather than
       covering them, which is the whole point: the counters and the
       list have to stay readable while a match is running, and that
       is exactly the state in which the old counters -- inside the
       opponent board's idle card -- were hidden.

       The rails are named here once and read by every rule below, so
       widening the list is one number rather than four.
       ========================================================= */
    body.is-live-mode {
      --live-chat-rail: 340px;
      --live-players-rail: 380px;
      --live-gutter: 16px;
      --live-hud-space: 116px;
      --live-ca-space: 92px;
      /* the chat is its own rail now, not docked to the list */
      --players-dock: 0px;
    }
    /* Everything a viewer cannot act on is gone. The chips and the
       spin button are also hidden inline by setLiveMode, which is
       belt and braces: a stale inline style from a previous build
       cannot leave a control on a screen meant to be watched.

       The earnings chip goes with the account chip and the holdings
       chip -- all three are "what this logged-in account has", which
       is the one thing a screen being watched by other people should
       not be showing.

       Deliberately absent: .scoreboard-panel, which is pinned to
       the top right -- where the players list now lives -- and
       would collide with it. There is no rule for it because there
       is no element: the markup is not on this page any more and
       every line that reads it is null-guarded. A rule against
       nothing is a rule nobody can check, and a check that cannot
       fail is worse than no check.

       The launcher and the chat's close button go because chat is
       permanent here -- there is nothing to launch and no way out
       that should exist. Same for the players list's collapse tab.

       The match timer is pinned at 9999 so it outlives every panel.
       It should not outlive this one: a clock counting down a search
       of this account's own, over the top of two other people's
       boards, is two rounds on one screen. */
    body.is-live-mode .account-chip,
    body.is-live-mode .holdings-chip,
    body.is-live-mode .earnings-chip,
    body.is-live-mode .side-menu,
    /* The audio controls and the payout panel are things the person
       at the keyboard uses. A screen being watched has nobody at the
       keyboard, so both go with the rest of the operator chrome. */
    body.is-live-mode .music-flyout,
    body.is-live-mode .reward-distribution-flyout,
    body.is-live-mode .game-chat-launcher,
    body.is-live-mode .game-chat-close,
    body.is-live-mode .players-panel-tab,
    body.is-live-mode .match-status-overlay { display: none !important; }

    /* ---- the two numbers, made the headline ---- */
    .live-hud {
      position: fixed;
      top: calc(var(--live-gutter) + env(safe-area-inset-top, 0px));
      left: 50%;
      transform: translateX(-50%);
      z-index: 1300;
      display: none;
      align-items: stretch;
      gap: 14px;
      max-width: calc(100vw - 32px);
    }
    body.is-live-mode .live-hud { display: flex; }
    .live-hud-stat {
      display: grid;
      justify-items: center;
      align-content: center;
      gap: 4px;
      min-width: 210px;
      padding: 12px 26px;
      border-radius: var(--tr-radius-medium);
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-panel-gradient);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
    }
    .live-hud-label {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    /* The headline figure. Big enough to read from the back of a
       room, tabular so a count ticking 99 -> 100 does not shuffle
       the bar it sits in. */
    .live-hud-value {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-variant-numeric: tabular-nums;
      font-size: 40px;
      font-weight: 900;
      line-height: 1;
    }
    /* The same two colours the lobby card used for these counts, so
       the numbers do not change meaning when F2 moves them. */
    .live-hud-total .live-hud-value { color: var(--tr-yellow); }
    .live-hud-online .live-hud-value { color: var(--tr-green); }
    .live-hud-brand {
      display: grid;
      justify-items: center;
      align-content: center;
      gap: 6px;
      padding: 12px 20px;
    }
    .live-hud-mark {
      font-size: 17px;
      font-weight: 900;
      letter-spacing: .3em;
      text-indent: .3em;
      background: var(--tr-cyan-violet-gradient);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
    }
    .live-hud-tag {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .22em;
      text-transform: uppercase;
      color: var(--tr-red);
    }
    .live-hud-dot {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--tr-red);
      animation: playersLivePulse 1.9s ease-in-out infinite;
    }

    /* ---- the chat, as a left rail that cannot be closed ---- */
    body.is-live-mode .game-chat-panel.live-mode {
      left: var(--live-gutter);
      top: var(--live-hud-space);
      right: auto;
      bottom: var(--live-ca-space);
      width: var(--live-chat-rail);
      max-height: none;
      z-index: 1300;
    }
    body.is-live-mode .game-chat-panel .game-chat-card {
      height: 100%;
      grid-template-rows: auto minmax(0, 1fr);
    }
    /* The feed fills the rail rather than sitting at its fixed 450px
       with dead space under it. */
    body.is-live-mode .game-chat-panel .game-chat-messages {
      height: auto;
      min-height: 0;
    }
    body.is-live-mode .game-chat-panel .game-chat-message { font-size: 13px; }

    /* ---- the players list, as a right rail and bigger ---- */
    body.is-live-mode .players-panel {
      width: var(--live-players-rail);
      top: var(--live-hud-space);
      bottom: var(--live-ca-space);
      right: var(--live-gutter);
      z-index: 1300;
      /* it cannot be collapsed in live mode -- the tab is hidden --
         so a list left collapsed before F2 is put back on screen */
      transform: none;
    }
    body.is-live-mode .players-panel-head { padding: 14px 16px; }
    body.is-live-mode .players-panel-title { font-size: 15px; }
    body.is-live-mode .players-panel-count { font-size: 12px; }
    body.is-live-mode .players-panel-list { gap: 7px; padding: 11px; }
    body.is-live-mode .players-row {
      grid-template-columns: 34px 1fr 38px 12px;
      gap: 12px;
      padding: 11px 13px;
    }
    body.is-live-mode .players-flag { width: 34px; height: 23px; }
    body.is-live-mode .players-name { font-size: 15px; }
    body.is-live-mode .players-address { font-size: 11px; }
    body.is-live-mode .players-rank { font-size: 13px; }
    body.is-live-mode .players-pip { width: 5px; height: 5px; }
    body.is-live-mode .players-stars { height: 5px; }
    body.is-live-mode .players-status { width: 11px; height: 11px; }
    body.is-live-mode .players-live-pill { font-size: 10px; padding: 4px 9px; }
    body.is-live-mode .players-live-vs { font-size: 11px; }
    /* Nobody clicks anything in live mode, and the viewer is not
       logged in as anyone in particular -- the row already says who
       is playing, and the screen is already showing a match. */
    body.is-live-mode .players-watch { display: none; }

    /* ---- the contract address stays put, and stays on top ---- */
    body.is-live-mode .ca-strip { z-index: 1300; }

    /* ---- and the spectator sits between all four ---- */
    body.is-live-mode .spectate-panel {
      inset:
        var(--live-hud-space)
        calc(var(--live-players-rail) + var(--live-gutter) * 2)
        var(--live-ca-space)
        calc(var(--live-chat-rail) + var(--live-gutter) * 2);
      padding: 0;
      /* no scrim: it is not a modal here, it is the middle of a
         layout, and dimming would dim the rails it sits between */
      background: none;
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
    }
    body.is-live-mode .spectate-card {
      width: 100%;
      max-height: 100%;
      height: 100%;
    }
    /* The boards grow into the height live mode gives them.

       At the fixed 240px they left about a third of a 1080p card
       empty under them, which on the one screen size this mode
       exists for is the whole point wasted. The figure subtracted
       is the furniture above and below a board inside its seat --
       the header, the player row, the stats row, the paddings --
       and the board is half what is left because it is drawn 1:2.
       Clamped at both ends so a short window shrinks it rather than
       overflowing, and a very tall one does not make it absurd. */
    body.is-live-mode .spectate-board {
      --spectate-board: clamp(170px, calc((100vh - 470px) / 2), 330px);
    }
    body.is-live-mode .spectate-mini { --spectate-mini: 54px; }
    body.is-live-mode .spectate-stat-value { font-size: 19px; }
    body.is-live-mode .spectate-name { font-size: 16px; }
    /* The one button on the screen turns live mode off rather than
       returning to a lobby that F2 dismantled, so it says that
       instead. Swapped in CSS rather than in script, because a label
       that depends only on a class cannot fall out of step with it. */
    .spectate-back-live { display: none; }
    body.is-live-mode .spectate-back-lobby { display: none; }
    body.is-live-mode .spectate-back-live { display: inline; }
    body.is-live-mode .spectate-back { font-size: 10px; padding: 8px 13px; }

    /* Live mode is for a big screen. Below these the rails are
       narrowed rather than dropped, because losing one of the four
       things live mode exists to show is worse than a tighter rail. */
    @media (max-width: 1600px) {
      body.is-live-mode {
        --live-chat-rail: 290px;
        --live-players-rail: 320px;
        --live-hud-space: 104px;
      }
      .live-hud-stat { min-width: 178px; padding: 10px 20px; }
      .live-hud-value { font-size: 34px; }
    }
    @media (max-width: 1320px) {
      body.is-live-mode {
        --live-chat-rail: 250px;
        --live-players-rail: 280px;
        --live-hud-space: 94px;
        --live-ca-space: 78px;
      }
      .live-hud-stat { min-width: 148px; padding: 8px 16px; }
      .live-hud-value { font-size: 28px; }
      .live-hud-brand { display: none; }
      body.is-live-mode .players-row { grid-template-columns: 28px 1fr 32px 10px; gap: 9px; }
      body.is-live-mode .players-name { font-size: 13px; }
    }
    /* Below this there is no room for rails at all. Live mode keeps
       its counters and its chat, and the spectator goes back to
       filling the screen -- which is what the narrow layout already
       does with the players list. */
    @media (max-width: 1100px), (orientation: portrait) {
      body.is-live-mode .spectate-panel {
        inset: var(--live-hud-space) var(--live-gutter) var(--live-ca-space);
      }
      body.is-live-mode .game-chat-panel.live-mode {
        top: auto;
        bottom: var(--live-ca-space);
        max-height: 38vh;
        width: min(92vw, 300px);
      }
    }

    .security-block-panel {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,.62);
      display: grid;
      place-items: center;
      z-index: 999;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
    }
    .security-block-panel.show { opacity: 1; pointer-events: auto; }
    /* =========================================================
       Outdated build notice

       This replaced a full-screen gate at z-index 9999 that dimmed
       the page and swallowed every click. A version bump is not a
       reason to end somebody's round, so this is a strip: it never
       takes pointer events, it sits clear of the match status
       overlay rather than over it, and it leaves at once when the
       published version and this build agree again.
       ========================================================= */
    .version-notice {
      position: fixed;
      top: calc(env(safe-area-inset-top, 0px) + 12px);
      /* Centred with auto margins rather than left:50% and a
         translate. A fixed box positioned from left:50% is
         shrink-to-fit against the space remaining to its RIGHT, so
         it could never be wider than half the viewport: the same
         sentence sat on one line at 1280 and wrapped to two at
         1024, for no reason visible in the rule. */
      left: 0;
      right: 0;
      margin-inline: auto;
      width: fit-content;
      z-index: 9998;
      display: flex;
      align-items: center;
      gap: 10px;
      /* Wide enough for the whole sentence on one line at desktop
         widths, because the pill radius only reads as a pill on one
         line. Narrower than that it wraps, and the radius drops to
         a rectangle to suit. */
      max-width: min(92vw, 620px);
      padding: 9px 16px 9px 12px;
      border: 1px solid rgba(224, 184, 114, 0.34);
      border-radius: 999px;
      background: rgba(28, 23, 14, 0.94);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.42);
      color: rgba(245, 240, 228, 0.9);
      font-size: 12.5px;
      line-height: 1.4;
      /* Never interactive. There is no dismiss, because the notice
         is only true while the versions differ and the console can
         put it away by publishing the right one. */
      pointer-events: none;
      opacity: 0;
      visibility: hidden;
      transform: translateY(-8px);
      transition: opacity .2s ease, transform .2s ease, visibility 0s linear .2s;
    }
    .version-notice.show {
      opacity: 1;
      visibility: visible;
      transform: translateY(0);
      transition: opacity .2s ease, transform .2s ease, visibility 0s;
    }
    .version-notice-mark {
      flex: 0 0 auto;
      width: 18px;
      height: 18px;
      border-radius: 50%;
      display: grid;
      place-items: center;
      background: rgba(224, 184, 114, 0.16);
      border: 1px solid rgba(224, 184, 114, 0.4);
      color: #e0b872;
      font-size: 11px;
      font-weight: 700;
      line-height: 1;
    }
    .version-notice-text { min-width: 0; }
    .version-notice-text strong {
      color: #e8c584;
      font-weight: 600;
    }
    .version-notice kbd {
      display: inline-block;
      padding: 0 5px;
      border: 1px solid rgba(245, 240, 228, 0.24);
      border-bottom-width: 2px;
      border-radius: 5px;
      background: rgba(245, 240, 228, 0.07);
      font-family: inherit;
      font-size: 11px;
      font-weight: 600;
      line-height: 16px;
    }
    /* The match status overlay is pinned to the same spot. While the
       notice is up it steps below it instead of sharing the pixels.

       --version-notice-gap is written by syncVersionNoticeGap from
       the notice's measured height, because the text is one line
       wide and three lines narrow. The 84px fallback is only for the
       instant before the first measurement, and is sized for the
       worst case rather than the common one. */
    body.is-version-stale .match-status-overlay {
      top: calc(env(safe-area-inset-top, 0px) + 12px +
        var(--version-notice-gap, 84px));
    }
    @media (max-width: 720px) {
      .version-notice {
        gap: 8px;
        padding: 8px 12px 8px 10px;
        font-size: 11.5px;
        border-radius: 14px;
      }
    }
    .security-block-card {
      position: relative;
      width: min(92vw, 760px);
      padding: 18px;
    }
    .security-block-message {
      font-size: 14px;
      line-height: 1.5;
      color: rgba(223, 244, 255, 0.9);
      text-align: center;
      padding: 12px 10px 4px;
    }
    .reward-distribution-card {
      position: relative;
      width: min(92vw, 860px);
      height: min(86vh, 800px);
      overflow: hidden;
      background: var(--tr-panel-violet-gradient);
      border: 1px solid var(--tr-border-subtle);
      border-radius: var(--tr-radius-large);
      padding: 20px 22px 22px;
      box-shadow: var(--tr-shadow-panel);
      display: grid;
      grid-template-rows: auto auto 1fr;
      gap: 4px;
    }
    /* A cyan-to-violet hairline across the top of the card. The one
       piece of pure decoration in the panel, and it is a single
       pixel rather than a glow. */
    .reward-distribution-card::before {
      content: '';
      position: absolute;
      top: 0;
      left: 18%;
      right: 18%;
      height: 1px;
      background: var(--tr-cyan-violet-gradient);
      opacity: .5;
    }
    /* Two classes deep on purpose. A shared selector list further
       down the file sets `position: relative` on every .controls-close
       at the same specificity as this rule and later in the source,
       which won -- so the button fell out of the corner and into the
       card's grid flow, landing under the subtitle at the left edge.
       Anything single-class here loses that race again. */
    .reward-distribution-card > .reward-distribution-close {
      position: absolute;
      top: 16px;
      right: 16px;
      left: auto;
      z-index: 3;
      width: 34px;
      height: 34px;
      padding: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 18px;
      line-height: 1;
      cursor: pointer;
      /* The house ghost circle, not the solid red one */
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-control-gradient);
      color: var(--tr-text-secondary);
      box-shadow: var(--tr-inset-highlight);
      transform: none;
      transition:
        color var(--tr-transition-standard),
        border-color var(--tr-transition-standard),
        background var(--tr-transition-standard);
    }
    .reward-distribution-card > .reward-distribution-close::before {
      content: none;
    }
    .reward-distribution-card > .reward-distribution-close:hover {
      transform: none;
      filter: none;
      color: var(--tr-text-primary);
      border-color: var(--tr-border-cyan);
      background: var(--tr-control-hover-gradient);
    }
    .reward-distribution-card > .reward-distribution-close:active {
      transform: none;
    }
    .security-alert-card {
      z-index: 999;
      width: min(92vw, 720px);
      max-height: min(72vh, 480px);
      padding: 24px;
      gap: 18px;
      grid-template-rows: auto 1fr;
      border-color: rgba(255, 92, 92, 0.22);
      background:
        radial-gradient(circle at top, rgba(255, 92, 92, 0.16), transparent 46%),
        linear-gradient(180deg, rgba(14, 20, 34, .99), rgba(8, 12, 20, .99));
    }
    .security-alert-card .winners-title {
      font-size: 20px;
      letter-spacing: .12em;
      color: #ffe9e9;
      margin-top: 6px;
    }
    .security-alert-message {
      text-align: center;
      padding: 18px 20px;
      border-radius: 18px;
      background: rgba(255, 92, 92, 0.08);
      border: 1px solid rgba(255, 92, 92, 0.18);
      color: #fff2f2;
      font-size: 19px;
      font-weight: 900;
      line-height: 1.45;
      display: grid;
      place-items: center;
    }
    .security-alert-message .security-alert-main {
      font-size: 22px;
      line-height: 1.1;
      font-weight: 900;
    }
     .security-alert-message .security-alert-small {
       display: block;
       margin-top: 8px;
       font-size: 12px;
       font-weight: 700;
       line-height: 1.4;
       color: rgba(255, 242, 242, 0.72);
     }
     .security-alert-contact {
       margin-top: 16px;
       font-size: 11px;
       font-weight: 600;
       line-height: 1.4;
       color: rgba(213, 234, 254, 0.6);
       text-align: center;
     }
     .security-alert-contact a {
       color: #55d4b1;
       text-decoration: none;
       font-weight: 700;
       transition: color 0.2s ease;
     }
     .security-alert-contact a:hover {
       color: #69d69a;
       text-decoration: underline;
     }
    /* ---- settling the match result ----
       Same panel language as the pvp countdown, tuned down: violet instead of
       cyan, a slow sweep instead of a pulse, and a comet ring for activity. */
    .overlay-spinner { display: none; }
    .overlay.calculating {
      inset: 18px;
      padding: 20px 14px;
      display: grid;
      place-items: center;
      align-content: center;
      overflow: hidden;
      box-sizing: border-box;
      border-radius: 18px;
      background:
        radial-gradient(circle at 50% 36%, rgba(152, 116, 255, 0.10), transparent 48%),
        linear-gradient(180deg, rgba(7, 11, 20, 0.88), rgba(4, 7, 14, 0.95));
      border: 1px solid var(--panel-edge-hi);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        inset 0 0 44px rgba(0, 0, 0, 0.45);
      backdrop-filter: blur(3px);
      -webkit-backdrop-filter: blur(3px);
    }
    /* slow sweep: reads as work in progress without pulsing at the player */
    .overlay.calculating::before {
      content: "";
      position: absolute;
      inset: 0;
      pointer-events: none;
      background: linear-gradient(90deg, transparent, rgba(152, 116, 255, 0.10), transparent);
      transform: translateX(-120%);
      animation: calculatingScan 2.8s ease-in-out infinite;
    }
    @keyframes calculatingScan {
      0% { transform: translateX(-120%); }
      100% { transform: translateX(120%); }
    }
    .overlay.calculating > div {
      position: relative;
      z-index: 1;
      display: grid;
      justify-items: center;
      gap: 10px;
      width: 100%;
      box-sizing: border-box;
    }
    .overlay.calculating h2 {
      position: relative;
      margin: 0;
      font-size: clamp(12px, 2.4vw, 15px);
      font-weight: 900;
      letter-spacing: .22em;
      text-transform: uppercase;
      color: #c6b8ff;
      text-shadow: none;
    }
    /* flanking rules; the h2 is shrink-to-fit here, so 100% is the text width */
    .overlay.calculating h2::before,
    .overlay.calculating h2::after {
      content: "";
      position: absolute;
      top: 50%;
      width: 24px;
      height: 1px;
      background: linear-gradient(90deg, transparent, rgba(152, 116, 255, 0.55));
    }
    .overlay.calculating h2::before { right: calc(100% + 10px); }
    .overlay.calculating h2::after { left: calc(100% + 10px); transform: scaleX(-1); }
    .overlay.calculating p {
      margin: 0;
      max-width: 24ch;
      font-size: clamp(10px, 1.9vw, 12px);
      line-height: 1.5;
      letter-spacing: .04em;
      color: var(--tr-text-secondary);
    }
    .overlay.calculating p::after {
      content: "";
      animation: calculatingDots 1.5s steps(1, end) infinite;
    }
    @keyframes calculatingDots {
      0% { content: ""; }
      25% { content: "."; }
      50% { content: ".."; }
      75%, 100% { content: "..."; }
    }
    .overlay.calculating .overlay-spinner {
      display: block;
      width: 46px;
      height: 46px;
      margin: 4px 0 0;
      border: 0;
      border-radius: 50%;
      background: conic-gradient(
        from 0deg,
        rgba(152, 116, 255, 0) 0deg,
        rgba(152, 116, 255, 0.16) 130deg,
        rgba(95, 212, 255, 0.75) 290deg,
        #bea7ff 360deg);
      -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
      mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
      animation: overlaySpin 1.15s linear infinite;
    }
    @keyframes overlaySpin {
      to { transform: rotate(360deg); }
    }
    @media (prefers-reduced-motion: reduce) {
      .overlay.calculating::before { animation: none; opacity: 0; }
      .overlay.calculating p::after { animation: none; content: "..."; }
      .overlay.calculating .overlay-spinner { animation-duration: 3s; }
    }
     .overlay-subtext-btn {
      margin-top: 8px;
      padding: 8px 14px;
      border-radius: 999px;
      border: 1px solid rgba(56, 160, 255, 0.24);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.98), rgba(9, 14, 24, 0.99));
      color: #dff4ff;
      font-size: 11px;
      font-weight: 900;
      letter-spacing: .08em;
      cursor: pointer;
      box-shadow: 0 10px 24px rgba(0,0,0,0.28);
    }
    .reward-distribution-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }
    .reward-distribution-title {
      text-align: center;
      width: 100%;
      font-size: 17px;
      font-weight: 900;
      letter-spacing: .14em;
      text-transform: uppercase;
      padding: 6px 44px 0;
      /* the house cyan-to-violet, painted through the letters */
      background: var(--tr-cyan-violet-gradient);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
    }
    .reward-distribution-subtitle {
      text-align: center;
      font-size: 11px;
      color: var(--tr-text-muted);
      letter-spacing: .18em;
      text-transform: uppercase;
      /* separates the fixed header from the scrolling body, so the
         content reads as sliding under it rather than floating */
      padding-bottom: 14px;
      margin-bottom: 12px;
      border-bottom: 1px solid var(--tr-border-subtle);
    }
    /* =========================================================
       REWARD DISTRIBUTION

       One scrolling body, not a stack of separate scrollers. The
       panel explains a calculation, and a reader following it from
       pool to share to band should not have to find and work three
       different scroll regions to do it.

       Everything here is on the house tokens: quiet surfaces,
       hairline borders, accent in small doses on the figures that
       matter. No glow.
       ========================================================= */
    .reward-distribution-body {
      min-height: 0;
      overflow-y: auto;
      overflow-x: hidden;
      padding-right: 10px;
      display: grid;
      gap: 14px;
      align-content: start;
      /* Each section is exactly as tall as its content and is never
         stretched or squeezed to fit the card. Stated rather than
         left to the default, because the deficit case is the one
         that goes wrong. */
      grid-auto-rows: max-content;
      /* The default bar is a white slab against a dark card. Thin and
         themed, matching the score sheet and the players list. */
      scrollbar-width: thin;
      scrollbar-color: rgba(152, 116, 255, 0.34) transparent;
    }
    .reward-distribution-body::-webkit-scrollbar { width: 8px; }
    .reward-distribution-body::-webkit-scrollbar-track {
      background: transparent;
    }
    .reward-distribution-body::-webkit-scrollbar-thumb {
      border-radius: 99px;
      background: rgba(152, 116, 255, 0.3);
    }
    .reward-distribution-body::-webkit-scrollbar-thumb:hover {
      background: rgba(152, 116, 255, 0.46);
    }
    .rd-section {
      position: relative;
      display: grid;
      gap: 11px;
      padding: 15px 17px;
      border: 1px solid var(--tr-border-subtle);
      border-radius: var(--tr-radius-medium);
      background: var(--tr-control-gradient);
      box-shadow: var(--tr-inset-highlight);
      /* NO overflow: hidden here, however tempting it is for keeping
         the ::before edge inside the rounded corner.

         It took this plus the body's default auto rows to go wrong,
         and it went wrong badly: all five sections pinned to an
         identical 91px, the rank table cut off after two rows, and
         no scrollbar to suggest anything was missing.

         An auto row is minmax(auto, max-content), and that `auto`
         minimum is the item's automatic minimum size -- which is
         content-based only while the item's overflow is visible.
         Hide it and the floor drops to 0, so under a container with
         a definite height the track algorithm is free to divide the
         deficit across the rows instead of overflowing into the
         scroller. The body now states grid-auto-rows: max-content,
         which fixes it from the other side; either alone is enough,
         and both are cheap. The edge is inset from the corners
         anyway and never needed the clip. */
    }
    /* Each section carries a short coloured edge on its left, so the
       blocks are told apart by colour rather than by reading every
       heading. Cyan by default; the pool and the creator split take
       their own. */
    .rd-section::before {
      content: '';
      position: absolute;
      left: 0;
      top: 14px;
      bottom: 14px;
      width: 2px;
      border-radius: 0 2px 2px 0;
      background: var(--tr-cyan);
      opacity: .5;
    }
    .rd-heading {
      font-size: 11px;
      font-weight: 900;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--tr-cyan);
    }
    .rd-label {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    /* The pool and the countdown side by side: the second is the
       answer to "and how old is that number", so they belong on one
       line rather than in two places. */
    .rd-pool {
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: center;
      gap: 18px;
      border-color: var(--tr-border-violet);
      background:
        radial-gradient(circle at 12% 0%, rgba(152, 116, 255, 0.12), transparent 58%),
        var(--tr-control-gradient);
    }
    .rd-pool::before { background: var(--tr-violet); opacity: .8; }
    .rd-pool-main { display: grid; gap: 7px; min-width: 0; }
    .rd-pool-value {
      display: flex;
      align-items: center;
      gap: 10px;
      font-size: 30px;
      font-weight: 900;
      line-height: 1;
      color: var(--tr-text-primary);
      font-variant-numeric: tabular-nums;
    }
    .rd-sol { width: 20px; height: 20px; display: block; flex: 0 0 auto; }
    .rd-sync { display: grid; gap: 5px; justify-items: end; text-align: right; }
    .rd-sync-value {
      font-size: 21px;
      font-weight: 900;
      line-height: 1;
      color: var(--tr-cyan-bright);
      /* it reads out second by second, so it must not reflow */
      font-variant-numeric: tabular-nums;
    }
    .rd-sync-note {
      font-size: 10px;
      color: var(--tr-text-muted);
      max-width: 180px;
      line-height: 1.4;
    }
    /* The formula with live numbers under each term. A row on a wide
       screen, a column on a phone -- the operators rotate with it so
       the division sign never ends up beside two stacked terms. */
    .rd-formula {
      display: flex;
      flex-wrap: wrap;
      align-items: stretch;
      gap: 8px;
    }
    .rd-term {
      flex: 1 1 120px;
      min-width: 0;
      display: grid;
      gap: 5px;
      padding: 10px 12px;
      border: 1px solid var(--tr-border-subtle);
      border-radius: var(--tr-radius-small);
      background: rgba(10, 16, 28, 0.55);
    }
    .rd-term-name {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      line-height: 1.3;
    }
    .rd-term-value {
      font-size: 15px;
      font-weight: 900;
      color: var(--tr-text-primary);
      font-variant-numeric: tabular-nums;
    }
    /* The one figure the player came for */
    .rd-term-result {
      border-color: var(--tr-border-violet);
      background: var(--tr-violet-soft);
    }
    .rd-term-result .rd-term-value { color: #cbb4ff; }
    .rd-op {
      flex: 0 0 auto;
      align-self: center;
      font-size: 15px;
      font-weight: 900;
      color: var(--tr-text-muted);
      padding: 0 2px;
    }
    .rd-op-equals { color: var(--tr-text-secondary); }
    .rd-share {
      font-size: 12px;
      color: var(--tr-text-secondary);
      letter-spacing: .02em;
      padding: 9px 12px;
      border-radius: var(--tr-radius-small);
      background: rgba(10, 16, 28, 0.45);
    }
    .rd-share strong {
      color: var(--tr-green);
      font-variant-numeric: tabular-nums;
    }
    .rd-band-note { color: var(--tr-text-muted); }
    /* The payout threshold. Quieter than the share above it: it is a
       condition on taking the money out, not part of working out how
       much there is. */
    .rd-min-payout {
      margin-top: 8px;
      color: var(--tr-text-muted);
      background: rgba(10, 16, 28, 0.3);
    }
    .rd-note {
      margin: 0;
      font-size: 11px;
      line-height: 1.6;
      color: var(--tr-text-muted);
    }
    .rd-bands-head,
    .rd-band {
      display: grid;
      grid-template-columns: minmax(0, 1fr) 72px 110px;
      gap: 10px;
      align-items: center;
    }
    .rd-bands-head {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      padding: 0 12px;
    }
    .rd-bands-head span:not(:first-child),
    .rd-band-percent,
    .rd-band-sol { text-align: right; }
    .rd-bands { display: grid; gap: 4px; }
    .rd-band {
      padding: 9px 12px;
      border: 1px solid transparent;
      border-radius: var(--tr-radius-small);
      background: rgba(10, 16, 28, 0.45);
      font-size: 12px;
      color: var(--tr-text-secondary);
      transition: border-color var(--tr-transition-standard);
    }
    .rd-band-name { font-weight: 800; letter-spacing: .04em; }
    .rd-band-percent {
      font-weight: 900;
      color: var(--tr-cyan);
      font-variant-numeric: tabular-nums;
    }
    .rd-band-sol {
      font-weight: 900;
      color: var(--tr-text-primary);
      font-variant-numeric: tabular-nums;
    }
    /* Which row is yours. A quiet edge and a label, not a highlight
       that makes the other nine look switched off. */
    .rd-band.is-mine {
      border-color: var(--tr-border-violet);
      background: var(--tr-violet-soft);
      color: var(--tr-text-primary);
    }
    .rd-band.is-mine .rd-band-name::after {
      content: 'You';
      margin-left: 8px;
      padding: 2px 7px;
      border-radius: var(--tr-radius-pill);
      background: rgba(152, 116, 255, 0.22);
      font-size: 9px;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: #cbb4ff;
    }
    .rd-splits {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 10px;
    }
    /* Three destinations, three accents -- the same violet, cyan and
       yellow the rest of the game uses, so the split reads as three
       things at a glance instead of three identical boxes. */
    .rd-split {
      display: grid;
      gap: 7px;
      padding: 13px 12px;
      border: 1px solid var(--rd-split-edge, var(--tr-border-subtle));
      border-radius: var(--tr-radius-small);
      background:
        linear-gradient(180deg, var(--rd-split-wash, transparent), transparent 70%),
        rgba(10, 16, 28, 0.55);
      text-align: center;
    }
    .rd-split:nth-child(1) {
      --rd-split-edge: var(--tr-border-violet);
      --rd-split-wash: var(--tr-violet-soft);
      --rd-split-ink: var(--tr-violet-bright);
    }
    .rd-split:nth-child(2) {
      --rd-split-edge: var(--tr-border-cyan);
      --rd-split-wash: var(--tr-cyan-soft);
      --rd-split-ink: var(--tr-cyan-bright);
    }
    .rd-split:nth-child(3) {
      --rd-split-edge: rgba(255, 216, 120, 0.3);
      --rd-split-wash: var(--tr-yellow-soft);
      --rd-split-ink: var(--tr-yellow);
    }
    .rd-split-pct {
      font-size: 22px;
      font-weight: 900;
      line-height: 1;
      color: var(--rd-split-ink, var(--tr-text-primary));
      font-variant-numeric: tabular-nums;
    }
    .rd-split-name {
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .06em;
      line-height: 1.4;
      color: var(--tr-text-muted);
    }
    .reward-distribution-list {
      display: grid;
      gap: 10px;
      min-height: 0;
      align-content: start;
      padding-right: 2px;
    }
    .reward-distribution-list.empty {
      place-items: center;
    }
    /* The payout list scrolls inside its own box, so a long history
       does not make the whole panel scroll. Keyed on the id so it
       beats the narrow-width rules that redeclare the list below. */
    #rewardDistributionList {
      max-height: min(320px, 42vh);
      overflow-y: auto;
      overscroll-behavior: contain;
      scrollbar-width: thin;
      scrollbar-color: rgba(125, 60, 255, 0.7) rgba(11, 19, 33, 0.85);
    }
    .reward-distribution-empty {
      width: 100%;
      min-height: 100%;
      display: grid;
      place-items: center;
      text-align: center;
      color: var(--muted);
      font-size: 14px;
      font-weight: 700;
      letter-spacing: .08em;
      line-height: 1.5;
      padding: 24px;
    }
    .reward-distribution-list::-webkit-scrollbar {
      width: 10px;
    }
    .reward-distribution-list::-webkit-scrollbar-track {
      background: rgba(11, 19, 33, 0.85);
      border-radius: 999px;
    }
    .reward-distribution-list::-webkit-scrollbar-thumb {
      background: linear-gradient(180deg, rgba(125, 60, 255, 0.9), rgba(67, 18, 168, 0.95));
      border-radius: 999px;
      border: 2px solid rgba(11, 19, 33, 0.85);
      box-shadow: 0 0 12px rgba(125, 60, 255, 0.22);
    }
    .reward-distribution-list::-webkit-scrollbar-thumb:hover {
      background: linear-gradient(180deg, rgba(145, 90, 255, 0.95), rgba(85, 35, 190, 0.98));
    }
    .ca-strip {
      position: fixed;
      left: 50%;
      bottom: 18px;
      transform: translateX(-50%);
      z-index: 999;
      pointer-events: none;
      transform-origin: bottom center;
    }
    /* ---- contract-address strip ----
       Was a glassy bar with a cyan halo holding two mismatched social icons
       (a bare 28px PNG jammed against a bordered 30px button with -10px
       margins on both sides), a proportional-font address and a button that
       resized the whole bar when its label changed to "Copied".

       Now it is one quiet panel on the same frame as the rest of the chrome,
       reading left to right as: two matched social tiles, a hairline divider,
       the label, the address in a recessed monospace field, and a single
       accented action. */
    .ca-strip-inner {
      position: relative;
      width: min(92vw, 540px);
      max-width: 540px;
      padding: 10px 12px;
      border-radius: 14px;
      background:
        radial-gradient(120% 120% at 50% 0%, rgba(95, 212, 255, 0.05), transparent 62%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.97), rgba(9, 13, 22, 0.99));
      border: 1px solid var(--panel-edge);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        inset 0 0 0 1px rgba(0, 0, 0, 0.32),
        0 16px 40px rgba(0, 0, 0, 0.42);
      display: flex;
      align-items: center;
      gap: 9px;
      text-align: left;
      backdrop-filter: blur(6px);
      -webkit-backdrop-filter: blur(6px);
      box-sizing: border-box;
      overflow: hidden;
    }
    /* the thin light along the top edge is what makes it read as a machined
       panel rather than a flat rectangle */
    .ca-strip-inner::after {
      content: "";
      position: absolute;
      top: 0;
      left: 16px;
      right: 16px;
      height: 1px;
      background: linear-gradient(90deg, transparent, rgba(95, 212, 255, 0.5), transparent);
      opacity: .75;
      pointer-events: none;
    }
    /* The strip is a container, not a control: the shared chrome hover rule
       lifted the whole bar whenever the copy button was hovered. That rule is
       declared further down the sheet, so this needs the extra class to win
       rather than relying on source order. */
    .ca-strip .ca-strip-inner:hover {
      translate: none;
      filter: none;
    }
    .ca-strip {
      position: fixed;
      top: auto;
      left: 50%;
      right: auto;
      bottom: calc(18px + env(safe-area-inset-bottom, 0px));
      width: max-content;
      max-width: calc(100vw - 24px);
      transform: translateX(-50%);
      transform-origin: bottom center;
      z-index: 999;
      pointer-events: none;
      /* the same easing and duration the chat panel slides its own
         right edge with, so the two move as one gesture. opacity and
         visibility are here so the strip can fade out under a panel
         that covers it rather than vanishing mid-gesture. */
      transition: left .28s cubic-bezier(.2, .85, .3, 1),
        opacity .18s ease,
        visibility .18s;
    }

    /* ---- the strip steps aside for the game chat ----

       Both live on the bottom edge: the strip is centred, the chat
       opens against the right gutter, and on anything narrower than
       about 1340px the two want the same pixels. The strip is the
       higher layer, so what actually happened was the strip covering
       the corner of the chat. Restacking only chooses which one is
       unreadable, so the strip moves instead.

       How far is the middle term of the clamp, and it is the real
       overlap rather than a guess: half the strip, plus everything
       the chat occupies from the right edge, minus half the viewport.
       On a screen wide enough for both that comes out negative and
       the clamp floors it at zero, so nothing moves when nothing
       needs to. The ceiling is the point where the strip sits centred
       in what is left beside the chat; past that it would start
       walking off the left edge, which is worse than the overlap it
       was fixing.

       --players-dock is in the clearance because the chat is docked to
       the players list and slides left when that opens. Both sides of
       the sum move together, so the strip follows without a second
       rule.

       Landscape only, and never in live mode: portrait puts the chat
       bottom-LEFT and clear above the strip, and live mode gives the
       chat a left rail with its own reserved space, so in both the
       strip has nothing to dodge and moving it would only push it
       into something else. */
    /* On body rather than :root, which matters: --players-dock is
       widened by body.players-open, and a custom property substitutes
       the value it has on the element that declares it. Declared up on
       :root these would read the dock as its 0px default forever, and
       the strip would sit still exactly when the chat had slid left
       and needed it to move furthest. */
    body {
      --ca-chat-clear:
        calc(min(92vw, 380px) + var(--game-chat-gutter) +
          var(--players-dock) + 20px);
      --ca-chat-shift: clamp(
        0px,
        calc(min(92vw, 540px) / 2 + var(--ca-chat-clear) - 50vw),
        calc(var(--ca-chat-clear) / 2));
    }
    @media (orientation: landscape) {
      body.game-chat-open:not(.is-live-mode) .ca-strip {
        left: calc(50% - var(--ca-chat-shift));
      }
    }
    /* Out of the way while the shop is open. The strip is fixed to
       the bottom of the viewport and the perks dialog is tall enough
       to reach it, so the address ended up sitting across the shop's
       own footer. Faded and lifted out rather than display:none, so
       it returns the way it left instead of snapping back. */
    body.perks-open .ca-strip,
    body.changelog-open .ca-strip,
    /* Game Information too: the strip (999) and the bottom-left dock
       (220) both outrank the welcome's 120, so they sat across the
       card's foot -- on the very first screen a newcomer reads. */
    body.welcome-open .ca-strip,
    body.score-open .ca-strip,
    /* And through the tutorial tour, whose callouts and spotlight
       would otherwise have the strip sitting across them. */
    body.tour-open .ca-strip,
    /* And while How To Play is open. Keyed off the panel's own .show
       rather than a body class, because three separate paths open it
       and two of them set .show directly. */
    body:has(#controlsPanel.show) .ca-strip,
    body.welcome-open .bottom-left-dock {
      opacity: 0;
      /* visibility, not transform. The strip's transform is rewritten
         by the zoom breakpoints and set to none outright on mobile,
         so anything moved here would be fought over at some widths
         and not others; opacity and visibility are contested by
         nothing. Not display:none either, which cannot fade. */
      visibility: hidden;
    }
    @media (prefers-reduced-motion: reduce) {
      .ca-strip { transition: none; }
    }
    .ca-strip-inner {
      margin: 0 auto;
    }
    /* One tile for both socials. The old rules sized them differently, gave
       only one of them a border, and pulled Medium 10px into its neighbours
       on each side. */
    .ca-social-link {
      pointer-events: auto;
      display: grid;
      place-items: center;
      width: 30px;
      height: 30px;
      flex: 0 0 auto;
      margin: 0;
      padding: 0;
      border-radius: 9px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(20, 27, 42, 1), rgba(10, 15, 24, 1));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        0 4px 12px rgba(0, 0, 0, 0.3);
      color: rgba(207, 228, 245, 0.7);
      text-decoration: none;
      overflow: hidden;
      transition:
        border-color var(--tr-transition-standard),
        color var(--tr-transition-standard),
        box-shadow var(--tr-transition-standard),
        translate var(--tr-transition-fast);
    }
    .ca-social-link svg { display: block; fill: currentColor; }
    /* the Medium mark is wide and short, X is square, so they are sized to
       match by eye rather than by box */
    .ca-medium-link svg { width: 18px; height: 18px; }
    .ca-x-link svg { width: 14px; height: 14px; }
    .ca-social-link:hover {
      border-color: var(--panel-edge-hi);
      color: #eaf6ff;
    }
    .ca-strip-inner,
    .ca-copy-btn,
    .ca-x-link,
    .ca-medium-link,
    .game-chat-launcher,
    .music-button,
    .daily-spin-btn,
    .account-chip,
    .holdings-chip,
    /* The earnings nub is the same kind of control as the holdings
       nub directly above it and was the only one of the three left
       out of these lists, so it alone sat still under the cursor. */
    .earnings-chip,
    .touch-btn,
    .controls-btn,
    .controls-close,
    .confirm-btn,
    .close-btn,
    .profile-toggle,
    .score-help,
    .scoreboard-toggle,
    .scoreboard-find,
    .scoreboard-mode-toggle,
    .game-chat-send,
    .game-chat-close,
    .game-chat-refresh,
    .holdings-reload-btn,
    .daily-spin-btn,
    .reward-distribution-btn,
    .pause-resume-btn {
      /* `right` is listed for the chat launcher, which slides sideways with the
         players panel; none of the other controls in this list animate it */
      transition: background var(--tr-transition-standard), border-color var(--tr-transition-standard), box-shadow var(--tr-transition-standard), color var(--tr-transition-standard), filter var(--tr-transition-standard), opacity var(--tr-transition-standard), translate var(--tr-transition-fast), right .28s cubic-bezier(.2, .85, .3, 1);
    }
    .ca-strip-inner,
    .ca-copy-btn,
    .ca-x-link,
    .ca-medium-link,
    .game-chat-launcher,
    .music-button,
    .daily-spin-btn,
    .account-chip,
    .holdings-chip,
    .earnings-chip,
    .touch-btn,
    .controls-btn,
    .controls-close,
    .confirm-btn,
    .close-btn,
    .profile-toggle,
    .score-help,
    .scoreboard-toggle,
    .scoreboard-find,
    .scoreboard-mode-toggle,
    .game-chat-send,
    .game-chat-close,
    .game-chat-refresh,
    .holdings-reload-btn,
    .reward-distribution-btn,
    .pause-resume-btn {
      position: relative;
      overflow: hidden;
      isolation: isolate;
    }
    .ca-strip-inner:hover,
    .ca-copy-btn:hover,
    .ca-x-link:hover,
    .ca-medium-link:hover,
    .game-chat-launcher:hover,
    .music-button:hover,
    .daily-spin-btn:hover,
    .account-chip:hover,
    .holdings-chip:hover,
    .earnings-chip:hover,
    .touch-btn:hover,
    .controls-btn:hover,
    .controls-close:hover,
    .confirm-btn:hover,
    .close-btn:hover,
    .profile-toggle:hover,
    .score-help:hover,
    .scoreboard-toggle:hover,
    .scoreboard-find:hover,
    .scoreboard-mode-toggle:hover,
    .game-chat-send:hover,
    .game-chat-close:hover,
    .game-chat-refresh:hover,
    .holdings-reload-btn:hover,
    .reward-distribution-btn:hover,
    .pause-resume-btn:hover {
      translate: 0 -2px;
      filter: brightness(1.06);
    }
    .ca-strip-inner:active,
    .ca-copy-btn:active,
    .ca-x-link:active,
    .ca-medium-link:active,
    .game-chat-launcher:active,
    .music-button:active,
    .daily-spin-btn:active,
    .account-chip:active,
    .holdings-chip:active,
    .earnings-chip:active,
    .touch-btn:active,
    .controls-btn:active,
    .controls-close:active,
    .confirm-btn:active,
    .close-btn:active,
    .profile-toggle:active,
    .score-help:active,
    .scoreboard-toggle:active,
    .scoreboard-find:active,
    .scoreboard-mode-toggle:active,
    .game-chat-send:active,
    .game-chat-close:active,
    .game-chat-refresh:active,
    .holdings-reload-btn:active,
    .reward-distribution-btn:active,
    .pause-resume-btn:active {
      translate: 0 0;
      filter: brightness(0.96);
    }
    .ca-strip-inner:disabled,
    .ca-copy-btn:disabled,
    .ca-x-link:disabled,
    .ca-medium-link:disabled,
    .game-chat-launcher:disabled,
    .music-button:disabled,
    .daily-spin-btn:disabled,
    .account-chip:disabled,
    .holdings-chip:disabled,
    .earnings-chip:disabled,
    .touch-btn:disabled,
    .controls-btn:disabled,
    .controls-close:disabled,
    .confirm-btn:disabled,
    .close-btn:disabled,
    .profile-toggle:disabled,
    .score-help:disabled,
    .scoreboard-toggle:disabled,
    .scoreboard-find:disabled,
    .scoreboard-mode-toggle:disabled,
    .game-chat-send:disabled,
    .game-chat-close:disabled,
    .game-chat-refresh:disabled,
    .holdings-reload-btn:disabled,
    .pause-resume-btn:disabled,
    .ca-strip-inner[aria-disabled="true"],
    .ca-copy-btn[aria-disabled="true"],
    .ca-x-link[aria-disabled="true"],
    .ca-medium-link[aria-disabled="true"],
    .game-chat-launcher[aria-disabled="true"],
    .music-button[aria-disabled="true"],
    .daily-spin-btn[aria-disabled="true"],
    .account-chip[aria-disabled="true"],
    .holdings-chip[aria-disabled="true"],
    .earnings-chip[aria-disabled="true"],
    .touch-btn[aria-disabled="true"],
    .controls-btn[aria-disabled="true"],
    .controls-close[aria-disabled="true"],
    .confirm-btn[aria-disabled="true"],
    .close-btn[aria-disabled="true"],
    .profile-toggle[aria-disabled="true"],
    .score-help[aria-disabled="true"],
    .scoreboard-toggle[aria-disabled="true"],
    .scoreboard-find[aria-disabled="true"],
    .scoreboard-mode-toggle[aria-disabled="true"],
    .game-chat-send[aria-disabled="true"],
    .game-chat-close[aria-disabled="true"],
    .game-chat-refresh[aria-disabled="true"],
    .holdings-reload-btn[aria-disabled="true"],
    .pause-resume-btn[aria-disabled="true"] {
      translate: none;
      filter: grayscale(0.25) brightness(0.72);
      opacity: 0.58;
      cursor: not-allowed;
      box-shadow: none;
    }
    .ca-strip-inner:focus-visible,
    .ca-copy-btn:focus-visible,
    .ca-x-link:focus-visible,
    .ca-medium-link:focus-visible,
    .game-chat-launcher:focus-visible,
    .music-button:focus-visible,
    .daily-spin-btn:focus-visible,
    .account-chip:focus-visible,
    .holdings-chip:focus-visible,
    .earnings-chip:focus-visible,
    .touch-btn:focus-visible,
    .controls-btn:focus-visible,
    .controls-close:focus-visible,
    .confirm-btn:focus-visible,
    .close-btn:focus-visible,
    .profile-toggle:focus-visible,
    .score-help:focus-visible,
    .scoreboard-toggle:focus-visible,
    .scoreboard-find:focus-visible,
    .scoreboard-mode-toggle:focus-visible,
    .game-chat-send:focus-visible,
    .game-chat-close:focus-visible,
    .game-chat-refresh:focus-visible,
    .holdings-reload-btn:focus-visible,
    .reward-distribution-btn:focus-visible,
    .pause-resume-btn:focus-visible {
      outline: 2px solid var(--tr-violet-bright);
      outline-offset: 3px;
      box-shadow: 0 0 0 4px rgba(152,116,255,0.13), 0 0 22px rgba(95,212,255,0.12);
    }
    /* separates the social pair from the address block */
    .ca-strip-label {
      position: relative;
      flex: 0 0 auto;
      padding-left: 12px;
      margin-left: 3px;
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .18em;
      color: #7fd2f5;
      text-transform: uppercase;
    }
    .ca-strip-label::before {
      content: "";
      position: absolute;
      left: 0;
      top: 50%;
      width: 1px;
      height: 20px;
      transform: translateY(-50%);
      background: linear-gradient(180deg, transparent, var(--panel-edge-hi), transparent);
    }
    /* the address sits in a recessed field and is set in mono: a hash in a
       proportional bold face is the single thing that made this look cheap */
    .ca-strip-value {
      flex: 1 1 auto;
      min-width: 0;
      padding: 5px 9px;
      border-radius: 8px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(8, 12, 20, 1), rgba(5, 9, 15, 1));
      box-shadow:
        inset 0 0 0 1px rgba(0, 0, 0, 0.4),
        inset 0 1px 5px rgba(0, 0, 0, 0.4);
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
      font-size: 11px;
      font-weight: 600;
      letter-spacing: .01em;
      color: #cfe4f5;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .ca-copy-btn {
      flex: 0 0 auto;
      pointer-events: auto;
      /* fixed width so swapping the label to "Copied" does not resize the bar */
      min-width: 86px;
      padding: 7px 12px;
      border: 1px solid rgba(95, 212, 255, 0.30);
      border-radius: 9px;
      background:
        radial-gradient(120% 130% at 50% 0%, rgba(95, 212, 255, 0.15), transparent 70%),
        linear-gradient(180deg, rgba(21, 31, 47, 1), rgba(11, 17, 28, 1));
      color: #d8f2ff;
      font-size: 9px;
      font-weight: 900;
      letter-spacing: .16em;
      text-transform: uppercase;
      cursor: pointer;
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.055),
        0 6px 14px rgba(0, 0, 0, 0.3);
    }
    .ca-copy-btn:hover {
      border-color: rgba(95, 212, 255, 0.5);
      color: #f2fbff;
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.07),
        0 8px 18px rgba(0, 0, 0, 0.34);
    }
    /* =========================================================
       SIDE MENU

       The column left of the board, where the floating chat used
       to be: Tetra Store, Daily Spin and Change Logs. syncSideMenu
       in script.js measures the room and hands over the position as
       three custom properties, plus a class for the layout:

       - no class / .is-wide: labelled tiles in the gutter, an icon
         beside a title and a one-line status.
       - .is-tucked: compact tiles under the HOLD box, icon over the
         title. .is-icons drops the title as well when even that is
         too tall for the space left above the help buttons.

       Each tile wears the chrome the account chip and the dock
       buttons wear -- hairline edge, inner bevel, a 2px accent bar
       along the top -- and its own accent: amber for the store,
       violet for the spin, cyan for the log. Hover eases the edge up
       a step; it never glows.
       ========================================================= */
    .side-menu {
      position: fixed;
      left: var(--side-menu-left, 16px);
      top: var(--side-menu-top, 50%);
      width: var(--side-menu-width, 230px);
      z-index: 36;
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    .side-menu-item {
      --item-accent: rgba(151, 184, 222, 0.52);
      --item-edge: rgba(151, 184, 222, 0.26);
      --item-wash: rgba(151, 184, 222, 0.045);
      --item-ink: #dff4ff;
      position: relative;
      display: flex;
      align-items: center;
      gap: 13px;
      width: 100%;
      min-height: 64px;
      margin: 0;
      padding: 12px 14px;
      border: 1px solid var(--panel-edge);
      border-radius: 14px;
      background:
        radial-gradient(120% 86% at 50% 0%, var(--item-wash), transparent 64%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.99), rgba(9, 13, 22, 1));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 8px 20px rgba(0, 0, 0, 0.28);
      color: var(--tr-text-primary);
      font-family: inherit;
      text-align: left;
      cursor: pointer;
      transition:
        border-color var(--tr-transition-standard),
        box-shadow var(--tr-transition-standard),
        translate var(--tr-transition-fast);
    }
    .side-menu-item::after {
      content: "";
      position: absolute;
      top: 0;
      left: 14px;
      right: 14px;
      height: 2px;
      border-radius: 0 0 2px 2px;
      background: linear-gradient(90deg, transparent, var(--item-accent), transparent);
      opacity: .8;
      pointer-events: none;
    }
    .side-menu-item:hover {
      border-color: var(--item-edge);
      translate: 0 -1px;
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.05),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 12px 26px rgba(0, 0, 0, 0.34);
    }
    .side-menu-item:active { translate: 0 0; }
    .side-menu-item:focus-visible {
      outline: 2px solid var(--item-edge);
      outline-offset: 2px;
    }
    .side-menu-store {
      --item-accent: rgba(255, 216, 120, 0.55);
      --item-edge: rgba(255, 216, 120, 0.32);
      --item-wash: rgba(255, 216, 120, 0.05);
      --item-ink: #ffd878;
    }
    .side-menu-spin {
      --item-accent: rgba(183, 156, 255, 0.55);
      --item-edge: rgba(152, 116, 255, 0.34);
      --item-wash: rgba(152, 116, 255, 0.06);
      --item-ink: #bea7ff;
    }
    .side-menu-tetranomics {
      --item-accent: rgba(110, 231, 183, 0.52);
      --item-edge: rgba(110, 231, 183, 0.32);
      --item-wash: rgba(110, 231, 183, 0.05);
      --item-ink: #6ee7b7;
      text-decoration: none;
    }
    /* Tetranomics, linked from Reward Distribution; same card as the
       one on the Daily Spin page. */
    .tetranomics-portal {
      display: flex;
      align-items: center;
      gap: 12px;
      margin-top: 12px;
      padding: 11px 12px;
      border-radius: 12px;
      border: 1px solid rgba(125, 211, 252, 0.28);
      background: linear-gradient(100deg, rgba(56, 189, 248, 0.10), rgba(152, 116, 255, 0.08)), rgba(10, 15, 25, 0.9);
      text-align: left;
      text-decoration: none;
      color: inherit;
      transition: border-color .2s ease, translate .2s ease;
    }
    .tetranomics-portal:hover, .tetranomics-portal:focus-visible {
      border-color: rgba(125, 211, 252, 0.5);
      translate: 0 -1px;
    }
    .tetranomics-portal .tn-icon {
      flex: none;
      display: grid;
      place-items: center;
      width: 36px;
      height: 36px;
      border-radius: 10px;
      background: rgba(56, 189, 248, 0.14);
      color: #5fd4ff;
    }
    .tetranomics-portal svg { width: 20px; height: 20px; }
    .tetranomics-portal .tn-text { flex: 1; min-width: 0; display: grid; gap: 2px; }
    .tetranomics-portal .tn-title { font-size: 13px; font-weight: 900; letter-spacing: .06em; text-transform: uppercase; color: #5fd4ff; }
    .tetranomics-portal .tn-sub { font-size: 11px; line-height: 1.35; color: var(--tr-text-muted); }
    .tetranomics-portal .tn-go { flex: none; color: #5fd4ff; transition: translate .2s ease; }
    .tetranomics-portal:hover .tn-go { translate: 3px 0; }
    .perks-burn-link {
      display: inline-block;
      margin-top: 6px;
      font-weight: 800;
      color: #ffb37a;
      text-decoration: none;
    }
    .perks-burn-link:hover, .perks-burn-link:focus-visible { text-decoration: underline; }
    .side-menu-logs {
      --item-accent: rgba(95, 212, 255, 0.52);
      --item-edge: rgba(95, 212, 255, 0.32);
      --item-wash: rgba(95, 212, 255, 0.05);
      --item-ink: #5fd4ff;
    }
    /* The icon sits in the same recess the HOLD and NEXT canvases
       do, so it reads as part of the game's furniture. */
    .side-menu-icon {
      position: relative;
      flex: 0 0 auto;
      width: 40px;
      height: 40px;
      display: grid;
      place-items: center;
      border-radius: 11px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(8, 12, 20, 1), rgba(5, 9, 15, 1));
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.40);
      color: var(--item-ink);
    }
    .side-menu-icon svg { width: 22px; height: 22px; display: block; }
    .side-menu-icon .sm-fill { fill: currentColor; opacity: .14; }
    .side-menu-icon .sm-bolt { fill: currentColor; }
    /* The wheel is a picture rather than a glyph, so it fills the
       space the recess would have taken instead of sitting in one. */
    .side-menu-spin .side-menu-icon {
      border-color: transparent;
      background: none;
      box-shadow: none;
    }
    .side-menu-spin .side-menu-icon svg { width: 40px; height: 40px; }
    .spin-wheel-icon .sw-face {
      transform-box: view-box;
      transform-origin: 50% 50%;
      transition: transform 700ms cubic-bezier(0.2, 0.8, 0.2, 1);
    }
    /* A quarter turn on hover, so the colours trade places the way
       the real wheel's do. */
    .side-menu-spin:hover .sw-face,
    .side-menu-spin:focus-visible .sw-face { transform: rotate(90deg); }

    .side-menu-text {
      display: grid;
      gap: 5px;
      min-width: 0;
    }
    .side-menu-label {
      font-size: 11.5px;
      font-weight: 900;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--tr-text-primary);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .side-menu-sub {
      font-size: 10.5px;
      font-weight: 600;
      letter-spacing: .02em;
      color: var(--tr-text-muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      transition: color var(--tr-transition-standard);
    }
    /* Perks running: the count, in the store's amber. */
    .side-menu-store.has-active .side-menu-sub { color: #f0cf84; }
    /* Out of energy with a recharge for sale. The one moment the
       store is the answer to what the player just ran into, so it
       takes the cyan the lobby uses for "do this next" -- steady,
       not blinking. */
    .side-menu-store.is-offering { border-color: var(--tr-border-cyan); }
    .side-menu-store.is-offering .side-menu-sub { color: var(--tr-cyan); }
    .side-menu-logs.has-news .side-menu-sub { color: rgba(156, 232, 255, 0.86); }
    /* A notice this browser has not opened the log on yet. */
    .side-menu-dot {
      position: absolute;
      top: -4px;
      right: -4px;
      width: 10px;
      height: 10px;
      border-radius: 50%;
      background: var(--tr-cyan);
      border: 2px solid #0a0f1a;
    }
    .side-menu-dot[hidden] { display: none; }

    /* The official logo at the head of the column. Not a tile: it is
       not a button, so it has no frame, only edges that fade into the
       page, and 'lighten' so the art's near-black background takes
       the page's own colour instead of reading as a box. The column
       width sizes it; syncSideMenu counts its height with the tiles. */
    .side-menu-brand {
      display: grid;
      place-items: center;
      pointer-events: none;
    }
    .side-menu-brand-mark {
      display: block;
      width: 100%;
      max-width: 200px;
      height: auto;
      aspect-ratio: 560 / 320;
      mix-blend-mode: lighten;
      -webkit-mask-image:
        linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%),
        linear-gradient(to bottom, transparent 0, #000 12%, #000 86%, transparent 100%);
      -webkit-mask-composite: source-in;
      mask-image:
        linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%),
        linear-gradient(to bottom, transparent 0, #000 12%, #000 86%, transparent 100%);
      mask-composite: intersect;
    }
    /* ---- tucked under the HOLD box ---- */
    .side-menu.is-tucked { gap: 8px; }
    .side-menu.is-tucked .side-menu-item {
      flex-direction: column;
      justify-content: center;
      gap: 7px;
      min-height: 0;
      padding: 10px 6px 9px;
      border-radius: 12px;
      text-align: center;
    }
    .side-menu.is-tucked .side-menu-item::after { left: 10px; right: 10px; }
    .side-menu.is-tucked .side-menu-icon { width: 34px; height: 34px; border-radius: 10px; }
    .side-menu.is-tucked .side-menu-icon svg { width: 19px; height: 19px; }
    .side-menu.is-tucked .side-menu-spin .side-menu-icon svg { width: 34px; height: 34px; }
    .side-menu.is-tucked .side-menu-text { justify-items: center; width: 100%; }
    .side-menu.is-tucked .side-menu-label {
      font-size: 9px;
      letter-spacing: .1em;
      line-height: 1.3;
      white-space: normal;
    }
    .side-menu.is-tucked .side-menu-sub { display: none; }
    /* Icons alone: the words move into a flyout beside the tile on
       hover or focus, so an icon is never the only thing saying what
       a button does. The same framed surface as the tiles, lifted
       clear of the board by position rather than by glow. */
    .side-menu.is-icons .side-menu-item { padding: 7px 4px; }
    .side-menu.is-icons .side-menu-text {
      position: absolute;
      left: calc(100% + 10px);
      top: 50%;
      z-index: 2;
      width: max-content;
      max-width: 220px;
      justify-items: start;
      padding: 9px 12px;
      border-radius: 10px;
      border: 1px solid var(--panel-edge-hi);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.99), rgba(9, 13, 22, 1));
      box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
      text-align: left;
      opacity: 0;
      translate: -4px -50%;
      pointer-events: none;
      transition:
        opacity var(--tr-transition-fast),
        translate var(--tr-transition-fast);
    }
    .side-menu.is-icons .side-menu-item:hover .side-menu-text,
    .side-menu.is-icons .side-menu-item:focus-visible .side-menu-text {
      opacity: 1;
      translate: 0 -50%;
    }
    .side-menu.is-icons .side-menu-label {
      font-size: 10.5px;
      letter-spacing: .12em;
      white-space: nowrap;
    }
    .side-menu.is-icons .side-menu-sub { display: block; }

    /* =========================================================
       CHANGE LOGS

       The serverNotices history as a timeline: the date on the left,
       written once per day, a dot on the line in the notice's kind
       colour, and the text on its own quiet surface. The kind colours
       are the welcome panel's, so a notice looks the same in both.
       ========================================================= */
    .changelog-card {
      width: min(94vw, 660px);
      grid-template-rows: auto 1fr;
      gap: 20px;
      padding: 30px 26px 24px;
    }
    .controls-card-title .changelog-card-icon {
      width: 16px;
      height: 16px;
      vertical-align: -3px;
      margin-right: 8px;
      color: rgba(95, 212, 255, 0.72);
    }
    .changelog-sheet {
      width: 100%;
      min-height: 0;
      align-self: stretch;
      overflow-y: auto;
      padding: 2px 8px 2px 0;
      scrollbar-width: thin;
      scrollbar-color: rgba(151, 184, 222, 0.22) transparent;
    }
    .changelog-sheet::-webkit-scrollbar { width: 8px; }
    .changelog-sheet::-webkit-scrollbar-track { background: transparent; }
    .changelog-sheet::-webkit-scrollbar-thumb {
      background: rgba(151, 184, 222, 0.18);
      border-radius: 99px;
    }
    .changelog-list {
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .changelog-entry {
      --note-accent: #5fd4ff;
      display: grid;
      grid-template-columns: 72px minmax(0, 1fr);
      column-gap: 18px;
    }
    .changelog-entry[data-kind="announcement"] { --note-accent: #b79cff; }
    .changelog-entry[data-kind="broadcast"] { --note-accent: #ffc860; }
    .changelog-date {
      padding-top: 17px;
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
      text-align: right;
      color: var(--tr-text-secondary);
      white-space: nowrap;
    }
    .changelog-body {
      position: relative;
      padding: 14px 0 22px 24px;
      border-left: 1px solid var(--panel-edge);
    }
    .changelog-entry:last-child .changelog-body { padding-bottom: 6px; }
    .changelog-body::before {
      content: "";
      position: absolute;
      left: -5px;
      top: 18px;
      width: 9px;
      height: 9px;
      border-radius: 50%;
      background: #0a0f1a;
      border: 2px solid var(--note-accent);
      box-sizing: border-box;
    }
    .changelog-kind {
      font-size: 9.5px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--note-accent);
    }
    .changelog-text {
      margin: 9px 0 0;
      padding: 13px 16px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.025);
      font-size: 13px;
      line-height: 1.65;
      color: rgba(198, 216, 236, 0.86);
      white-space: pre-wrap;
      overflow-wrap: anywhere;
    }
    .changelog-empty {
      padding: 18px;
      border-radius: 12px;
      border: 1px dashed var(--panel-edge);
      text-align: center;
      font-size: 12px;
      color: rgba(151, 184, 222, 0.5);
    }
    /* A phone: the date goes above its entry instead of beside it. */
    @media (max-width: 520px) {
      .changelog-card { padding: 26px 16px 18px; gap: 16px; }
      .changelog-entry { grid-template-columns: minmax(0, 1fr); }
      .changelog-date {
        padding: 12px 0 0 24px;
        border-left: 1px solid var(--panel-edge);
        text-align: left;
      }
      .changelog-date:empty { display: none; }
      .changelog-text { padding: 11px 13px; font-size: 12.5px; }
    }

    /* =========================================================
       NARROW LANDSCAPE

       Two boards, each with a HOLD and a NEXT column, measure about
       1183px together and nothing between the 1300px chip
       breakpoint and the 1000px portrait reflow ever narrowed them
       -- and the portrait rules are keyed to orientation, so a
       short WIDE window got no reflow at all. The result was a
       lobby that scrolled sideways, which for a fixed layout means
       part of the second board is simply unreachable.

       1366 at 125% zoom lands in that band, and so do 1600 at 150%
       and 1440 at 125%. The side columns are what give way: the
       piece previews read perfectly well smaller, and the boards
       have already been capped against the viewport above.
       ========================================================= */
    @media (max-width: 1250px) and (orientation: landscape) {
      .hold-column,
      .next-column { width: 76px; }
      .piece-stack,
      .piece-stack.next-stack { width: 72px; }
      .piece-card { width: 72px; padding: 5px; }
      .board-side { gap: 8px; }
      .board-side + .board-side { padding-left: 14px; }
      .game-layout { gap: 18px; }
    }
    /* Tighter again below the width the portrait rules would have
       taken over at, because they never fire in landscape. This is
       a 1366 or 1440 screen at 150% zoom -- a 900px-wide canvas for
       two boards -- so the previews give up as much as they can
       while staying legible. */
    @media (max-width: 1000px) and (orientation: landscape) {
      .hold-column,
      .next-column { width: 62px; }
      .piece-stack,
      .piece-stack.next-stack { width: 58px; }
      .piece-card { width: 58px; padding: 4px; }
      .board-side { gap: 6px; }
      .board-side + .board-side { padding-left: 10px; }
      .game-layout { gap: 12px; }
    }
    .game-chat-launcher {
      position: fixed;
      right: calc(var(--game-chat-gutter) + var(--players-dock));
      bottom: 18px;
      z-index: 40;
      display: flex;
      align-items: center;
      /* a size up from the 12px/14px it had: the room's only way in,
         and it read as small beside the boards */
      gap: 11px;
      padding: 14px 18px;
      border-radius: 18px;
      border: 1px solid var(--tr-border-subtle);
      background:
        radial-gradient(circle at 20% 0%, rgba(95, 212, 255, 0.08), transparent 46%),
        var(--tr-control-gradient);
      color: var(--tr-text-primary);
      cursor: pointer;
      box-shadow:
        var(--tr-inset-highlight),
        0 12px 30px rgba(0, 0, 0, 0.32),
        0 0 20px rgba(95, 212, 255, 0.055);
      font-size: 14px;
      font-weight: 900;
      letter-spacing: .08em;
      text-transform: uppercase;
    }
    /* The shared control rule above clips its members with overflow:
       hidden, which cut the unread badge off where it hangs over the
       corner. The launcher has no sheen of its own to contain. */
    .game-chat-launcher { position: fixed; overflow: visible; }
    .game-chat-badge {
      position: absolute;
      top: -8px;
      right: -8px;
      z-index: 2;
      min-width: 22px;
      height: 22px;
      padding: 0 6px;
      border-radius: 999px;
      background: linear-gradient(180deg, #ff6666 0%, #c11212 100%);
      color: #fff;
      font-size: 11px;
      font-weight: 900;
      display: none;
      align-items: center;
      justify-content: center;
      box-shadow: 0 0 0 1px rgba(255,255,255,0.14) inset, 0 8px 18px rgba(193, 18, 18, 0.35);
    }
    .game-chat-launcher .chat-icon {
      width: 25px;
      height: 20px;
      display: grid;
      place-items: center;
      border-radius: 5px;
      background: linear-gradient(180deg, rgba(230, 239, 255, 0.96), rgba(184, 202, 232, 0.98));
      box-shadow: 0 0 0 1px rgba(255,255,255,0.16) inset;
      color: #111827;
      font-size: 11px;
      line-height: 1;
      position: relative;
    }
    .game-chat-launcher .chat-icon::before {
      content: '...';
      letter-spacing: 1px;
      transform: translateY(-1px);
      font-weight: 900;
    }
    .game-chat-launcher .chat-icon::after {
      content: '';
      position: absolute;
      left: 4px;
      bottom: -4px;
      width: 0;
      height: 0;
      border-left: 4px solid transparent;
      border-right: 0 solid transparent;
      border-top: 4px solid #f4f7fb;
    }
    .game-chat-panel {
      position: fixed;
      right: calc(var(--game-chat-gutter) + var(--players-dock));
      bottom: 18px;
      width: min(92vw, 380px);
      z-index: 230;
      opacity: 0;
      pointer-events: none;
      transform: translateY(8px) scale(0.98);
      /* the right transition matches the players panel's own slide */
      transition: opacity .18s ease, transform .18s ease, right .28s cubic-bezier(.2, .85, .3, 1);
    }
    .game-chat-panel.open {
      opacity: 1;
      pointer-events: auto;
      transform: translateY(0) scale(1);
    }
    .game-chat-panel.live-mode {
      left: 12px;
      top: 12px;
      right: auto;
      bottom: auto;
      width: min(92vw, 380px);
      max-height: 52vh;
    }
    .game-chat-card {
      background: var(--tr-panel-gradient);
      border: 1px solid var(--tr-border-subtle);
      border-radius: 20px;
      box-shadow:
        var(--tr-inset-highlight),
        var(--tr-shadow-panel);
      padding: 14px;
      display: grid;
      gap: 12px;
    }
    .game-chat-messages {
      height: 450px;
      overflow-y: auto;
      overflow-x: hidden;
      display: grid;
      align-content: start;
      /* 2px, not 8: the rows carry their own padding now, and the
         space between two turns of speech is set there so that a
         follow-on line from the same sender can sit closer to the
         one above it than a new speaker does. */
      gap: 2px;
      padding: 6px;
      border-radius: 14px;
      background: rgba(6, 11, 21, 0.42);
      border: 1px solid rgba(255,255,255,0.06);
      scrollbar-width: thin;
      scrollbar-color: rgba(125, 60, 255, 0.75) rgba(7, 12, 22, 0.85);
    }
    .game-chat-messages::-webkit-scrollbar { width: 10px; }
    .game-chat-messages::-webkit-scrollbar-track { background: rgba(7, 12, 22, 0.85); border-radius: 999px; }
    .game-chat-messages::-webkit-scrollbar-thumb {
      background: linear-gradient(180deg, rgba(37, 183, 255, 0.55), rgba(125, 51, 255, 0.75));
      border-radius: 999px;
      border: 2px solid rgba(7, 12, 22, 0.85);
    }
    .game-chat-messages::-webkit-scrollbar-thumb:hover {
      background: linear-gradient(180deg, rgba(37, 183, 255, 0.8), rgba(125, 51, 255, 0.95));
    }
    .game-chat-empty {
      display: block;
      color: var(--muted);
      font-size: 12px;
      letter-spacing: .08em;
      text-align: center;
      padding-top: 2px;
    }
    /* ===== ONE MESSAGE =====

         [flag]  [name · time        ]  [react]
                 [message text       ]
                 [reaction pills     ]
                 [picker, when open  ]

       Every part of this was inline styles set from JavaScript as
       each element was built. In the stylesheet it can be themed,
       it can respond to live mode and to hover, and the rules that
       were already here can actually win.

       The third column is reserved whether or not the button is in
       it. The button only appears on hover, and a column that is
       already there means appearing cannot reflow the message. On
       a row nobody can react to -- live mode, signed out -- no
       button is built and the auto column collapses to nothing. */
    .game-chat-message {
      display: grid;
      grid-template-columns: 22px minmax(0, 1fr) auto;
      column-gap: 10px;
      row-gap: 4px;
      align-items: start;
      /* a grid item's automatic minimum is its min-content width;
         without this one long unbroken word widens the scroller */
      min-width: 0;
      padding: 6px 8px;
      border-radius: 10px;
      color: var(--tr-text-primary);
      font-size: 13px;
      line-height: 1.45;
      transition: background var(--tr-transition-fast);
    }
    /* A wash on hover, which is also what introduces the react
       button rather than having it appear out of clear space. */
    .game-chat-message:hover,
    .game-chat-message:focus-within {
      background: rgba(255, 255, 255, 0.035);
    }
    /* A follow-on line from the same sender: no repeated name, flag
       or clock, and pulled up close under the line above.

       The negative margin is what makes the grouping visible. Row
       padding alone left 10px inside a turn against 16px between
       two, which measures as a difference and does not read as one;
       cancelling the previous row's bottom padding gives 3px
       against 14px, which does. It eats into space that is blank by
       construction -- the bottom padding of a row from the same
       sender -- so nothing overlaps anything drawn. */
    /* ---- a follow-on line, and the run it belongs to ----

       This used to pull itself up with margin-top: -6px against the
       list's 2px gap, so two consecutive rows overlapped by four
       pixels. On a plain row nothing showed, because a plain row
       paints no background. On an admin or a dev row the two
       translucent fills stacked in that band and came out as a dark
       stripe, with both sets of rounded corners cutting into it --
       one turn of speech reading as two broken cards.

       The pull is taken off the bottom of the row above instead of
       out of the row above. The gap between the two lines of text is
       the same three pixels it always was -- 6+2-6+1 became 2+2-2+1
       -- and nothing is painted twice.

       The corners then join the run into one shape. They are set on
       every row rather than only the tinted ones because the hover
       wash is a background too, and a hovered row mid-run should not
       round itself out of the run either. */
    .game-chat-message.is-grouped {
      margin-top: -2px;
      padding-top: 1px;
      border-top-left-radius: 0;
      border-top-right-radius: 0;
    }
    .game-chat-message:has(+ .is-grouped) {
      padding-bottom: 2px;
      border-bottom-left-radius: 0;
      border-bottom-right-radius: 0;
    }
    /* The one row treatment there is. An announcement is what a
       reader most needs to pick out of a moving feed; a player's
       own lines are already marked by the colour of the name and
       do not need saying twice. */
    .game-chat-message.is-admin {
      background: rgba(140, 116, 255, 0.07);
      box-shadow: inset 2px 0 0 rgba(140, 116, 255, 0.45);
    }
    .game-chat-message.is-admin:hover { background: rgba(140, 116, 255, 0.11); }
    /* The same shape as the admin row, in the dev gold. Both rules
       can match one message; this one is later, so a dev who is also
       an admin reads as a dev and keeps the ADMIN pill. */
    .game-chat-message.is-dev {
      background: rgba(224, 184, 114, 0.06);
      box-shadow: inset 2px 0 0 rgba(224, 184, 114, 0.5);
    }
    .game-chat-message.is-dev:hover { background: rgba(224, 184, 114, 0.1); }
    .game-chat-body {
      grid-column: 2;
      min-width: 0;
      display: grid;
      /* Pinned, not implicit. An implicit column is sized to the
         widest child, and the picker is a max-content tray of eight
         -- in a 340px rail that made the body wider than the column
         it sits in, and everything right-aligned inside it (the
         clock) was pushed out under the scroller's clipped edge. */
      grid-template-columns: minmax(0, 1fr);
      gap: 3px;
    }
    .game-chat-meta {
      display: flex;
      align-items: baseline;
      gap: 7px;
      min-width: 0;
    }
    /* em rather than px throughout the row, so live mode can size
       the whole message from the one declaration on the row. */
    .game-chat-name {
      font-size: 0.96em;
      font-weight: 800;
      letter-spacing: 0.01em;
      color: #dff4ff;
      overflow-wrap: anywhere;
    }
    .game-chat-message.is-mine .game-chat-name { color: #69d69a; }
    .game-chat-message.is-admin .game-chat-name { color: #b3a1ff; }
    /* After the admin rules on purpose: an account can carry both,
       and dev is the rarer mark, so it is the one that shows. */
    .game-chat-message.is-dev .game-chat-name { color: #f0d5a4; }
    .game-chat-crown {
      flex: none;
      display: inline-flex;
      align-self: center;
      width: 0.95em;
      height: 0.95em;
      /* the muted gold already used for the mobile notice heading and
         the searching dot, rather than a new accent */
      color: #e0b872;
    }
    .game-chat-crown svg { width: 100%; height: 100%; display: block; }
    /* The word, for anyone who does not read a crown as meaning
       anything. Lower case and quiet: it is a note against the name,
       not a title competing with it. */
    .game-chat-dev {
      flex: none;
      margin-left: -3px;
      font-size: 0.74em;
      font-weight: 700;
      letter-spacing: 0.04em;
      color: rgba(224, 184, 114, 0.85);
    }
    /* A badge of its own rather than "(ADMIN)" glued to the end of
       the name: the name belongs to the player, the badge to the
       game, and a reader can tell which is which. */
    .game-chat-tag {
      flex: none;
      padding: 1px 6px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid rgba(140, 116, 255, 0.4);
      background: rgba(140, 116, 255, 0.14);
      color: #c4b6ff;
      font-size: 0.68em;
      font-weight: 800;
      letter-spacing: 0.1em;
      line-height: 1.7;
    }
    /* Far side of the line: there to be read when wanted, never
       part of the sentence. */
    .game-chat-time {
      margin-left: auto;
      flex: none;
      font-size: 0.76em;
      font-variant-numeric: tabular-nums;
      letter-spacing: 0.06em;
      color: var(--tr-text-muted);
    }
    .game-chat-text {
      color: #eef4fb;
      overflow-wrap: anywhere;
      word-break: break-word;
    }

    /* ---- server notices ----

       A message from the game, not from anybody in the room. It
       breaks the feed's own column grid deliberately: a player row is
       a flag, a name and a line of text, and a notice has none of
       those, so borrowing that shape would leave a gap where a face
       should be and read as somebody with no name.

       A full-width card instead, with a rule down its left edge --
       the one piece of colour, and the only thing that changes
       between the three kinds. No glow: a notice is meant to be
       noticed once, not to sit there pulsing. */
    /* A burn in the room. Built on the notice's frame so it reads as
       the game speaking rather than as a player's message, but warm
       instead of blue -- it is the one line in the chat that is good
       news for everybody holding the token. */
    .game-chat-burn {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      align-items: start;
      gap: 9px;
      min-width: 0;
      margin: 3px 0;
      padding: 9px 11px 10px;
      border-radius: 10px;
      border: 1px solid rgba(232, 138, 96, 0.22);
      border-left: 2px solid rgba(232, 138, 96, 0.55);
      background:
        linear-gradient(180deg, rgba(232, 138, 96, 0.07), rgba(232, 138, 96, 0.02));
      font-size: 13px;
      line-height: 1.45;
    }
    .game-chat-burn-flame { width: 17px; height: 17px; color: #e0906a; }
    .game-chat-burn-flame svg { width: 100%; height: 100%; display: block; }
    /* Only while it is still going. A finished burn should sit still
       -- a flame that never stops moving turns into wallpaper. */
    .game-chat-burn.is-pending .game-chat-burn-flame {
      animation: burnFlicker 1.9s ease-in-out infinite;
    }
    @keyframes burnFlicker {
      0%, 100% { opacity: .85; transform: scale(1); }
      45% { opacity: 1; transform: scale(1.09); }
    }
    .game-chat-burn-body { min-width: 0; display: grid; gap: 5px; }
    .game-chat-burn-line { color: var(--tr-text-secondary); }
    .game-chat-burn-who { font-weight: 800; color: #e4eef8; }
    .game-chat-burn-amount {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-weight: 800;
      color: #f0c49c;
    }
    .game-chat-burn-perk { font-weight: 700; color: #cfe0f2; }
    .game-chat-burn-foot {
      display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    }
    .game-chat-burn-status {
      padding: 2px 7px;
      border-radius: 999px;
      border: 1px solid rgba(232, 138, 96, 0.3);
      background: rgba(232, 138, 96, 0.1);
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .07em;
      text-transform: uppercase;
      color: #e8a882;
    }
    .game-chat-burn.is-burned .game-chat-burn-status {
      border-color: rgba(105, 214, 154, 0.3);
      background: rgba(105, 214, 154, 0.09);
      color: #8fe0b0;
    }
    .game-chat-burn.is-failed .game-chat-burn-status {
      border-color: rgba(255, 111, 130, 0.3);
      background: rgba(255, 111, 130, 0.09);
      color: #e8a0a8;
    }
    .game-chat-burn-link {
      font-size: 10px;
      font-weight: 700;
      color: rgba(159, 192, 228, 0.9);
      text-decoration: none;
      border-bottom: 1px solid rgba(159, 192, 228, 0.3);
      pointer-events: auto;
    }
    .game-chat-burn-link:hover { color: #cfe0f2; border-bottom-color: #cfe0f2; }
    @media (prefers-reduced-motion: reduce) {
      .game-chat-burn.is-pending .game-chat-burn-flame { animation: none; }
    }
    /* A reward payout in the room: the burn card's frame in a quiet
       green, still and unanimated -- it is a receipt, not an alert. */
    .game-chat-payout {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      align-items: start;
      gap: 9px;
      min-width: 0;
      margin: 3px 0;
      padding: 9px 11px 10px;
      border-radius: 10px;
      border: 1px solid rgba(105, 214, 154, 0.2);
      border-left: 2px solid rgba(105, 214, 154, 0.5);
      background:
        linear-gradient(180deg, rgba(105, 214, 154, 0.06), rgba(105, 214, 154, 0.015));
      font-size: 13px;
      line-height: 1.45;
    }
    .game-chat-payout-mark { width: 17px; height: 17px; color: #7fd6a6; }
    .game-chat-payout-mark svg { width: 100%; height: 100%; display: block; }
    .game-chat-payout-body { min-width: 0; display: grid; gap: 5px; }
    .game-chat-payout-line { color: var(--tr-text-secondary); overflow-wrap: anywhere; }
    .game-chat-payout-who { font-weight: 800; color: #e4eef8; }
    .game-chat-payout-amount {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-weight: 800;
      color: #9fe3bd;
    }
    .game-chat-payout-foot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
    .game-chat-payout-status {
      padding: 2px 7px;
      border-radius: 999px;
      border: 1px solid rgba(105, 214, 154, 0.3);
      background: rgba(105, 214, 154, 0.09);
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .07em;
      text-transform: uppercase;
      color: #8fe0b0;
    }
    .game-chat-payout-link {
      font-size: 10px;
      font-weight: 700;
      color: rgba(159, 192, 228, 0.9);
      text-decoration: none;
      border-bottom: 1px solid rgba(159, 192, 228, 0.3);
      pointer-events: auto;
    }
    .game-chat-payout-link:hover { color: #cfe0f2; border-bottom-color: #cfe0f2; }

    .game-chat-notice {
      display: grid;
      gap: 5px;
      min-width: 0;
      margin: 3px 0;
      padding: 9px 11px 10px;
      border-radius: 10px;
      border: 1px solid var(--tr-border-subtle);
      border-left: 2px solid rgba(95, 212, 255, 0.55);
      background:
        linear-gradient(180deg, rgba(95, 212, 255, 0.055), rgba(95, 212, 255, 0.015));
      color: var(--tr-text-primary);
      font-size: 13px;
      line-height: 1.45;
    }
    .game-chat-notice-head {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
    }
    .game-chat-notice-badge {
      flex: none;
      padding: 2px 7px;
      border-radius: 999px;
      background: rgba(95, 212, 255, 0.14);
      color: #bfe9ff;
      font-size: 9px;
      font-weight: 900;
      letter-spacing: .12em;
      text-transform: uppercase;
    }
    .game-chat-notice-from {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: rgba(151, 184, 222, 0.6);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .game-chat-notice-text {
      color: #eef4fb;
      overflow-wrap: anywhere;
      word-break: break-word;
      /* the console sends one block of text; its line breaks are
         part of what was written and are kept */
      white-space: pre-wrap;
    }
    /* An update is the routine one and keeps the house cyan. The
       other two shift the edge and the badge only, so all three read
       as the same object at a glance. */
    .game-chat-notice.is-announcement {
      border-left-color: rgba(183, 156, 255, 0.6);
      background:
        linear-gradient(180deg, rgba(183, 156, 255, 0.06), rgba(183, 156, 255, 0.015));
    }
    .game-chat-notice.is-announcement .game-chat-notice-badge {
      background: rgba(183, 156, 255, 0.16);
      color: #d9cbff;
    }
    .game-chat-notice.is-broadcast {
      border-left-color: rgba(255, 200, 96, 0.6);
      background:
        linear-gradient(180deg, rgba(255, 200, 96, 0.06), rgba(255, 200, 96, 0.015));
    }
    .game-chat-notice.is-broadcast .game-chat-notice-badge {
      background: rgba(255, 200, 96, 0.16);
      color: #ffe2ab;
    }
    .game-chat-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      padding-bottom: 10px;
      border-bottom: 1px solid var(--tr-border-subtle);
    }
    .game-chat-title {
      font-size: 13px;
      font-weight: 900;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: #dff4ff;
    }
    .game-chat-close {
      width: 30px;
      height: 30px;
      border: 0;
      border-radius: 50%;
      background: linear-gradient(180deg, #ff6666 0%, #c11212 60%, #8b0d0d 100%);
      color: #fff;
      font-size: 18px;
      cursor: pointer;
    }
    .game-chat-input-row {
      display: flex;
      gap: 8px;
      align-items: center;
    }
    .game-chat-panel.live-mode .game-chat-input-row {
      display: none;
    }
    /* ---- pictures in the chat ----
       The attachment strip, above the input rather than inside it so
       a wide picture cannot squash the box it is being sent from. */
    .game-chat-attachment {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-bottom: 8px;
      padding: 8px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
    }
    .game-chat-attachment[hidden] { display: none; }
    .game-chat-attachment-preview {
      flex: 0 0 auto;
      width: 46px;
      height: 46px;
      border-radius: 8px;
      object-fit: cover;
      background: rgba(0, 0, 0, 0.3);
    }
    .game-chat-attachment-meta {
      min-width: 0;
      display: grid;
      gap: 2px;
    }
    .game-chat-attachment-name {
      font-size: 11px;
      font-weight: 700;
      color: var(--tr-text-primary);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .game-chat-attachment-note {
      font-size: 10px;
      color: var(--tr-text-muted);
    }
    .game-chat-attachment-remove {
      margin-left: auto;
      flex: 0 0 auto;
      width: 24px;
      height: 24px;
      display: grid;
      place-items: center;
      border-radius: 50%;
      border: 1px solid var(--panel-edge);
      background: rgba(0, 0, 0, 0.3);
      color: rgba(207, 228, 245, 0.7);
      font-size: 14px;
      line-height: 1;
      cursor: pointer;
    }
    .game-chat-attachment-remove:hover {
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }
    .game-chat-attach {
      flex: 0 0 auto;
      width: 38px;
      height: 38px;
      display: grid;
      place-items: center;
      border-radius: 11px;
      border: 1px solid var(--panel-edge);
      background: var(--tr-control-gradient);
      color: rgba(207, 228, 245, 0.72);
      cursor: pointer;
      transition: color var(--tr-transition-fast),
        border-color var(--tr-transition-fast);
    }
    .game-chat-attach svg { width: 19px; height: 19px; display: block; }
    .game-chat-attach:hover:not(:disabled) {
      color: #dff4ff;
      border-color: var(--tr-border-cyan);
    }
    .game-chat-attach:disabled { opacity: .5; cursor: default; }
    /* The game chat's box and its tools as one field. The border and
       fill move from the input to the field, the input goes bare
       inside it, and the three tools -- picture, GIF, emoji -- are
       plain icons at its right edge. The row is then just the field
       and Send, which is what fits a 340px rail. */
    .game-chat-field {
      flex: 1 1 auto;
      min-width: 0;
      display: flex;
      align-items: center;
      border: 1px solid rgba(55, 168, 255, 0.18);
      border-radius: 12px;
      background: rgba(255, 255, 255, .04);
      transition: border-color var(--tr-transition-fast);
    }
    .game-chat-field:focus-within { border-color: rgba(55, 168, 255, 0.4); }
    /* ---- @mention suggestions, just above the field ---- */
    .game-chat-field { position: relative; }
    .game-chat-mentions {
      position: absolute;
      left: 0;
      right: 0;
      bottom: calc(100% + 6px);
      z-index: 6;
      display: flex;
      flex-direction: column;
      padding: 4px;
      border: 1px solid rgba(55, 168, 255, 0.18);
      border-radius: 10px;
      background: rgba(12, 18, 30, 0.97);
      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
    }
    .game-chat-mentions[hidden] { display: none; }
    .game-chat-mention-option {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 6px 8px;
      border-radius: 7px;
      color: #d9e3ee;
      font-size: 13px;
      cursor: pointer;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    /* a quiet dot for somebody here right now; offline names go without */
    .game-chat-mention-option::before {
      content: '';
      flex: 0 0 auto;
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: rgba(184, 179, 168, 0.35);
    }
    .game-chat-mention-option.is-online::before { background: #5fd49a; }
    .game-chat-mention-option.is-active,
    .game-chat-mention-option:hover { background: rgba(55, 168, 255, 0.12); color: #fff; }
    /* ---- @mentions in a posted message ---- */
    .game-chat-mention { color: #7cc4ff; font-weight: 600; }
    .game-chat-mention.is-me {
      color: #ffd98a;
      background: rgba(255, 217, 138, 0.1);
      border-radius: 4px;
      padding: 0 2px;
    }
    .game-chat-message.is-mentioning-me { background: rgba(255, 217, 138, 0.05); }
    .game-chat-field .game-chat-input {
      border: 0;
      background: transparent;
      border-radius: 12px 0 0 12px;
      padding-right: 4px;
    }
    .game-chat-tools {
      flex: 0 0 auto;
      display: flex;
      align-items: center;
      gap: 0;
      padding-right: 4px;
    }
    .game-chat-tools .game-chat-attach {
      width: 28px;
      height: 30px;
      border: 0;
      border-radius: 8px;
      background: transparent;
      color: rgba(207, 228, 245, 0.5);
    }
    .game-chat-tools .game-chat-attach svg { width: 17px; height: 17px; }
    .game-chat-tools .game-chat-attach:hover:not(:disabled),
    .game-chat-tools .game-chat-attach.is-active {
      color: #dff4ff;
      background: rgba(255, 255, 255, 0.06);
    }
    .game-chat-gif-btn {
      font: inherit;
      font-size: 9.5px;
      font-weight: 800;
      letter-spacing: .04em;
    }
    .game-chat-tools .game-chat-gif-btn {
      width: 30px;
    }
    /* ---- the emoji and GIF picker ---- */
    .game-chat-picker {
      display: flex;
      flex-direction: column;
      gap: 6px;
      margin: 0 10px 6px;
      padding: 8px;
      border: 1px solid var(--panel-edge);
      border-radius: 12px;
      background: rgba(10, 16, 26, 0.92);
    }
    .game-chat-picker[hidden] { display: none; }
    .game-chat-picker-head {
      display: flex;
      align-items: center;
      gap: 6px;
    }
    .game-chat-picker-search {
      flex: 1 1 auto;
      min-width: 0;
      height: 30px;
      padding: 0 10px;
      border: 1px solid var(--tr-border-subtle);
      border-radius: 8px;
      background: rgba(255, 255, 255, 0.04);
      color: var(--tr-text-primary);
      font: inherit;
      font-size: 12px;
      outline: none;
    }
    .game-chat-picker-search:focus { border-color: var(--tr-border-cyan); }
    .game-chat-picker-tabs {
      display: flex;
      gap: 2px;
      overflow-x: auto;
      scrollbar-width: none;
    }
    .game-chat-picker-tabs[hidden] { display: none; }
    .game-chat-picker-tab {
      flex: 0 0 auto;
      width: 30px;
      height: 26px;
      padding: 0;
      border: 0;
      border-radius: 7px;
      background: transparent;
      font-size: 15px;
      line-height: 1;
      cursor: pointer;
      opacity: .6;
      transition: opacity var(--tr-transition-fast), background var(--tr-transition-fast);
    }
    .game-chat-picker-tab:hover,
    .game-chat-picker-tab.is-active {
      opacity: 1;
      background: rgba(255, 255, 255, 0.06);
    }
    .game-chat-picker-grid {
      max-height: 210px;
      overflow-y: auto;
      overflow-x: hidden;
      overscroll-behavior: contain;
      padding-right: 2px;
      /* the chat feed's own scrollbar, thinner: Firefox first, then
         the WebKit parts */
      scrollbar-width: thin;
      scrollbar-color: rgba(37, 183, 255, 0.45) transparent;
    }
    .game-chat-picker-grid::-webkit-scrollbar { width: 6px; height: 0; }
    .game-chat-picker-grid::-webkit-scrollbar-track { background: transparent; }
    .game-chat-picker-grid::-webkit-scrollbar-thumb {
      background: linear-gradient(180deg, rgba(37, 183, 255, 0.45), rgba(125, 51, 255, 0.6));
      border-radius: 999px;
    }
    .game-chat-picker-grid::-webkit-scrollbar-thumb:hover {
      background: linear-gradient(180deg, rgba(37, 183, 255, 0.7), rgba(125, 51, 255, 0.85));
    }
    .game-chat-picker-grid::-webkit-scrollbar-button { display: none; height: 0; }
    .game-chat-picker-grid.is-emoji {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));
      gap: 2px;
    }
    .game-chat-picker-emoji {
      height: 32px;
      padding: 0;
      border: 0;
      border-radius: 7px;
      background: transparent;
      font-size: 19px;
      line-height: 1;
      cursor: pointer;
    }
    .game-chat-picker-emoji:hover { background: rgba(255, 255, 255, 0.07); }
    /* GIFs as an even two-column grid of equal tiles. A CSS columns
       layout was here first and spilled sideways into a horizontal
       scrollbar; a grid cannot. Each GIF is cropped to its tile. */
    .game-chat-picker-grid.is-gif {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      grid-auto-rows: 92px;
      gap: 6px;
    }
    .game-chat-picker-gif {
      display: block;
      width: 100%;
      height: 100%;
      min-width: 0;
      margin: 0;
      padding: 0;
      border: 1px solid transparent;
      border-radius: 8px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.04);
      cursor: pointer;
      break-inside: avoid;
      transition: border-color var(--tr-transition-fast);
    }
    .game-chat-picker-gif:hover { border-color: var(--tr-border-cyan); }
    .game-chat-picker-gif img {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    .game-chat-picker-empty {
      grid-column: 1 / -1;
      padding: 18px 8px;
      text-align: center;
      font-size: 11.5px;
      color: var(--tr-text-muted);
    }
    .game-chat-picker-foot {
      font-size: 9.5px;
      letter-spacing: .06em;
      text-align: right;
      color: var(--tr-text-muted);
    }
    .game-chat-picker-foot[hidden] { display: none; }
    /* The picture in the feed.

       Capped on both axes rather than only on width: a tall
       screenshot constrained by width alone runs for several
       screens. The box keeps the file's own aspect ratio, which the
       script sets from naturalWidth once it has decoded, so the feed
       does not jump as pictures arrive. */
    .game-chat-image {
      display: block;
      margin-top: 6px;
      max-width: min(100%, 260px);
      max-height: 220px;
      border-radius: 10px;
      overflow: hidden;
      border: 1px solid var(--panel-edge);
      background: rgba(0, 0, 0, 0.25);
      line-height: 0;
    }
    .game-chat-image img {
      display: block;
      width: 100%;
      height: 100%;
      max-height: 220px;
      object-fit: contain;
    }
    /* A file that will not load leaves a marked gap rather than a
       broken-image glyph and a stretched box. */
    .game-chat-image.is-broken {
      min-height: 54px;
      line-height: 1.4;
      font-size: 10px;
      letter-spacing: .06em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      display: grid;
      place-items: center;
      aspect-ratio: auto !important;
    }
    .game-chat-image.is-broken::after { content: "Picture unavailable"; }
    .game-chat-image:not(.is-broken) { cursor: zoom-in; }
    /* ---- a chat picture at full size ----
       Above everything, including the chat panel it was opened from.
       The picture is capped against the viewport rather than given a
       size, so a tall screenshot and a wide one both land whole
       inside the window with nothing cropped and nothing scrolled. */
    .chat-image-viewer {
      position: fixed;
      inset: 0;
      z-index: 9997;
      display: grid;
      place-items: center;
      padding: 40px 20px 64px;
      background: rgba(2, 5, 11, 0.88);
      backdrop-filter: blur(6px);
      -webkit-backdrop-filter: blur(6px);
      opacity: 0;
      visibility: hidden;
      pointer-events: none;
      transition: opacity .18s ease, visibility 0s linear .18s;
      cursor: zoom-out;
    }
    .chat-image-viewer.show {
      opacity: 1;
      visibility: visible;
      pointer-events: auto;
      transition: opacity .18s ease, visibility 0s;
    }
    .chat-image-viewer-image {
      max-width: 100%;
      max-height: 100%;
      /* the file's own dimensions decide the rest; nothing is
         stretched past them on a large monitor */
      width: auto;
      height: auto;
      object-fit: contain;
      border-radius: 10px;
      box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
      cursor: default;
    }
    .chat-image-viewer-close {
      position: absolute;
      top: 14px;
      right: 16px;
      width: 36px;
      height: 36px;
      display: grid;
      place-items: center;
      border-radius: 50%;
      border: 1px solid var(--panel-edge);
      background: rgba(12, 18, 30, 0.9);
      color: rgba(207, 228, 245, 0.75);
      font-size: 19px;
      line-height: 1;
      cursor: pointer;
    }
    .chat-image-viewer-close:hover {
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }
    .chat-image-viewer-open {
      position: absolute;
      left: 50%;
      bottom: 20px;
      transform: translateX(-50%);
      padding: 7px 14px;
      border-radius: 999px;
      border: 1px solid var(--panel-edge);
      background: rgba(12, 18, 30, 0.9);
      color: rgba(207, 228, 245, 0.7);
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .1em;
      text-transform: uppercase;
      text-decoration: none;
      cursor: pointer;
    }
    .chat-image-viewer-open:hover {
      color: #dff4ff;
      border-color: var(--tr-border-cyan);
    }
    @media (max-width: 600px) {
      .chat-image-viewer { padding: 52px 12px 60px; }
    }
    /* The tip doubles as the place a send failure is reported, so it
       shows on its own rather than only on hover, and in a colour
       that is not the access warning's red. */
    .game-chat-access-tip.is-notice {
      opacity: 1;
      transform: translateY(0);
      background: rgba(28, 23, 14, 0.96);
      border-color: rgba(224, 184, 114, 0.4);
      color: #f2e3c4;
      white-space: normal;
      max-width: 260px;
    }
    /* ---- a sender's flag ----
       This had no rule at all: the markup asked for `scoreboard-flag`,
       which does not exist in this stylesheet, so the cloned <img>
       rendered at the PNG's own size. The row is a grid with a 22px
       first column, but a grid item's automatic minimum size is its
       min-content width -- the image's intrinsic width -- so the
       column was forced open and one message filled the panel with a
       flag. Fixed box, and the image told to fill it. */
    .game-chat-flag {
      grid-column: 1;
      grid-row: 1;
      /* level with the name rather than with the top of the box */
      margin-top: 3px;
      width: 22px;
      height: 15px;
      flex: none;
      /* the floor as well as the ceiling: min-width:0 is what stops
         the column being pushed open by the content again */
      min-width: 0;
      overflow: hidden;
      border-radius: 3px;
      background: rgba(255, 255, 255, 0.05);
      display: block;
    }
    .game-chat-flag img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }

    /* ===== REACTIONS =====
       Pills under the message, tray under those, and the button
       that opens the tray in the row's third column. Only the
       reactions somebody has actually used get a pill: a standing
       row of eight emoji under every message would be more
       furniture than message in a feed fifty rows deep. */
    .chat-reactions {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 4px;
      margin-top: 2px;
    }
    .chat-reaction {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      height: 21px;
      padding: 0 8px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid var(--tr-border-subtle);
      background: rgba(255, 255, 255, 0.05);
      color: var(--tr-text-secondary);
      font: inherit;
      font-size: 0.84em;
      line-height: 1;
      /* a span in live mode, a button everywhere else */
      cursor: default;
      transition:
        border-color var(--tr-transition-standard),
        background var(--tr-transition-standard),
        transform var(--tr-transition-fast);
    }
    button.chat-reaction { cursor: pointer; }
    button.chat-reaction:hover {
      border-color: var(--tr-border-cyan);
      background: rgba(255, 255, 255, 0.085);
    }
    button.chat-reaction:active { transform: scale(0.94); }
    /* Your own reaction, so a second click reads as taking it back
       rather than as adding another. */
    .chat-reaction.is-mine {
      border-color: rgba(95, 212, 255, 0.42);
      background: var(--tr-cyan-soft);
      color: var(--tr-cyan-bright);
    }
    .chat-reaction-glyph { font-size: 12px; line-height: 1; }
    .chat-reaction-count {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-variant-numeric: tabular-nums;
      font-weight: 700;
      font-size: 11px;
    }
    /* ---- the button that offers the eight ----
       A quiet square in the row's reserved third column, shown when
       the row is hovered or holds focus. It used to be a dashed
       circle standing open on all fifty rows at once, which read as
       fifty empty slots waiting to be filled rather than as one
       action available on the line you are looking at. */
    .chat-reaction-add {
      grid-column: 3;
      grid-row: 1;
      width: 25px;
      height: 25px;
      padding: 0;
      display: grid;
      place-items: center;
      border-radius: 8px;
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-glass-strong);
      color: var(--tr-text-muted);
      cursor: pointer;
      opacity: 0;
      /* invisible has to mean unclickable too */
      pointer-events: none;
      transition:
        opacity var(--tr-transition-fast),
        color var(--tr-transition-standard),
        border-color var(--tr-transition-standard);
    }
    .game-chat-message:hover .chat-reaction-add,
    .game-chat-message:focus-within .chat-reaction-add,
    /* and it stays while its own tray is open, however far the
       pointer has wandered */
    .game-chat-message.is-picking .chat-reaction-add {
      opacity: 1;
      pointer-events: auto;
    }
    /* Nothing hovers on a touch screen, so there it simply stays. */
    @media (hover: none) {
      .chat-reaction-add { opacity: 1; pointer-events: auto; }
    }
    .chat-reaction-add:hover,
    .game-chat-message.is-picking .chat-reaction-add {
      color: var(--tr-text-primary);
      border-color: var(--tr-border-cyan);
    }
    .chat-reaction-add svg {
      width: 15px;
      height: 15px;
      /* so a click always lands on the button, never on a path
         inside it */
      pointer-events: none;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.3;
      stroke-linecap: round;
    }
    .chat-reaction-add .chat-react-eye { fill: currentColor; stroke: none; }
    /* ---- replies ----
       Reply and react share the third column, side by side; the
       buttons inside keep the react button's look and hover rules. */
    .game-chat-actions {
      grid-column: 3;
      grid-row: 1;
      display: flex;
      gap: 4px;
    }
    .game-chat-message.is-picking .chat-reply-btn {
      color: var(--tr-text-muted);
      border-color: var(--tr-border-subtle);
    }
    .chat-reply-btn:hover {
      color: var(--tr-text-primary);
      border-color: var(--tr-border-cyan);
    }
    /* The quoted message, above the reply's own words: a thin edge
       and a muted line, so it reads as context and not as a second
       message. */
    .game-chat-quote {
      display: flex;
      align-items: baseline;
      gap: 6px;
      max-width: 100%;
      min-width: 0;
      margin: 1px 0 3px;
      padding: 3px 8px;
      border: 0;
      border-left: 2px solid rgba(150, 178, 210, 0.35);
      border-radius: 0 6px 6px 0;
      background: rgba(255, 255, 255, 0.03);
      color: var(--tr-text-muted);
      font: inherit;
      font-size: 11.5px;
      line-height: 1.4;
      text-align: left;
      cursor: pointer;
      transition: background var(--tr-transition-fast);
    }
    .game-chat-quote:hover { background: rgba(255, 255, 255, 0.06); }
    .game-chat-quote-name {
      flex: 0 0 auto;
      font-weight: 700;
      color: rgba(198, 216, 236, 0.8);
    }
    .game-chat-quote-text {
      min-width: 0;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    /* Somebody replied to you: the quote's edge takes the house cyan,
       softly, so it can be found in a busy feed. */
    .game-chat-quote.is-me { border-left-color: rgba(95, 212, 255, 0.6); }
    .game-chat-message.is-reply-to-me { background: rgba(95, 212, 255, 0.04); }
    /* Where a quote jumps to: a brief tint that fades. */
    @keyframes chat-flash {
      from { background: rgba(95, 212, 255, 0.14); }
      to { background: transparent; }
    }
    .game-chat-message.is-flash { animation: chat-flash 1.6s ease-out; }
    /* The bar above the input while a reply is being written. */
    .game-chat-reply {
      display: flex;
      align-items: center;
      gap: 8px;
      margin: 0 10px 6px;
      padding: 6px 8px 6px 10px;
      border-left: 2px solid rgba(95, 212, 255, 0.5);
      border-radius: 0 8px 8px 0;
      background: rgba(255, 255, 255, 0.035);
      font-size: 11.5px;
      line-height: 1.4;
    }
    .game-chat-reply[hidden] { display: none; }
    .game-chat-reply-meta {
      display: flex;
      flex-direction: column;
      flex: 1 1 auto;
      min-width: 0;
    }
    .game-chat-reply-label { color: var(--tr-text-muted); }
    .game-chat-reply-label strong { color: var(--tr-text-primary); font-weight: 700; }
    .game-chat-reply-text {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      color: var(--tr-text-muted);
    }
    @media (prefers-reduced-motion: reduce) {
      .game-chat-message.is-flash { animation: none; }
    }
    /* ---- the eight, on demand ----

       A tray on its own line under the pills, in normal flow. It
       was absolutely positioned above them at first, which is the
       usual answer for a popover and the wrong one here: sitting
       directly over the message being reacted to, it hid the thing
       you were reacting to at the moment you were choosing how.
       Flipping it downwards instead would have put it outside the
       scroller on the last message, which is the message people
       react to most.

       In flow it covers nothing and can never be clipped. It costs
       a row of height while open, below the line being acted on, so
       nothing above it moves. */
    .chat-reaction-picker {
      display: none;
      /* Four by two, fixed, rather than a row of eight that wraps.
         A row of eight is 234px and the body column is 206px on a
         phone and 213px in the live rail, so it wrapped -- seven
         and a stray one. This is 122px and is the same shape in
         every panel at every width. */
      grid-template-columns: repeat(4, 26px);
      width: max-content;
      max-width: 100%;
      gap: 2px;
      margin-top: 4px;
      padding: 5px;
      border-radius: 12px;
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-glass-strong);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
    }
    .chat-reaction-picker.is-open { display: grid; }
    .chat-reaction-choice {
      width: 26px;
      height: 26px;
      padding: 0;
      display: grid;
      place-items: center;
      border: 0;
      border-radius: 8px;
      background: transparent;
      font-size: 15px;
      line-height: 1;
      cursor: pointer;
      transition: background var(--tr-transition-fast), transform var(--tr-transition-fast);
    }
    .chat-reaction-choice:hover {
      background: rgba(255, 255, 255, 0.09);
      transform: scale(1.14);
    }
    @media (prefers-reduced-motion: reduce) {
      .chat-reaction-choice:hover { transform: none; }
      .chat-reaction-add,
      .game-chat-message { transition-duration: 0s; }
    }
    .winners-solscan-btn {
      position: absolute;
      right: 16px;
      bottom: 16px;
      border: 0;
      border-radius: 10px;
      padding: 7px 11px;
      background: linear-gradient(180deg, #f4f7fb 0%, #cfd8e3 100%);
      color: #111827;
      font-weight: 800;
      cursor: pointer;
      text-transform: uppercase;
      letter-spacing: .06em;
      font-size: 11px;
      box-shadow: 0 0 0 1px rgba(255,255,255,0.12) inset;
    }
    .winners-solscan-btn:hover { filter: brightness(1.05); }
    .game-chat-input {
      flex: 1;
      min-width: 0;
      border: 1px solid rgba(55, 168, 255, 0.18);
      background: rgba(255,255,255,.04);
      color: #f7fbff;
      border-radius: 12px;
      padding: 12px 14px;
      font-size: 14px;
      outline: none;
    }
    .game-chat-input::placeholder { color: rgba(184, 179, 168, 0.75); }
    .game-chat-input:disabled {
      opacity: 0.55;
      cursor: not-allowed;
    }

    .game-chat-access-tip {
      position: absolute;
      right: 0;
      bottom: calc(100% + 8px);
      padding: 8px 10px;
      border-radius: 10px;
      background: rgba(182, 18, 18, 0.96);
      border: 1px solid rgba(255,255,255,0.18);
      color: #f7fbff;
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .03em;
      box-shadow: 0 10px 24px rgba(0,0,0,0.35);
      white-space: nowrap;
      opacity: 0;
      pointer-events: none;
      transform: translateY(4px);
      transition: opacity .08s linear, transform .08s linear;
      z-index: 5;
    }
    .game-chat-input-row.access-locked {
      position: relative;
    }
    .game-chat-input-row.access-locked:hover .game-chat-access-tip {
      opacity: 1;
      transform: translateY(0);
    }
    .game-chat-send {
      border: 0;
      border-radius: 12px;
      padding: 12px 14px;
      background: linear-gradient(180deg, #7d3cff 0%, #5d1fd8 50%, #4312a8 100%);
      color: #f7f3ff;
      font-weight: 900;
      cursor: pointer;
      white-space: nowrap;
    }
    /* ===== REGISTER / LOGIN RESULT MESSAGE =====
       Shares the .security-alert-card markup but not its red alarm skin: this one is a
       routine confirmation, so it keeps the standard panel surface and carries its state
       in a single accent colour (the icon ring and a hairline) instead of a tinted card. */
    #registerLoginMessagePanel .rl-message-card {
      --rl-accent: rgba(150, 185, 224, 0.5);
      --rl-accent-soft: rgba(150, 185, 224, 0.1);
      position: relative;
      width: min(92vw, 404px);
      max-width: 404px;
      max-height: none;
      padding: 30px 26px 28px;
      border-radius: 22px;
      gap: 14px;
      justify-items: center;
      grid-template-rows: auto auto auto;
      border-color: var(--tr-border-subtle);
      background: var(--tr-panel-gradient);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
    }
    #registerLoginMessagePanel .rl-message-card::before {
      left: 22%;
      right: 22%;
      background: linear-gradient(90deg, transparent, var(--rl-accent), transparent);
      opacity: 0.55;
    }
    .rl-message-icon {
      width: 54px;
      height: 54px;
      border-radius: 50%;
      display: grid;
      place-items: center;
      color: var(--rl-accent);
      border: 1px solid var(--rl-accent-soft);
      background:
        radial-gradient(120% 120% at 50% 0%, var(--rl-accent-soft), transparent 72%),
        linear-gradient(180deg, rgba(20, 27, 42, 0.96), rgba(10, 14, 24, 0.98));
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
    }
    .rl-message-icon .rl-glyph {
      width: 25px;
      height: 25px;
      display: none;
    }
    #registerLoginMessagePanel.success .rl-glyph-ok,
    #registerLoginMessagePanel.error .rl-glyph-warn { display: block; }
    #registerLoginMessagePanel .rl-message-card .winners-title {
      margin: 0;
      padding: 0;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: .22em;
      color: rgba(178, 202, 226, 0.52);
    }
    #registerLoginMessagePanel .rl-message-card .security-alert-message {
      padding: 0;
      border: 0;
      background: none;
      color: var(--tr-text-primary);
    }
    #registerLoginMessagePanel .rl-message-card .security-alert-main {
      font-size: 17px;
      font-weight: 600;
      line-height: 1.45;
      letter-spacing: .005em;
      color: rgba(233, 242, 251, 0.92);
    }
    /* Quiet ghost close button, matching the user profile panel rather than the
       solid red circle the leaderboard-style panels use. */
    #registerLoginMessagePanel .rl-message-card .winner-close {
      top: 14px;
      right: 14px;
      left: auto;
      transform: none;
      width: 30px;
      height: 30px;
      font-size: 16px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.98), rgba(9, 13, 22, 0.99));
      color: rgba(207, 228, 245, 0.62);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    #registerLoginMessagePanel .rl-message-card .winner-close:hover {
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }
    #registerLoginMessagePanel.success .rl-message-card {
      --rl-accent: rgba(105, 214, 154, 0.9);
      --rl-accent-soft: rgba(105, 214, 154, 0.16);
    }
    #registerLoginMessagePanel.error .rl-message-card {
      --rl-accent: rgba(255, 128, 145, 0.92);
      --rl-accent-soft: rgba(255, 111, 130, 0.16);
    }
      .daily-spin-card {
        z-index: 999;
        width: min(92vw, 520px);
        max-height: min(92vh, 650px);
        padding: 32px;
        gap: 20px;
        grid-template-rows: auto 1fr auto;
        border-color: var(--tr-border-subtle);
        background: var(--tr-panel-gradient);
        box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
        overflow-y: auto;
      }
      .daily-spin-content {
        display: grid;
        gap: 16px;
        justify-items: center;
        text-align: center;
        overflow-y: auto;
        max-height: calc(92vh - 180px);
      }
     .daily-spin-title {
       font-size: 28px;
       font-weight: 900;
       letter-spacing: .12em;
       text-transform: uppercase;
        color: var(--tr-cyan-bright);
        text-shadow: 0 0 10px rgba(95, 212, 255, 0.4);
       margin: 0;
     }
     .daily-spin-message {
       font-size: 16px;
       font-weight: 600;
       line-height: 1.6;
        color: var(--tr-text-primary);
       margin: 0;
     }
     .daily-spin-message p {
       margin: 0;
     }
     .daily-spin-message strong {
        color: var(--tr-cyan-bright);
        font-weight: 900;
        text-shadow: 0 0 8px rgba(95, 212, 255, 0.3);
     }
     .daily-spin-btn {
        border: 1px solid var(--tr-border-subtle);
        background: var(--tr-control-gradient);
        color: var(--tr-text-primary);
       border-radius: 18px;
       padding: 14px 32px;
       font-size: 12px;
       font-weight: 900;
       letter-spacing: .12em;
       text-transform: uppercase;
       cursor: pointer;
        box-shadow:
          var(--tr-inset-highlight),
          0 12px 24px rgba(0, 0, 0, 0.28),
          0 0 18px rgba(95, 212, 255, 0.08);
        transition: background var(--tr-transition-standard), border-color var(--tr-transition-standard), box-shadow var(--tr-transition-standard), filter var(--tr-transition-standard), translate var(--tr-transition-fast);
      }
      .daily-spin-btn.primary {
        background: var(--tr-primary-gradient);
        border-color: rgba(191, 167, 255, 0.48);
      }
      .daily-spin-btn:hover {
        background: var(--tr-primary-hover-gradient);
        border-color: rgba(211, 194, 255, 0.7);
        filter: brightness(1.07);
        box-shadow:
          inset 0 1px rgba(255,255,255,0.2),
          0 12px 28px rgba(71, 39, 164, 0.4),
          0 0 28px rgba(152, 116, 255, 0.28);
      }
       .daily-spin-btn:active {
        translate: 0 0;
      }
      .daily-spin-wheel-container {
        position: relative;
        width: 300px;
        height: 300px;
        margin: 20px auto;
      }
      .daily-spin-wheel-pointer {
        position: absolute;
        top: -12px;
        left: 50%;
        transform: translateX(-50%);
        width: 0;
        height: 0;
        border-left: 10px solid transparent;
        border-right: 10px solid transparent;
        border-top: 12px solid var(--tr-cyan);
        z-index: 10;
        filter: drop-shadow(0 0 4px rgba(95, 212, 255, 0.6));
      }
      .daily-spin-wheel-image {
        width: 100%;
        height: 100%;
        object-fit: contain;
        filter: drop-shadow(0 0 20px rgba(37, 183, 255, 0.4)) drop-shadow(0 0 10px rgba(95, 212, 255, 0.3));
      }
      @media (max-width: 1300px), (max-height: 900px) {
      .ca-strip { transform: translateX(-50%) scale(0.8); bottom: 14px; }
      .game-chat-launcher { transform: scale(0.8); transform-origin: bottom right; --game-chat-gutter: 12px; bottom: 12px; }
      .game-chat-panel { --game-chat-gutter: 12px; bottom: 64px; }
      
    }
    @media (max-width: 1200px), (max-height: 700px) {
      .ca-strip { transform: translateX(-50%) scale(0.6); bottom: 10px; }
      .game-chat-launcher { transform: scale(0.6); transform-origin: bottom right; --game-chat-gutter: 10px; bottom: 10px; }
      .game-chat-panel { --game-chat-gutter: 10px; bottom: 56px; }

      
    }
    @media (orientation: portrait) {
      /* the two links used to be restyled from scratch here, with a different
         set of negative margins again; they now just shrink */
      .ca-social-link {
        width: 26px;
        height: 26px;
        border-radius: 8px;
      }
      .ca-medium-link svg { width: 15px; height: 15px; }
      .ca-x-link svg { width: 12px; height: 12px; }

    .winners-solscan-btn {
      position: inherit;
      transform: translate(0.5);
      margin-left: 77%;
      bottom: 3px;
      margin-top: 5px;
      width: 95px;
      border: 0;
      border-radius: 10px;
      padding: 4px 7px;
      background: linear-gradient(180deg, #f4f7fb 0%, #cfd8e3 100%);
      color: #111827;
      font-weight: 800;
      cursor: pointer;
      text-transform: uppercase;
      letter-spacing: .06em;
      font-size: 8px;
      box-shadow: 0 0 0 1px rgba(255,255,255,0.12) inset;
    }

      .game-chat-launcher {
        right: auto;
        left: 10px;
        bottom: 120px;
        transform-origin: bottom left;
      }
       .game-chat-panel {
         right: auto;
         left: 10px;
         bottom: 56px;
       }
       .daily-spin-card {
         width: min(95vw, 100%);
         max-height: min(85vh, 550px);
         padding: 24px;
         gap: 16px;
       }
       .daily-spin-wheel-container {
         width: 220px;
         height: 220px;
         margin: 12px auto;
       }
       .daily-spin-title {
         font-size: 22px;
       }
       .daily-spin-message {
         font-size: 14px;
       }
     }
    .reward-pool-summary {
      display: grid;
      gap: 8px;
      padding: 12px 14px;
      border-radius: 16px;
      background: linear-gradient(180deg, rgba(11, 19, 33, 0.96), rgba(9, 15, 26, 0.98));
      border: 1px solid rgba(55, 168, 255, 0.14);
      box-shadow: 0 0 0 1px rgba(125, 51, 255, 0.08) inset;
    }
    .reward-pool-box {
      display: grid;
      grid-template-columns: auto 1fr;
      align-items: center;
      gap: 12px;
      padding: 12px 14px;
      border-radius: 14px;
      background: rgba(124, 61, 255, 0.08);
      border: 1px solid rgba(125, 51, 255, 0.18);
    }
    .reward-pool-icon {
      width: 30px;
      height: 30px;
      border-radius: 50%;
      display: grid;
      place-items: center;
      background: rgba(255,255,255,0.06);
      box-shadow: 0 0 0 1px rgba(255,255,255,0.08) inset;
      overflow: hidden;
    }
    .reward-pool-icon img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }
    .reward-pool-copy {
      display: grid;
      gap: 4px;
      min-width: 0;
    }
    .reward-pool-label {
      font-size: 11px;
      font-weight: 900;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: #dff4ff;
    }
    .reward-pool-value {
      font-size: 22px;
      font-weight: 900;
      color: #ffffff;
      line-height: 1;
    }
    .reward-pool-note {
      font-size: 11px;
      color: var(--muted);
      line-height: 1.4;
    }
    @media (orientation: portrait) {
      .reward-distribution-empty{
        font-size: 8px;
      }

      #rewardDistributionClose{
        left: 100%;
        transform: translate(-115%, -40%);
        font-size: 12px;
        width: 25px;
        height: 25px;

      }








      .reward-pool-box {
        grid-template-columns: 1fr;
        justify-items: center;
        text-align: center;
      }

      .reward-pool-copy {
        justify-items: center;
      }

      .reward-pool-icon {
        width: 34px;
        height: 34px;
      }

      .reward-pool-value {
        font-size: 20px;
      }

      .reward-pool-summary {
        gap: 6px;
        padding: 8px 10px;
      }

      .reward-pool-box {
        padding: 8px 10px;
        border-radius: 12px;
        gap: 4px;
      }

      .reward-pool-label {
        font-size: 8px;
        letter-spacing: .10em;
      }

      .reward-pool-note {
        font-size: 8px;
      }

      
      .reward-panel .top-total{
        width: 100px;
        position: fixed;
      }
      
      .reward-panel .reward-value{
        font-size: 8px;
      }

      .reward-panel .reward-title{
        font-size: 5px;
      }

      .account-chip {
        transform: scale(0.6);
        transform-origin: top left;
        top: 10px !important;
        left: 10px;
      }
      /* the flag drops to 30px in portrait, so the chip is 52px tall here, not
         64: 10 + (52 - 2) * 0.6. The nub matches the chip's scale so the two
         still read as one control. */
      .holdings-chip {
        transform: scale(0.6);
        transform-origin: top left;
        top: 40px !important;
        left: 10px;
      }
      /* 40 + (44 - 2) * 0.6 */
      .earnings-chip {
        transform: scale(0.6);
        transform-origin: top left;
        top: 65px !important;
        left: 10px;
      }
      .music-flyout {
        position: fixed;
        transform: scale(0.8);
        transform-origin: bottom right;
        top: 87%;
        left: auto;
        right: 12px;
        z-index: 220;
      }
      .music-flyout.left-anchor .music-popover {
        left: auto;
        right: 0;
      }
      .music-flyout.left-anchor .music-popover::before {
        left: auto;
        right: 14px;
      }
      html, body {
        width: 100%;
        height: 100%;
        min-height: 100vh;
      }
      html, body { min-height: 100dvh; }
      body {
        display: block;
        overflow-x: hidden;
        overflow-y: auto;
        min-height: 100dvh;
        background:
          radial-gradient(circle at top, rgba(88, 55, 191, 0.22) 0%, transparent 36%),
          radial-gradient(circle at 50% 40%, rgba(37, 183, 255, 0.12) 0%, transparent 50%),
          linear-gradient(180deg, #0a101b 0%, #070b14 100%);
        background-attachment: fixed;
      }
      .app {
        width: 100%;
        min-height: 100vh;
        min-height: 100dvh;
        display: flex;
        justify-content: center;
        align-items: flex-start;
        overflow-x: auto;
        padding: 72px 6px 18px;
        box-sizing: border-box;
      }
      .game-layout {
        display: grid;
        grid-template-columns: 112px max-content 112px;
        gap: 12px;
        width: max-content;
        margin: 0 auto;
      }
      .board-side {
        gap: 5px;
        --side-board-offset: 78px;
        padding-top: var(--side-board-offset);
      }
      .hold-column,
      .next-column,
      .piece-stack {
        --stack-gap: 5px;
        width: 72px;
        gap: var(--stack-gap);
        margin: 0;
        padding: 0;
        transform: none;
      }
      .main-column,
      .main-column.f2main-column {
        width: max-content;
        gap: 10px;
        justify-items: center;
      }
      .piece-card {
        width: 72px;
        padding: 4px;
        box-sizing: border-box;
        border-radius: 6px;
      }
      .piece-card .mini {
        display: block;
        width: 62px;
        height: 62px;
      }
      .piece-card .label { font-size: 8px; }
      .piece-heading {
        gap: 4px;
        margin-bottom: 3px;
        padding-bottom: 0;
        font-size: 10px;
        letter-spacing: 0.16em;
      }
      .piece-heading::before,
      .piece-heading::after {
        height: 1px;
      }
      .hold-row { gap: 2px; }
      .play-shell { width: 100%; justify-self: center; display: grid; justify-items: center; }
      .play-shell canvas { max-width: 100%; height: auto; }
      .board-wrap {
        width: min(70vw, 332px);
        padding: 7px;
        border-radius: 10px;
        --frame-pad: 7px;
      }
      #board { width: 100%; height: auto; }
      .ko-flash { inset: 7px; border-radius: 4px; }
      .ko-flash-label { font-size: clamp(26px, 11vw, 44px); }
      .board-idle-title { font-size: 14px; }
      .board-idle-subtitle { font-size: 9px; }
      .controls-visibility-toggle {
        display: none;
      }
      .top-bar { justify-content: center; top: 120px; position:fixed; z-index: 40; gap: 8px;}
      .main-column > div[style*="display:flex"] {
        display: flex !important;
        flex-direction: column;
        gap: 12px;
        width: 100%;
      }
      /* no room beside the board in portrait: drop the big "how to play" tile
         but keep the scoring guide reachable, pinned above the music button */
      .controls-flyout {
        display: grid;
        position: fixed;
        top: 78%;
        left: auto;
        right: 12px;
        bottom: auto;
        gap: 0;
        z-index: 220;
      }
      .controls-flyout .controls-icon { display: none; }
      .controls-flyout .score-help {
        width: 30px;
        height: 30px;
        border-radius: 9px;
      }
      /* at 30px a glyph and a corner badge together are illegible, so the
         question mark becomes the whole button */
      .controls-flyout .score-help svg { display: none; }
      .controls-flyout .score-help .help-mark {
        position: static;
        width: auto;
        height: auto;
        border: 0;
        background: none;
        box-shadow: none;
        font-size: 15px;
      }
      .touch-controls {
        display: grid;
        grid-template-columns: repeat(3, auto);
        align-items: center;
        transform-origin: bottom center;
      }
      .touch-controls.collapsed { display: none !important; }
      .touch-controls .touch-row { display: contents; }
      .controls-visibility-toggle {
        display: block;
        margin: 8px auto 0;
        padding: 0;
        border: 1px solid rgba(29, 31, 32, 0);
        border-radius: 14px;
        background: transparent;
        overflow: hidden;
      }
      .controls-visibility-toggle img {
        display: block;
        width: 100%;
        height: auto;
      }
      .scoreboard-panel {
        position: fixed;
        top: 10px;
        right: 10px;
        width: 140px;
        margin-top: 0;
      }
      .scoreboard-panel.collapsed {
        width: auto;
        height: auto;
        padding: 0;
        background: transparent;
        border: 0;
        box-shadow: none;
        top: 10px;
        right: 10px;
        left: auto;
        bottom: auto;
      }
      .scoreboard-panel.collapsed .scoreboard-panel-inner { gap: 0; }
      .scoreboard-panel.collapsed .scoreboard-subtitle,
      .scoreboard-panel.collapsed .scoreboard-list,
      .scoreboard-panel.collapsed .scoreboard-footer { display: none; }
      .scoreboard-panel.collapsed .scoreboard-mode-toggle { display: none; }
      .scoreboard-panel.collapsed .scoreboard-toggle {
        width: 100%;
        min-height: 38px;
        padding: 8px 12px;
        font-size: 12px;
        border-radius: 50px;
      }
      .scoreboard-panel:not(.collapsed) {
        inset: 0;
        width: 100vw;
        height: 100vh;
        max-height: 100vh;
        border-radius: 0;
        padding: 18px 14px 20px;
        z-index: 80;
      }
      .scoreboard-panel-inner { align-items: center; }
      .scoreboard-panel .scoreboard-toggle {
        width: 100%;
        display: block;
        display: grid;
        place-items: center;
        min-height: 64px;
        padding: 16px 18px;
        font-size: 20px;
        letter-spacing: .08em;
        line-height: 1;
      }
      .scoreboard-panel.collapsed .scoreboard-toggle {
        min-height: 40px;
        font-size: 15px;
      }
      .scoreboard-panel:not(.collapsed) .scoreboard-toggle {
        min-height: 50px;
        font-size: 24px;
        line-height: 1;
      }
      .score-chip-main .value,
      .score-chip-main .value {font-size: 20px;}
    .score-chip-rank {font-size: 25px;}
      .scoreboard-panel:not(.collapsed) .scoreboard-subtitle { font-size: 14px; }
      .scoreboard-panel:not(.collapsed) .scoreboard-list { gap: 14px; }
      .scoreboard-panel:not(.collapsed) .scoreboard-row {
        padding: 14px 16px;
        grid-template-columns: 44px 34px 1fr auto;
        border-radius: 18px;
      }
      .scoreboard-panel:not(.collapsed) .scoreboard-name { font-size: 15px; }
      .scoreboard-panel:not(.collapsed) .scoreboard-address { font-size: 11px; }
      .scoreboard-panel:not(.collapsed) .scoreboard-score-player { font-size: 15px; min-width: 45px; }
      .scoreboard-panel:not(.collapsed) .scoreboard-score-earner { font-size: 10px; min-width: 45px; }
      .scoreboard-panel:not(.collapsed) .scoreboard-rank { width: 34px; height: 34px; font-size: 14px; }
      .scoreboard-panel:not(.collapsed) .scoreboard-find {
        width: min(100%, 150px);
        min-height: 30px;
        padding: 5px 20px;
        font-size: 10px;
        border-radius: 25px;
      }
      .scoreboard-panel:not(.collapsed) .scoreboard-footer { padding-top: 10px; }
      
      .bottom-left-dock {
        left: 12px;
        bottom: calc(18px + env(safe-area-inset-bottom, 0px));
        transform: none;
      }
      .ca-strip { left: 50%; transform: translateX(-50%) scale(1); bottom: calc(18px + env(safe-area-inset-bottom, 0px)); }
      .game-timer-banner { top: 10px; transform: translateX(-50%) scale(0.4); }
      .winners-card {
        width: 95vw;
        max-width: none;
        max-height: 90vh;
        padding: 12px;
        gap: 8px;
        border-radius: 18px;
      }
      .winners-title {
        font-size: 14px;
        letter-spacing: .10em;
        padding: 8px 10px;
      }
      .winners-list {
        gap: 8px;
        padding-right: 2px;
      }
      .winner-row {
        padding: 8px 10px;
        gap: 8px;
        border-radius: 14px;
      }
      .winner-name { font-size: 13px; }
      .winner-address { font-size: 9px; }
      .winner-score { font-size: 14px; min-width: 42px; }
      .winner-rank { width: 28px; height: 28px; font-size: 11px; }
      .winners-summary {
        width: 100%;
        gap: 8px;
        padding: 10px 12px;
        border-radius: 14px;
      }
      .winners-summary .summary-box { padding: 8px 10px; border-radius: 12px; }
      .winners-summary .summary-label { font-size: 9px; }
      .winners-summary .summary-value { font-size: 14px; }
    }
    @media (orientation: portrait) {
      .winner-close{
        height: 22px;
        width: 22px;
        left: 100%;
        transform: translate(-110%, -40%);
        font-size: 12px;
      }

      .winners-title {
        font-size: 18px;
      }

      .winner-row {
        grid-template-columns: 20px 25px minmax(0, 1fr) auto 58px;
        gap: 6px;
      }

      .winner-rank {
        width: 20px;
        height: 20px;
        font-size: 8px;
      }

      .winner-name {
        font-size: 12px;
      }

      .winner-address {
        font-size: 8px;
      }

      .winner-reward {
        font-size: 8px;
      }

      .winner-reward img {
        width: 12px;
        height: 12px;
      }

      .winner-score {
        width: 58px;
        padding: 7px 6px;
        font-size: 13px;
      }

      /* these were 4px to 6px, which is past the point of being readable */
      .ca-strip-value{
        font-size: 9px;
        padding: 4px 7px;
      }

      .reward-distribution-btn{
        min-height: 30px;
        padding: 0 10px;
        width: auto;
        /* never wider than the old fixed 135px, so it stays clear of the centred CA strip */
        max-width: min(135px, 44vw);
        border-radius: 12px;
        font-size: 8px;
        letter-spacing: .06em;
      }
      /* the badge does not fit next to the label at portrait widths */
      .reward-distribution-btn-icon{
        display: none;
      }

      /* was a hard 200x30 box; letting it size to its contents keeps the
         address field from being crushed at narrow widths */
      .ca-strip-inner{
        width: min(96vw, 330px);
        height: auto;
        padding: 6px 8px;
        gap: 6px;
        border-radius: 12px;
      }

      .ca-strip-label{
        font-size: 7px;
        letter-spacing: .12em;
        padding-left: 8px;
        margin-left: 1px;
      }
      .ca-strip-label::before { height: 16px; }

      .ca-copy-btn{
        width: auto;
        height: auto;
        min-width: 62px;
        padding: 5px 7px;
        font-size: 7px;
        letter-spacing: .08em;
      }

      .reward-distribution-stats{
        font-size: 5px;
        width: 120px;
      }

      /* No max-height and no scroller of its own. The card body is
         the one scroll region now, and a nested one here trapped the
         list at a few rows with its own bar inside the page's. */
      .reward-distribution-list {
        display: grid;
        gap: 6px;
        padding-right: 2px;
      }
      /* The panel's blocks at narrow widths: the pool and its
         countdown stack, the formula becomes a column, and the three
         creator splits go one per row rather than three slivers. */
      .rd-section { padding: 12px; gap: 9px; }
      .rd-pool { grid-template-columns: minmax(0, 1fr); gap: 12px; }
      .rd-pool-value { font-size: 23px; }
      .rd-sync { justify-items: start; text-align: left; }
      .rd-sync-note { max-width: none; }
      .rd-formula { flex-direction: column; align-items: stretch; }
      .rd-term { flex: 0 0 auto; }
      .rd-op { align-self: center; }
      .rd-bands-head,
      .rd-band { grid-template-columns: minmax(0, 1fr) 58px 92px; gap: 8px; }
      .rd-band { font-size: 11px; padding: 8px 10px; }
      .rd-splits { grid-template-columns: minmax(0, 1fr); }
      .rd-split {
        grid-template-columns: 54px minmax(0, 1fr);
        align-items: center;
        text-align: left;
        gap: 12px;
      }
      .reward-distribution-row {
        grid-template-columns: 22px minmax(0, 1fr) 90px 72px 84px;
        gap: 6px;
        padding: 8px 10px;
        border-radius: 14px;
        min-height: 44px;
      }
      .reward-distribution-player .scoreboard-name { font-size: 5px; }
      .reward-distribution-player .scoreboard-address { font-size: 5px; }
      .reward-distribution-time { font-size: 9px; }
      .reward-distribution-flag { width: 18px; height: 18px; }
      .reward-distribution-row .addr,
      .reward-distribution-row .amt { font-size: 10px; }
      .reward-status-btn {
        width: 100%;
        min-width: 0;
        min-height: 34px;
        padding: 8px 10px;
        font-size: 12px;
        border-radius: 10px;
        justify-self: stretch;
      }
      .reward-distribution-row .reward-distribution-player { min-width: 0; }
      .reward-distribution-row .reward-distribution-time { text-align: left; }
      .reward-distribution-row .reward-distribution-sol { justify-self: center; }
      .reward-distribution-list::-webkit-scrollbar { width: 8px; }
      .reward-distribution-list::-webkit-scrollbar-track { background: rgba(7, 12, 22, 0.7); border-radius: 999px; }
      .reward-distribution-list::-webkit-scrollbar-thumb { background: linear-gradient(180deg, rgba(37, 183, 255, 0.55), rgba(125, 51, 255, 0.65)); border-radius: 999px; border: 2px solid rgba(7, 12, 22, 0.7); }
      .reward-distribution-list::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, rgba(37, 183, 255, 0.8), rgba(125, 51, 255, 0.9)); }
    }
    .reward-distribution-row {
      display: grid;
      grid-template-columns: 26px minmax(0, 1fr) 162px 120px 104px;
      align-items: center;
      gap: 10px;
      padding: 10px 12px;
      border-radius: 16px;
      min-height: 54px;
      background: rgba(11, 19, 33, 0.92);
      border: 1px solid rgba(55, 168, 255, 0.14);
    }
    .reward-distribution-index { display: none; }
    .reward-distribution-flag {
      width: 22px;
      height: 22px;
      border-radius: 50%;
      overflow: hidden;
      background: rgba(255,255,255,0.06);
      box-shadow: 0 0 0 1px rgba(255,255,255,0.1) inset;
      display: grid;
      place-items: center;
      flex: 0 0 auto;
    }
    .reward-distribution-flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .reward-distribution-row .addr,
    .reward-distribution-row .amt,
    .reward-distribution-time {
      min-width: 0;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      font-size: 8px;
      font-weight: 700;
    }
    .reward-distribution-player { min-width: 0; }
    .reward-distribution-player .scoreboard-name { font-size: 10px; }
    .reward-distribution-player .scoreboard-address { font-size: 8px; }
    .reward-distribution-row .addr {
      color: #eaf4ff;
    }
    .reward-distribution-row .amt {
      color: #dff4ff;
    }
    .reward-distribution-time {
      font-size: 8px;
      font-weight: 700;
      color: var(--muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .reward-distribution-sol {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 11px;
      font-weight: 700;
      color: #dff4ff;
      white-space: nowrap;
    }
    .reward-distribution-sol img {
      width: 15px;
      height: 15px;
      display: block;
    }
    .reward-status-btn {
      justify-self: end;
      border: 0;
      border-radius: 10px;
      padding: 8px 12px;
      font-weight: 900;
      cursor: pointer;
      background: linear-gradient(180deg, #7d3cff 0%, #5d1fd8 50%, #4312a8 100%);
      color: #f7f3ff;
      min-width: 100px;
    }
    .reward-status-btn.pending { cursor: default; }
    .reward-status-btn.sent { background: linear-gradient(180deg, #69d69a, #2e9f78); }
    /* The payout queue's other states: muted, so only a sent entry --
       the one that is a link -- reads as a button. Pending keeps the
       base purple. */
    .reward-status-btn.is-processing { background: linear-gradient(180deg, #4f7fc4, #34598f); color: #eef4ff; }
    .reward-status-btn.is-underReview { background: linear-gradient(180deg, #b4924a, #86692f); color: #fff8ea; }
    .reward-status-btn.is-failed { background: linear-gradient(180deg, #b37a4a, #85552f); color: #fff3ea; }
    .reward-status-btn.is-rejected { background: linear-gradient(180deg, #b25a66, #843e49); color: #fff0f2; }
    .reward-status-btn.is-refunded { background: linear-gradient(180deg, #5d6878, #434c5a); color: #eef1f5; }
    .welcome-panel {
      position: fixed;
      inset: 0;
      z-index: 120;
      display: grid;
      place-items: center;
      background: rgba(0, 0, 0, 0.72);
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
    }
    .welcome-panel.show {
      opacity: 1;
      pointer-events: auto;
    }
    /* The card is the house panel now -- the same gradient, hairline
       and shadow every other panel uses, inherited from the shared
       rule near the top of this file. The green-tinted edge and the
       glow it used to carry belonged to the image it was framing. */
    .welcome-card {
      width: min(94vw, 960px);
      max-height: min(90vh, 920px);
      padding: 22px 22px 20px;
      border-radius: 24px;
      display: grid;
      grid-template-rows: auto minmax(0, 1fr);
      gap: 16px;
    }
    /* The house ghost circle, not the solid red one. */
    .welcome-close {
      position: absolute;
      top: 14px;
      right: 14px;
      z-index: 2;
      width: 34px;
      height: 34px;
      padding: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 18px;
      line-height: 1;
      cursor: pointer;
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--panel-edge);
      color: rgba(207, 228, 245, 0.7);
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .welcome-close::before { content: none; }
    .welcome-close:hover {
      transform: none;
      filter: none;
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }

    /* ---- the information itself ---- */
    .wi-head { text-align: center; display: grid; gap: 5px; }
    /* Readable first, decorated second. `color: transparent` with a
       clipped background is invisible type on any engine that does
       not do background-clip -- which is a heading nobody can read
       rather than a heading without a gradient. The plain colour is
       the rule; the gradient is applied only where it will paint. */
    .wi-title {
      margin: 0;
      font-size: clamp(20px, 3.4vw, 30px);
      font-weight: 900;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: #dff4ff;
    }
    @supports ((-webkit-background-clip: text) or (background-clip: text)) {
      .wi-title {
        /* the cyan-to-violet the rest of the chrome uses, on the one
           piece of type big enough to carry it */
        background: var(--tr-cyan-violet-gradient);
        -webkit-background-clip: text;
        background-clip: text;
        color: transparent;
      }
    }
    .wi-tagline {
      margin: 0;
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .2em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    /* Laid out by name rather than by auto-fit.
     *
     * auto-fit was leaving the panel ragged: five cards of unequal
     * height flowing into however many columns happened to fit, with
     * a gap wherever the last row ran short. These areas tile
     * exactly -- twelve columns, five rows, nothing left over -- and
     * Controls takes a full-height column of its own because it is
     * the tallest card and stretching its neighbours to match it is
     * what produced most of the empty space.
     *
     *   play  play  play  play  play  play  play  play  keys keys keys keys
     *   score score score score score score score score keys keys keys keys
     *   rew   rew   rew   rew   rew   rew   rew   rew   rew  rew  rew  rew
     *   rank  rank  rank  rank  rank  rank  ener ener ener ener ener ener
     *   band  band  band  band  band  band  band band band band band band
     */
    .wi-body {
      display: grid;
      grid-template-columns: repeat(12, minmax(0, 1fr));
      grid-template-areas:
        "play play play play play play play play keys keys keys keys"
        "score score score score score score score score keys keys keys keys"
        "rew rew rew rew rew rew rew rew rew rew rew rew"
        "rank rank rank rank rank rank ener ener ener ener ener ener"
        "band band band band band band band band band band band band";
      gap: 12px;
      align-content: start;
      overflow-y: auto;
      padding-right: 4px;
    }
    .wi-area-play { grid-area: play; }
    .wi-area-score { grid-area: score; }
    .wi-area-keys { grid-area: keys; }
    .wi-area-rewards { grid-area: rew; }
    .wi-area-rank { grid-area: rank; }
    .wi-area-energy { grid-area: ener; }
    .wi-area-bands { grid-area: band; }
    /* The patch notes used to lead this panel. They live in Change Logs
       alone now: this is the game's rules, read by somebody new, and a
       release history is news only to the people who already play. */

    /* The formula, given its own line because it is the answer to
       the question everybody opens this panel with. */
    .wi-formula {
      padding: 10px 12px;
      border-radius: 11px;
      border: 1px solid rgba(95, 212, 255, 0.22);
      background: rgba(95, 212, 255, 0.055);
      text-align: center;
    }
    .wi-formula-line {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 12px;
      font-weight: 700;
      line-height: 1.5;
      color: #bfe9ff;
    }
    /* A thing you can buy with $TETRA rather than earn by playing,
       which is why it sits apart from the rows above it. */
    .wi-perk {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 10px;
      flex-wrap: wrap;
      padding: 8px 11px;
      border-radius: 11px;
      border: 1px solid rgba(183, 156, 255, 0.22);
      background: rgba(183, 156, 255, 0.055);
    }
    .wi-perk-label {
      font-size: 10px;
      font-weight: 900;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: #d9cbff;
    }
    .wi-perk-note { font-size: 11px; color: rgba(217, 203, 255, 0.75); }
    /* A qualifier inside a figure, so it does not read as part of it */
    .wi-dim { font-size: 10px; font-weight: 600; color: rgba(151, 184, 222, 0.6); }
    /* Ten bands as chips rather than ten rows saying "1-10 fights
       1-10". The rule is one sentence and the chips are the ladder;
       writing each band twice was the infographic's way of filling a
       rectangle. */
    .wi-bands {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
    }
    .wi-band {
      padding: 5px 10px;
      border-radius: 999px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      font-weight: 700;
      color: rgba(196, 220, 244, 0.9);
      white-space: nowrap;
    }
    .wi-body::-webkit-scrollbar { width: 7px; }
    .wi-body::-webkit-scrollbar-track { background: transparent; }
    .wi-body::-webkit-scrollbar-thumb {
      border-radius: 999px;
      background: rgba(151, 184, 222, 0.18);
    }
    .wi-card {
      display: grid;
      align-content: start;
      gap: 9px;
      padding: 14px 15px 15px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.96), rgba(9, 13, 22, 0.99));
      min-width: 0;
    }
    .wi-card-title {
      margin: 0;
      font-size: 10px;
      font-weight: 900;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--tr-cyan);
    }
    .wi-list {
      margin: 0;
      padding-left: 16px;
      display: grid;
      gap: 6px;
      font-size: 12.5px;
      line-height: 1.5;
      color: var(--tr-text-primary);
    }
    .wi-list li::marker { color: rgba(95, 212, 255, 0.5); }
    .wi-rows { margin: 0; display: grid; gap: 5px; }
    .wi-row {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: baseline;
      gap: 10px;
      padding: 5px 0;
      border-bottom: 1px solid rgba(151, 184, 222, 0.07);
    }
    .wi-row:last-child { border-bottom: 0; }
    .wi-row dt {
      font-size: 12.5px;
      color: var(--tr-text-primary);
      min-width: 0;
    }
    .wi-row dd {
      margin: 0;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 12.5px;
      font-weight: 800;
      color: var(--tr-text-primary);
      white-space: nowrap;
    }
    /* Two tints, the same pair the player card uses for a win and a
       loss, so a gain and a penalty read the same way everywhere. */
    .wi-good { color: #8fe0b0 !important; }
    .wi-bad { color: #e8a0a8 !important; }
    /* The keys column leads here, which is the opposite of the rows
       above: a control is looked up by its key, not by its name. */
    .wi-keys .wi-row { grid-template-columns: auto minmax(0, 1fr); }
    .wi-keys dd {
      font-family: inherit;
      font-weight: 600;
      white-space: normal;
      text-align: right;
      color: var(--tr-text-muted);
    }
    .wi-keys dt { display: flex; gap: 4px; flex-wrap: wrap; }
    kbd {
      display: inline-grid;
      place-items: center;
      min-width: 24px;
      padding: 2px 6px;
      border-radius: 6px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.04);
      box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.35);
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 10px;
      font-weight: 800;
      color: #dff4ff;
    }
    .wi-combo {
      display: flex;
      align-items: center;
      gap: 9px;
      flex-wrap: wrap;
      padding: 9px 11px;
      border-radius: 11px;
      border: 1px solid rgba(183, 156, 255, 0.22);
      background: rgba(183, 156, 255, 0.055);
    }
    .wi-combo-label {
      flex: none;
      font-size: 9px;
      font-weight: 900;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: #d9cbff;
    }
    .wi-combo-steps { font-size: 11.5px; color: var(--tr-text-primary); }
    .wi-combo-steps b { color: #d9cbff; }
    .wi-note {
      margin: 0;
      font-size: 11.5px;
      line-height: 1.55;
      color: rgba(151, 184, 222, 0.72);
    }
    .wi-note strong { color: #bfe9ff; font-weight: 700; }
    /* The one cell in the scoring table that is a sentence rather
       than a figure: topping out costs no points, it hands the other
       player a knockout. */
    .wi-row-text {
      font-family: inherit !important;
      font-weight: 700;
      white-space: normal !important;
      text-align: right;
    }

    /* ---- narrower than a monitor ----

       Two columns, and Controls stops being a column of its own --
       at this width a full-height rail beside two short cards is the
       gap it was meant to fill. It goes full width instead and its
       keys run two to a row, which is what keeps it from becoming a
       long thin list of seven. */
    @media (max-width: 1040px) {
      .wi-body {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-areas:
          "play play"
          "score keys"
          "rew rew"
          "rank ener"
          "band band";
      }
      .wi-area-keys .wi-rows {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: 20px;
      }
    }

    /* ---- phones ----

       One column, and the card takes the screen rather than floating
       in the middle of it: at this size the margin around a dialog is
       space the content needed. The header keeps clear of the close
       button by padding rather than by moving, so the title stays
       centred.

       The keys go back to one per row here. Two columns of them at
       360px wide is four words to a cell. */
    @media (max-width: 640px) {
      /* ONE scroll container, and it is this one.
       *
       * The desktop shape is a height-capped card centred in the
       * panel with its own scrolling body inside it. On a phone that
       * is two nested scrollers, and a drag lands on whichever the
       * browser decides: the page moves, or the card moves, or
       * nothing does. What it looked like was a panel opened part
       * way down with the heading already gone.
       *
       * So on a phone the card has no height limit and grows to fit
       * its content, the body stops scrolling, and the panel behind
       * it scrolls the lot. There is nothing to get stuck inside. */
      .welcome-panel {
        padding: 8px;
        place-items: start center;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior: contain;
      }
      .welcome-card {
        width: 100%;
        max-width: 560px;
        max-height: none;
        /* At least a screenful, so a short panel does not float as a
           small box in the middle of a phone. dvh after vh: the
           dynamic unit accounts for the address bar, and the static
           one is the fallback where it is not understood. */
        min-height: calc(100vh - 16px);
        min-height: calc(100dvh - 16px);
        padding: 16px 12px 12px;
        border-radius: 18px;
        gap: 12px;
      }
      .wi-body {
        overflow: visible;
        padding-right: 0;
      }
      /* Pinned to the viewport rather than to the card. The card is
         now taller than the screen, so a close button in its corner
         is a button you have to scroll back up to find. */
      .welcome-close {
        position: fixed;
        top: calc(10px + env(safe-area-inset-top, 0px));
        right: 10px;
        z-index: 4;
        /* opaque, because content passes underneath it now */
        background: rgba(12, 18, 30, 0.94);
        backdrop-filter: blur(6px);
        -webkit-backdrop-filter: blur(6px);
      }
      .wi-head { padding: 0 34px; }
      .wi-title { font-size: clamp(17px, 5.6vw, 24px); letter-spacing: .08em; }
      .wi-tagline { font-size: 9px; letter-spacing: .14em; }
      .wi-body {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas:
          "play" "score" "keys" "rew" "rank" "ener" "band";
        gap: 10px;
      }
      .wi-area-keys .wi-rows { grid-template-columns: minmax(0, 1fr); }
      .wi-card { padding: 12px 13px 13px; gap: 8px; }
      .wi-list, .wi-row dt, .wi-row dd { font-size: 12px; }
      .wi-note { font-size: 11px; }
    }

    /* ---- a phone held sideways ----

       The game's own orientation, and the one where a dialog sized
       in vh has the least to work with. Everything above the
       scrolling body shrinks so the body keeps most of the height,
       and the tagline goes: it is the one line on the panel that
       says nothing a player needs. */
    @media (max-height: 560px) {
      /* The same one-scroller treatment as the phone rules above.
         This query catches a phone held sideways, where the width is
         often over 640 and those rules do not apply -- and a short
         screen is exactly where a capped card with a scroller inside
         it goes wrong. */
      .welcome-panel {
        padding: 8px;
        place-items: start center;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior: contain;
      }
      .welcome-card {
        max-height: none;
        padding: 12px 12px 10px;
        gap: 9px;
      }
      .wi-body { overflow: visible; padding-right: 0; }
      .welcome-close {
        position: fixed;
        top: calc(8px + env(safe-area-inset-top, 0px));
        right: 8px;
        z-index: 4;
        background: rgba(12, 18, 30, 0.94);
      }
      .wi-title { font-size: clamp(15px, 3.4vh, 22px); }
      .wi-tagline { display: none; }
      .wi-card { padding: 10px 12px 11px; gap: 7px; }
      .wi-combo { padding: 7px 9px; }
      .welcome-close { top: 10px; right: 10px; width: 30px; height: 30px; font-size: 16px; }
    }
    .loading-panel {
      position: fixed;
      inset: 0;
      z-index: 300;
      display: grid;
      place-items: center;
      background: rgba(0, 0, 0, 0.72);
      opacity: 0;
      pointer-events: none;
      transition: opacity .15s ease;
    }
    .loading-panel.show {
      opacity: 1;
      pointer-events: auto;
    }
    .loading-spinner {
      width: 64px;
      height: 64px;
      border-radius: 50%;
      border: 6px solid rgba(255,255,255,0.16);
      border-top-color: #69d69a;
      animation: loadingSpin .8s linear infinite;
      box-shadow: 0 0 24px rgba(105, 214, 154, 0.18);
    }
    @keyframes loadingSpin {
      to { transform: rotate(360deg); }
    }
    .confirm-btn.loading {
      opacity: 0.85;
      pointer-events: none;
    }

    @media (max-width: 820px) {

       
      .winners-summary {
        width: 75%;
      }

      .reward-distribution-card{
        width: 95%;
        height: 98%;
        z-index: 100;
      }

      /* Same reason as the block above: the card body scrolls, the
         list does not. */
      .reward-distribution-list {
        display: grid;
        gap: 6px;
        padding-right: 2px;
      }

      .reward-distribution-player {
        grid-column: 2;
        grid-row: 1;
      }

      .scoreboard-name{
        margin-top: -5px;
      }

      .reward-distribution-time {
        grid-column: 2;
        grid-row: 2;
        text-align: left;
        top: -10px;
      }
      .reward-distribution-sol {
        grid-column: 3;
        grid-row: 1 / span 1;
        justify-self: end;
        font-size: 8px;
      }
      .reward-status-btn {
        grid-column: 4;
        grid-row: 1 / span 1;
        align-self: center;
        font-size: 8px;
        padding: 5px;
        min-height: 10px;
        min-width: 5px;
      }
      .reward-distribution-row {
        grid-template-columns: 22px minmax(0, 1fr) 72px 50px;
        gap: 6px;
        padding: 8px 10px;
        border-radius: 14px;
        min-height: 50px;
      }
    }

    @media (orientation: portrait) {

      /* A second copy of the old .welcome-card lived here -- fixed
         `height: min(70vh, 700px)`, `place-items: center`,
         `overflow: hidden`, the green edge and the glow. Being later
         in this file than the real rule, it won in portrait: the card
         was pinned to 70% of the screen whatever was in it, and
         anything past that was clipped with no way to scroll to it.
         That is the whole of "I cannot see the other information".

         Deleted rather than corrected. The card is sized by the
         welcome rules above and by the phone and short-screen
         queries beside them; a portrait-only copy is a fourth place
         to change it and the one that would win. */

      .score-chip{
        scale: 0.5;
      }

      .piece-stack{
        position: absolute;
        left: 5%;
        top: 20%;
      }
      body {
        display: block;
        overflow-x: hidden;
        overflow-y: auto;
      }
      .app {
        width: 100%;
        min-height: 100%;
        padding: 10px 8px 14px;
        align-items: start;
        justify-items: stretch;
      }
      .main-column {
        width: 100%;
        justify-items: center;
      }
      .main-column > div[style*="display:flex; gap:18px; align-items:flex-start;"] {
        display: grid !important;
        gap: 12px;
        width: 100%;
        align-items: start;
      }
      .play-shell {
        width: 100%;
        display: grid;
        justify-items: center;
      }
      .overlay{
        inset: 5px;
      }

      .overlay h2{
        font-size: 15px;
      }

      .overlay p{
        font-size: 10px;
      }

      .board-wrap {
        padding: 5px;
        border-radius: 7px;
        width: min(82vw, 150px);
        --frame-pad: 5px;
      }
      .ko-flash { inset: 5px; border-radius: 3px; }
      .piece-stack { display: grid; }
      .liveboards-shell { display: none !important; }
      .scoreboard-panel {
        right: 12px;
        left: auto;
        transform: none;
        width: min(86vw, 320px);
      }
      .scoreboard-panel.collapsed {
        width: auto;
        height: auto;
        padding: 0;
        background: #070b1400;
        box-shadow: none;
        border: 0;
        top: 10px;
        right: 10px;
        left: auto;
        bottom: auto;
      }
      .scoreboard-panel.collapsed .scoreboard-panel-inner {
        background: transparent;
        border: 0;
        box-shadow: none;
      }
      .scoreboard-panel.collapsed .scoreboard-panel-inner { gap: 0; }
      .scoreboard-panel.collapsed .scoreboard-subtitle,
      .scoreboard-panel.collapsed .scoreboard-list,
      .scoreboard-panel.collapsed .scoreboard-footer { display: none; }
      .scoreboard-panel.collapsed .scoreboard-toggle {
        width: 80px;
        height: 15px;
        font-size: 8px;
        border-radius: 5px;
      }

      .scoreboard-panel:not(.collapsed) {
        right: 10px;
        transform: none;
        inset: 0;
        width: 100%;
        height: 95%;
        max-height: 100vh;
        border-radius: 20px;
        left: auto;
        top: 10px;
        transform: scale(0.6);
      }
      .scoreboard-panel-inner {
        align-items: center;
      }
      .scoreboard-panel .scoreboard-toggle {
        width: 100%;
        display: block;
        height: 15px;
        font-size: 45px;
        letter-spacing: .08em;
        border-radius: 50px;
        top: 1%;
      }
      .scoreboard-panel:not(.collapsed) .scoreboard-mode-toggle {
        display: block;
        margin-top: 6px;
        top: 99%;
      }

      .winners-title{
        font-size: 15px;
      }

      .winner-summary .summary-label{
        font-size: 8px;
      }

      .winner-summary .summary-value{
        font-size: 10px;
      }

    }

    /* =========================================================
       CHROME PANEL
       The account chip, holdings chip, Daily Spin launcher, music
       button and Reward Distribution button still wore the old glassy
       control treatment -- 14-20px corners, a cyan wash and a soft
       outer glow -- while the playfield, the HOLD/NEXT cards and the
       KO / LINES SENT / RANK modules moved to the quiet PANEL FRAME.
       They read as two different products side by side, so the chrome
       now follows the same rules: a hairline edge, an inner bevel, a
       diffuse drop shadow, and one accent colour per control carried
       by a thin bar along the top edge instead of by a lit outline.
       Hover eases the edge up a step; it never glows.

       Declared last so it owns border, background and box-shadow for
       these controls, and scoped by id where a later component rule
       (the Daily Spin modal's own buttons) would otherwise win.
       ========================================================= */
    #accountChip,
    #holdingsChip,
    .music-button,
    .reward-distribution-btn {
      /* neutral by default; each control overrides below */
      --chrome-accent: rgba(151, 184, 222, 0.52);
      --chrome-edge: rgba(151, 184, 222, 0.26);
      --chrome-wash: rgba(151, 184, 222, 0.045);
      position: relative;
      border: 1px solid var(--panel-edge);
      border-radius: 12px;
      background:
        radial-gradient(120% 86% at 50% 0%, var(--chrome-wash), transparent 64%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.99), rgba(9, 13, 22, 1));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 8px 20px rgba(0, 0, 0, 0.28);
      transition:
        background var(--tr-transition-standard),
        border-color var(--tr-transition-standard),
        box-shadow var(--tr-transition-standard),
        color var(--tr-transition-standard),
        filter var(--tr-transition-standard),
        opacity var(--tr-transition-standard),
        translate var(--tr-transition-fast);
    }
    /* the nub keeps the group's frame but not its 12px radius: its top edge is
       tucked under the account chip, so only the bottom corners round off */
    #holdingsChip { border-radius: 4px 4px 14px 14px; }
    /* same 2px top bar the score modules use, inset to the corner radius. The
       nub is left out of the list: that edge sits behind the account chip. */
    #accountChip::after,
    .reward-distribution-btn::after {
      content: "";
      position: absolute;
      top: 0;
      left: 12px;
      right: 12px;
      height: 2px;
      border-radius: 0 0 2px 2px;
      background: linear-gradient(90deg, transparent, var(--chrome-accent), transparent);
      opacity: .8;
      pointer-events: none;
    }
    /* the music button is a 44px square -- a bar across it would read as a
       stripe rather than an accent, so it keeps the frame alone */
    #accountChip {
      --chrome-accent: rgba(95, 212, 255, 0.52);
      --chrome-edge: rgba(95, 212, 255, 0.30);
      --chrome-wash: rgba(95, 212, 255, 0.055);
    }
    .reward-distribution-btn {
      --chrome-accent: rgba(255, 200, 96, 0.52);
      --chrome-edge: rgba(255, 200, 96, 0.30);
      --chrome-wash: rgba(255, 200, 96, 0.05);
    }
    /* holdings carries the one piece of state in the group, so its accent
       follows holder status instead of being fixed */
    #holdingsChip:has(.holdings-value.holdings-holder) {
      --chrome-accent: rgba(105, 214, 154, 0.52);
      --chrome-edge: rgba(105, 214, 154, 0.30);
      --chrome-wash: rgba(105, 214, 154, 0.05);
    }
    #holdingsChip:has(.holdings-value.holdings-non-holder) {
      --chrome-accent: rgba(255, 118, 118, 0.44);
      --chrome-edge: rgba(255, 118, 118, 0.26);
      --chrome-wash: rgba(255, 96, 96, 0.04);
    }
    #accountChip:hover,
    #holdingsChip:hover,
    .music-button:hover,
    .reward-distribution-btn:hover {
      border-color: var(--chrome-edge);
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.05),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 10px 24px rgba(0, 0, 0, 0.32);
    }
    /* the SOL badge stops being a lit ring and becomes a recessed token */
    .reward-distribution-btn-icon {
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(8, 12, 20, 1), rgba(5, 9, 15, 1));
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.40);
      color: #ffd68f;
    }
    /* the refresh control was a bare transparent hit area; give it the same
       recess the HOLD/NEXT canvases sit in so it reads as a button */
    #holdingsReloadBtn {
      border: 1px solid var(--panel-edge);
      border-radius: 8px;
      background: linear-gradient(180deg, rgba(8, 12, 20, 1), rgba(5, 9, 15, 1));
      box-shadow:
        inset 0 0 0 1px rgba(0, 0, 0, 0.40),
        inset 0 1px 6px rgba(0, 0, 0, 0.45);
    }
    #holdingsReloadBtn:hover {
      border-color: var(--panel-edge-hi);
    }
    #holdingsReloadBtn .reload-icon { font-size: 16px; color: #cfdcea; }
    /* value colours stay, the halos around them go */
    #holdingsChip .holdings-value.holdings-holder {
      color: #7fdfa9;
      text-shadow: none;
    }
    #holdingsChip .holdings-value.holdings-non-holder {
      color: #ff8b8b;
      text-shadow: none;
    }
    /* ---- music popover: same frame as the button it hangs off ---- */
    .music-popover {
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.985), rgba(9, 13, 22, 0.995));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        inset 0 0 0 1px rgba(0, 0, 0, 0.30),
        0 14px 30px rgba(0, 0, 0, 0.34);
    }
    /* the shared panel hairline is cyan-to-violet; match the dock's accent bar
       instead. The anchor rules below used to pin one side to `auto`, which
       collapsed the hairline to nothing whenever the popover was anchored. */
    .music-popover::before,
    .music-flyout.right-anchor .music-popover::before,
    .music-flyout.left-anchor .music-popover::before {
      top: 0;
      left: 14px;
      right: 14px;
      height: 2px;
      border-radius: 0 0 2px 2px;
      background: linear-gradient(90deg, transparent, rgba(151, 184, 222, 0.5), transparent);
      opacity: .8;
    }
    .music-mute-btn {
      border: 1px solid var(--panel-edge);
      border-radius: 8px;
      background: linear-gradient(180deg, rgba(20, 27, 42, 0.99), rgba(10, 15, 24, 1));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        0 6px 16px rgba(0, 0, 0, 0.26);
      transition:
        border-color var(--tr-transition-standard),
        box-shadow var(--tr-transition-standard);
    }
    .music-mute-btn:hover {
      border-color: var(--panel-edge-hi);
    }

    /* =========================================================
       BOMB EXCHANGE
       A bomb earned on one board flies to the other and lands as a
       garbage row. The projectiles live on a fixed layer over the whole
       viewport rather than inside either board, because an arc between
       two boards would otherwise be clipped by the first one it leaves.
       Kept warm and dim rather than neon: the boards stay the brightest
       thing on screen.
       ========================================================= */
    .bomb-flight-layer {
      position: fixed;
      inset: 0;
      z-index: 62;
      pointer-events: none;
      overflow: hidden;
    }
    /* the negative margin makes the element's centre sit on the
       translate() point, so the flight path can be written in plain
       viewport coordinates */
    .bomb-flight {
      position: absolute;
      top: 0;
      left: 0;
      width: 24px;
      height: 24px;
      margin: -12px 0 0 -12px;
      background: url("Tiles/Block.png") center / contain no-repeat;
      filter: drop-shadow(0 0 7px rgba(236, 158, 92, 0.42));
      will-change: transform, opacity;
    }
    .bomb-impact {
      position: absolute;
      top: 0;
      left: 0;
      width: 28px;
      height: 28px;
      margin: -14px 0 0 -14px;
      border-radius: 50%;
      border: 1px solid rgba(236, 168, 104, 0.5);
      background: radial-gradient(circle, rgba(236, 168, 104, 0.22), transparent 66%);
      will-change: transform, opacity;
    }
    canvas.bomb-hit {
      animation: bomb-shake .36s cubic-bezier(.36, .07, .19, .97) both;
    }
    @keyframes bomb-shake {
      10%, 90% { transform: translate3d(-1px, 0, 0); }
      20%, 80% { transform: translate3d(2px, 0, 0); }
      30%, 50%, 70% { transform: translate3d(-3px, 0, 0); }
      40%, 60% { transform: translate3d(3px, 0, 0); }
    }
    @media (prefers-reduced-motion: reduce) {
      canvas.bomb-hit { animation: none; }
    }

    /* ---- the queue: bombs waiting to land ----
       Bombs no longer land when they arrive; they wait for the board's next
       lock, which blocks what it can. This is where they wait: a slim track
       in the left padding of every board frame -- yours, the opponent's, the
       spectator's and the replay's seats. One segment per bomb, each exactly
       a row of the playfield tall, so the fill reads as how far the stack is
       about to come up. Warm like the bombs in flight, deepening toward red
       once it is a real threat. No glow: the fill is the signal. */
    .garbage-meter {
      --meter-w: clamp(3px, calc(var(--frame-pad, 12px) * 0.36), 5px);
      --garbage-rows: 0;
      position: absolute;
      top: var(--frame-pad, 12px);
      bottom: var(--frame-pad, 12px);
      left: calc((var(--frame-pad, 12px) - var(--meter-w)) / 2);
      width: var(--meter-w);
      border-radius: 2px;
      background: rgba(151, 184, 222, 0.07);
      overflow: hidden;
      pointer-events: none;
      z-index: 3;
    }
    .garbage-meter-fill {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: calc(var(--garbage-rows) * 5%);
      border-radius: 2px;
      /* a dark hairline at the top of every row splits the fill into bombs */
      background:
        linear-gradient(to bottom, rgba(8, 12, 20, 0.85) 0 1px, transparent 1px)
          top / 100% calc(100% / max(1, var(--garbage-rows))) repeat-y,
        linear-gradient(to top, #cf7e4c, #e6a86a);
      transition: height .24s cubic-bezier(.2, .8, .2, 1);
    }
    .garbage-meter.is-heavy .garbage-meter-fill {
      background:
        linear-gradient(to bottom, rgba(8, 12, 20, 0.85) 0 1px, transparent 1px)
          top / 100% calc(100% / max(1, var(--garbage-rows))) repeat-y,
        linear-gradient(to top, #b8424f, #e0706a);
    }
    /* bombs arriving: the track swells for a beat */
    .garbage-meter.is-arriving { animation: garbageArrive .42s cubic-bezier(.2, .8, .2, 1); }
    @keyframes garbageArrive {
      0% { transform: none; background-color: rgba(151, 184, 222, 0.07); }
      30% { transform: scaleX(1.9); background-color: rgba(236, 168, 104, 0.3); }
      100% { transform: none; background-color: rgba(151, 184, 222, 0.07); }
    }
    /* a block: the waiting bombs go ice-white as they drain away */
    .garbage-meter.is-blocking { animation: garbageBlockTrack .6s ease-out; }
    .garbage-meter.is-blocking .garbage-meter-fill { animation: garbageBlockFill .5s ease-out; }
    @keyframes garbageBlockTrack {
      0% { background-color: rgba(169, 212, 255, 0.5); transform: scaleX(2); }
      100% { background-color: rgba(151, 184, 222, 0.07); transform: none; }
    }
    @keyframes garbageBlockFill {
      0% { filter: brightness(2.4) saturate(0); }
      100% { filter: none; }
    }

    /* ---- a block, on the board that made it ----
       Covers the playfield exactly, like the K.O. stamp, but under it and
       under the perfect-clear pill: a block is good news, not the news. A
       translucent band sweeps up from the bottom -- the side the rows would
       have come from -- carrying BLOCKED and the count, while the bombs
       that were waiting are knocked out of the meter. Ice blue rather than
       the warm bomb family: it is the one bomb event that went your way. */
    .deflect-fx {
      position: absolute;
      inset: var(--frame-pad, 12px);
      overflow: hidden;
      border-radius: 5px;
      opacity: 0;
      pointer-events: none;
      z-index: 21;
      /* Its size comes from the frame, never from what is in it, so it can
         be a size container safely -- and the label below is measured
         against the playfield it has to fit. A fixed-size label was wider
         than the board at 150% zoom on a 1366x768 screen. */
      container-type: size;
    }
    .deflect-fx.show { opacity: 1; }
    .deflect-fx-sweep {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 34%;
      background: linear-gradient(to top,
        rgba(140, 200, 255, 0) 0%,
        rgba(140, 200, 255, 0.09) 52%,
        rgba(214, 238, 255, 0.28) 97%,
        rgba(214, 238, 255, 0) 100%);
      opacity: 0;
      transform: translateY(100%);
    }
    .deflect-fx.show .deflect-fx-sweep { animation: deflectSweep 760ms cubic-bezier(.2, .7, .25, 1) forwards; }
    @keyframes deflectSweep {
      0% { transform: translateY(100%); opacity: 0; }
      14% { opacity: 1; }
      72% { opacity: .85; }
      100% { transform: translateY(-200%); opacity: 0; }
    }
    .deflect-fx-label {
      position: absolute;
      left: 50%;
      top: 64%;
      display: inline-flex;
      align-items: center;
      gap: .54em;
      padding: .54em 1.08em .54em .85em;
      border-radius: 999px;
      white-space: nowrap;
      /* about 12 of these wide, 13.5 at the peak of its pop: this keeps it
         inside every playfield from the 150px of a zoomed laptop to the
         full board. The px value is the fallback without container units. */
      font-size: 13px;
      font-size: clamp(7px, 6.2cqw, 13px);
      font-weight: 900;
      line-height: 1;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: #e8f4ff;
      background: linear-gradient(180deg, rgba(56, 92, 136, 0.95), rgba(24, 42, 70, 0.97));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.2),
        inset 0 0 0 1px rgba(169, 212, 255, 0.24),
        0 8px 20px rgba(0, 0, 0, 0.34);
      opacity: 0;
      transform: translate(-50%, -50%) scale(.7);
    }
    .deflect-fx.show .deflect-fx-label { animation: deflectLabel 1250ms cubic-bezier(.2, .9, .25, 1) forwards; }
    @keyframes deflectLabel {
      0% { opacity: 0; transform: translate(-50%, -20%) scale(.7); }
      16% { opacity: 1; transform: translate(-50%, -64%) scale(1.12); }
      30% { transform: translate(-50%, -50%) scale(1); }
      76% { opacity: 1; transform: translate(-50%, -58%) scale(1); }
      100% { opacity: 0; transform: translate(-50%, -84%) scale(.96); }
    }
    .deflect-fx-icon {
      width: 1.15em;
      height: 1.15em;
      flex: none;
      fill: rgba(169, 212, 255, 0.92);
    }
    .deflect-fx-count {
      font-size: 1.15em;
      letter-spacing: .04em;
      font-variant-numeric: tabular-nums;
      color: #ffffff;
    }
    /* the blocked bombs themselves, knocked back out of the meter;
       positioned and flown by spawnDeflectShards() */
    .deflect-fx-shard {
      position: absolute;
      top: 0;
      left: 0;
      width: 14px;
      height: 14px;
      margin: -7px 0 0 -7px;
      background: url("Tiles/Block.png") center / contain no-repeat;
      filter: saturate(.3) brightness(1.3);
      will-change: transform, opacity;
    }
    /* the spectator's and replay's boards are 240px wide, 168px on a phone */
    .spectate-board-wrap .deflect-fx-shard { width: 10px; height: 10px; margin: -5px 0 0 -5px; }

    /* A watched seat's own perfect-clear banner, in two lines so it fits a
       board this narrow. Before it, a seat only had the canvas popup, which
       was clipped at both ends. */
    .spectate-perfect-popup {
      top: 24%;
      flex-direction: column;
      gap: 4px;
      padding: 9px 14px;
      font-size: 11px;
      letter-spacing: .12em;
    }
    .spectate-perfect-popup span:last-child { font-size: 15px; letter-spacing: .05em; }

    /* The full-screen perfect clear standing aside for a KO -- see
       yieldPerfectClearFx(). */
    .pc-fx.show.is-yielding { animation: pcFxYield .32s ease-out forwards; }
    @keyframes pcFxYield {
      from { opacity: 1; }
      to { opacity: 0; visibility: hidden; }
    }

    @media (max-width: 720px) {
      .spectate-perfect-popup { font-size: 9px; padding: 7px 10px; }
      .spectate-perfect-popup span:last-child { font-size: 12px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .garbage-meter.is-arriving,
      .garbage-meter.is-blocking,
      .garbage-meter.is-blocking .garbage-meter-fill,
      .deflect-fx.show .deflect-fx-sweep { animation: none; }
      .garbage-meter-fill { transition: none; }
      .deflect-fx.show .deflect-fx-label {
        animation: deflectLabelStill 1250ms linear forwards;
      }
      @keyframes deflectLabelStill {
        0%, 80% { opacity: 1; transform: translate(-50%, -50%); }
        100% { opacity: 0; transform: translate(-50%, -50%); }
      }
    }

    /* =========================================================
       USER PROFILE PANEL
       Opened by the account chip once an account exists. The frame,
       gradient and top hairline come from the shared panel rules above --
       this section only adds what is specific to it. Identity is read-only;
       the country select is the one thing the Save button commits.
       ========================================================= */
    .user-profile-panel {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, .62);
      display: grid;
      place-items: center;
      z-index: 112;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
      padding: 16px;
    }
    .user-profile-panel.show { opacity: 1; pointer-events: auto; }
    .user-profile-card {
      width: min(94vw, 520px);
      max-height: min(88vh, 860px);
      overflow-y: auto;
      border-radius: 24px;
      padding: 26px 22px 22px;
    }
    .user-profile-card::-webkit-scrollbar { width: 7px; }
    .user-profile-card::-webkit-scrollbar-track { background: transparent; }
    .user-profile-card::-webkit-scrollbar-thumb {
      border-radius: 999px;
      background: rgba(151, 184, 222, 0.18);
    }
    .user-profile-title {
      margin: 0 0 18px;
      font-size: 13px;
      font-weight: 900;
      letter-spacing: .22em;
      text-transform: uppercase;
      color: var(--tr-cyan);
      text-align: center;
    }

    /* ---- identity ---- */
    .up-identity {
      display: grid;
      grid-template-columns: 44px 1fr;
      align-items: center;
      gap: 12px;
      padding: 12px 14px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background:
        radial-gradient(120% 90% at 50% 0%, rgba(95, 212, 255, 0.05), transparent 64%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.96), rgba(9, 13, 22, 0.99));
    }
    .up-flag {
      width: 44px;
      height: 30px;
      border-radius: 6px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.05);
      border: 1px solid var(--panel-edge);
    }
    .up-flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .up-identity-text { min-width: 0; display: grid; gap: 3px; }
    /* The nickname and its edit control share a row so the pencil sits against the
       right edge of the identity block, level with the name. */
    .up-nickname-row {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
    }
    .up-nickname {
      flex: 1 1 auto;
      font-size: 17px;
      font-weight: 900;
      letter-spacing: .01em;
      color: #eaf4ff;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .up-nickname-input {
      flex: 1 1 auto;
      min-width: 0;
      width: 100%;
      margin: -3px 0;
      padding: 3px 8px;
      border-radius: 8px;
      border: 1px solid var(--panel-edge-hi);
      background: linear-gradient(180deg, rgba(10, 15, 25, 0.92), rgba(7, 10, 18, 0.96));
      color: #eaf4ff;
      font: inherit;
      font-size: 16px;
      font-weight: 800;
      letter-spacing: .01em;
      outline: none;
      transition: border-color var(--tr-transition-standard);
    }
    .up-nickname-input:focus { border-color: rgba(95, 212, 255, 0.42); }
    .up-nickname-input.invalid { border-color: rgba(255, 111, 130, 0.5); }
    .up-edit-btn {
      flex: 0 0 auto;
      width: 26px;
      height: 26px;
      padding: 0;
      display: grid;
      place-items: center;
      border-radius: 8px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.98), rgba(9, 14, 24, 0.99));
      color: rgba(178, 202, 226, 0.6);
      cursor: pointer;
      transition: border-color var(--tr-transition-standard), color var(--tr-transition-standard);
    }
    .up-edit-btn:hover { border-color: var(--panel-edge-hi); color: #eaf6ff; }
    .up-edit-glyph { width: 15px; height: 15px; }
    /* pencil while read-only, a revert cross while the field is open */
    .up-edit-revert,
    .up-nickname-row.editing .up-edit-pencil { display: none; }
    .up-nickname-row.editing .up-edit-revert { display: block; }
    .up-nickname-row.editing .up-edit-btn { color: rgba(255, 178, 188, 0.72); }
    .up-nickname-row.editing .up-edit-btn:hover { border-color: rgba(255, 111, 130, 0.36); color: #ffd2d6; }
    .up-address-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
    .up-address {
      flex: 1 1 auto;
      min-width: 0;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      color: rgba(151, 184, 222, 0.72);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .up-copy-btn {
      flex: 0 0 auto;
      padding: 4px 10px;
      border-radius: 999px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.98), rgba(9, 14, 24, 0.99));
      color: #cfe4f5;
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .06em;
      cursor: pointer;
      transition: border-color var(--tr-transition-standard), color var(--tr-transition-standard);
    }
    .up-copy-btn:hover { border-color: var(--panel-edge-hi); color: #eaf6ff; }
    .up-copy-btn.copied { color: var(--tr-green); border-color: rgba(120, 229, 178, 0.4); }

    /* ---- the three stat tiles ---- */
    .up-stats {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
      margin-top: 12px;
    }
    .up-stat {
      display: grid;
      align-content: start;
      gap: 7px;
      padding: 12px 12px 13px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.96), rgba(9, 13, 22, 0.99));
    }
    .up-stat-head { display: flex; align-items: center; gap: 7px; min-width: 0; }
    .up-stat-label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .up-icon {
      flex: 0 0 auto;
      width: 15px;
      height: 15px;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.5;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    .up-icon .up-icon-fill { fill: currentColor; stroke: none; }
    .up-stat-holdings { color: var(--tr-cyan); }
    /* The flask is stroked in currentColor, so the block's colour is
       the icon's. Taking the ramp here puts the panel's icon on the
       same scale as the cell above the board: red low, yellow at 50,
       blue at 80. The fallback is the colour it always was, for the
       moment before the document arrives. */
    .up-stat-energy { color: var(--energy-tint, var(--tr-yellow)); }
    .up-stat-earnings { color: var(--tr-green); }
    .up-stat-value {
      display: flex;
      align-items: baseline;
      gap: 5px;
      font-size: 19px;
      font-weight: 900;
      color: #eaf4ff;
      line-height: 1;
    }
    .up-stat-unit {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .06em;
      color: var(--tr-text-muted);
    }
    .up-energy-value {
      display: flex;
      align-items: baseline;
      gap: 4px;
      font-size: 19px;
      font-weight: 900;
      color: #eaf4ff;
      line-height: 1;
    }
    .up-energy-max { font-size: 10px; font-weight: 800; color: var(--tr-text-muted); }
    .up-energy-track {
      height: 6px;
      border-radius: 999px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.06);
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
    }
    .up-energy-fill {
      height: 100%;
      width: 0%;
      border-radius: 999px;
      /* the shared ramp, same as everywhere else energy is drawn */
      background: linear-gradient(90deg,
          color-mix(in srgb, var(--energy-tint, #ffd878) 55%, transparent),
          var(--energy-tint, #ffd878));
      transition: width .32s cubic-bezier(.2, .85, .3, 1),
                  background .22s ease;
    }

    /* ---- country, the one editable field ---- */
    .up-country-field { margin-top: 14px; }

    /* ---- actions ---- */
    .up-actions {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      margin-top: 14px;
    }
    /* Right under Request Payout, because it is the condition on
       pressing it. Empty until the settings snapshot lands, and the
       margin goes with it so nothing leaves a gap for a line that
       is not there. */
    .up-min-payout {
      margin: 8px 0 0;
      font-size: 11px;
      letter-spacing: .06em;
      color: var(--tr-text-muted, #8c9aab);
      text-align: right;
    }
    .up-min-payout:empty { display: none; }
    .up-secondary-btn,
    .up-payout-btn {
      padding: 11px 14px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.98), rgba(9, 14, 24, 0.99));
      color: #dff4ff;
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .07em;
      cursor: pointer;
      transition: border-color var(--tr-transition-standard), color var(--tr-transition-standard);
    }
    .up-secondary-btn:hover { border-color: var(--panel-edge-hi); color: #eaf6ff; }
    .up-payout-btn {
      border-color: rgba(120, 229, 178, 0.22);
      color: var(--tr-green);
    }
    .up-payout-btn:hover:not(:disabled) { border-color: rgba(120, 229, 178, 0.42); }
    .up-payout-btn:disabled { opacity: .45; cursor: not-allowed; }

    /* ---- payout history ---- */
    .up-payouts { margin-top: 18px; }
    .up-payouts-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-bottom: 8px;
    }
    .up-payouts-title {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .up-payouts-table {
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      overflow: hidden;
      background: linear-gradient(180deg, rgba(14, 20, 32, 0.9), rgba(8, 12, 20, 0.96));
    }
    .up-payout-row {
      display: grid;
      grid-template-columns: minmax(0, 1.5fr) minmax(0, 1.1fr) minmax(0, .9fr) 62px;
      align-items: center;
      gap: 8px;
      padding: 9px 12px;
    }
    .up-payout-headrow {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      border-bottom: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.018);
    }
    .up-payout-list > .up-payout-row + .up-payout-row { border-top: 1px solid rgba(151, 184, 222, 0.07); }
    .up-payout-address {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 10.5px;
      color: rgba(151, 184, 222, 0.78);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .up-payout-date { font-size: 11px; color: var(--tr-text-muted); }
    .up-payout-amount { font-size: 12px; font-weight: 800; color: var(--tr-green); }
    .up-solscan-btn {
      justify-self: end;
      padding: 5px 10px;
      border-radius: 999px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(16, 22, 35, 0.98), rgba(9, 14, 24, 0.99));
      color: #cfe4f5;
      font-size: 9.5px;
      font-weight: 800;
      letter-spacing: .06em;
      cursor: pointer;
      text-decoration: none;
      transition: border-color var(--tr-transition-standard), color var(--tr-transition-standard);
    }
    .up-solscan-btn:hover { border-color: var(--panel-edge-hi); color: #eaf6ff; }
    /* The payout's state, as a quiet pill: tinted text on a faint
       fill of the same hue, no glow. */
    .up-payout-chip {
      justify-self: start;
      max-width: 100%;
      padding: 3px 8px;
      border-radius: 999px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      color: var(--tr-text-muted);
      font-size: 9.5px;
      font-weight: 800;
      letter-spacing: .04em;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .up-payout-chip.is-sent { color: var(--tr-green); border-color: rgba(120, 229, 178, 0.24); }
    .up-payout-chip.is-underReview,
    .up-payout-chip.is-failed { color: #e9c87a; border-color: rgba(233, 200, 122, 0.24); }
    .up-payout-chip.is-rejected { color: #f0a3ad; border-color: rgba(255, 111, 130, 0.24); }
    .up-payout-note {
      margin: 8px 0 0;
      font-size: 11.5px;
      line-height: 1.45;
      color: var(--tr-text-muted);
    }
    .up-payout-note:empty { display: none; }
    .up-payout-note.is-success { color: var(--tr-green); }
    .up-payout-note.is-error { color: #f0a3ad; }
    .up-payout-empty {
      padding: 22px 12px;
      text-align: center;
      font-size: 10px;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }

    /* ---- close ----
       Every card styles its own close button; without a rule here it fell
       back to the generic button fill and sat as a violet square in the
       top-left. The other cards use a solid red circle, which is louder
       than this panel wants, so this one is a quiet ghost that only warms
       up on hover. */
    .user-profile-card .close-btn {
      position: absolute;
      top: 14px;
      right: 14px;
      width: 32px;
      height: 32px;
      padding: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 17px;
      line-height: 1;
      cursor: pointer;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.98), rgba(9, 13, 22, 0.99));
      color: rgba(207, 228, 245, 0.7);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    /* the shared button rules add a sheen sweep and a lift; neither suits a
       close affordance, and they are what made it read as a solid block */
    .user-profile-card .close-btn::before { content: none; }
    .user-profile-card .close-btn:hover {
      transform: none;
      filter: none;
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }
    .user-profile-card .close-btn:active { transform: none; }

    /* =========================================================
       PLAYER INSPECT -- somebody else's card

       The profile panel above, read only, opened from a row in the
       players list. It borrows that panel's frame, its identity
       block and its close button deliberately: the two are the same
       object seen from two sides, and a second visual language for
       "an account" would make them look like different things.

       What it does not borrow is the editing furniture -- no
       nickname pencil, no country select, no save, no payout -- and
       its stat blocks are smaller, because there are nine of them
       rather than three.
       ========================================================= */
    .player-inspect-panel {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, .62);
      display: grid;
      place-items: center;
      /* above the profile panel's 112: a row in the players list can
         be clicked while that one is open */
      z-index: 120;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
      padding: 16px;
    }
    .player-inspect-panel.show { opacity: 1; pointer-events: auto; }
    .player-inspect-card {
      width: min(94vw, 520px);
      max-height: min(88vh, 780px);
      overflow-y: auto;
      border-radius: 24px;
      padding: 26px 22px 22px;
      color: var(--tr-text-primary);
    }
    .player-inspect-card::-webkit-scrollbar { width: 7px; }
    .player-inspect-card::-webkit-scrollbar-track { background: transparent; }
    .player-inspect-card::-webkit-scrollbar-thumb {
      border-radius: 999px;
      background: rgba(151, 184, 222, 0.18);
    }
    .player-inspect-title {
      margin: 0 0 18px;
      font-size: 13px;
      font-weight: 900;
      letter-spacing: .22em;
      text-transform: uppercase;
      color: var(--tr-cyan);
      text-align: center;
    }
    /* the profile card's quiet ghost, not the solid red circle */
    .player-inspect-card .close-btn {
      position: absolute;
      top: 14px;
      right: 14px;
      left: auto;
      width: 32px;
      height: 32px;
      padding: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 17px;
      line-height: 1;
      cursor: pointer;
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--panel-edge);
      color: rgba(207, 228, 245, 0.7);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .player-inspect-card .close-btn::before { content: none; }
    .player-inspect-card .close-btn:hover {
      transform: none;
      filter: none;
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }
    .player-inspect-card .close-btn:active { transform: none; }

    /* ---- identity, with the rank riding on the right ---- */
    .pi-identity {
      display: grid;
      grid-template-columns: 44px minmax(0, 1fr) auto;
      align-items: center;
      gap: 12px;
      padding: 12px 14px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background:
        radial-gradient(120% 90% at 50% 0%, rgba(95, 212, 255, 0.05), transparent 64%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.96), rgba(9, 13, 22, 0.99));
    }
    .pi-flag {
      width: 44px;
      height: 30px;
      border-radius: 6px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.05);
      border: 1px solid var(--panel-edge);
    }
    .pi-flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .pi-identity-text { min-width: 0; display: grid; gap: 3px; }
    .pi-nickname {
      font-size: 17px;
      font-weight: 800;
      color: var(--tr-text-primary);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .pi-address-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
    .pi-address {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      color: rgba(151, 184, 222, 0.66);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .pi-copy-btn {
      flex: 0 0 auto;
      padding: 3px 9px;
      border-radius: 8px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      color: rgba(207, 228, 245, 0.72);
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .pi-copy-btn::before { content: none; }
    .pi-copy-btn:hover {
      transform: none;
      filter: none;
      color: var(--tr-text-primary);
      border-color: var(--tr-border-cyan);
    }
    .pi-report-btn[hidden] { display: none; }
    /* Report is red: red ink, a red hairline and a faint red wash, the
       same red as the game's other warnings -- no glow. */
    .pi-report-btn {
      color: var(--tr-red);
      border-color: rgba(255, 111, 130, 0.38);
      background: var(--tr-red-soft);
    }
    .pi-report-btn:hover {
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.6);
      background: rgba(255, 111, 130, 0.24);
    }
    /* Already reported: the same red, quieter, and it does nothing when
       pressed -- but it still takes the hover, which shows why. */
    .pi-report-btn.is-reported,
    .pi-report-btn.is-reported:hover,
    .tr-plate-report.is-reported,
    .tr-plate-report.is-reported:hover {
      position: relative;
      color: rgba(255, 111, 130, 0.62);
      border-color: rgba(255, 111, 130, 0.2);
      background: transparent;
      cursor: not-allowed;
      opacity: 1;
    }
    /* the hover note: a small themed panel above the button, not the
       browser's own tooltip */
    .pi-report-btn.is-reported::after,
    .tr-plate-report.is-reported::after {
      content: attr(data-tip);
      position: absolute;
      right: 0;
      /* below, not above: both buttons sit near the top of a card that
         clips what spills past its edge */
      top: calc(100% + 8px);
      z-index: 20;
      padding: 7px 10px;
      border-radius: 8px;
      border: 1px solid rgba(255, 111, 130, 0.28);
      background: rgba(10, 16, 29, 0.97);
      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.45);
      color: var(--tr-text-primary);
      font-size: 11.5px;
      font-weight: 600;
      letter-spacing: 0;
      text-transform: none;
      white-space: nowrap;
      pointer-events: none;
      opacity: 0;
      transform: translateY(-3px);
      transition: opacity .14s ease, transform .14s ease;
    }
    .pi-report-btn.is-reported:hover::after,
    .pi-report-btn.is-reported:focus-visible::after,
    .tr-plate-report.is-reported:hover::after,
    .tr-plate-report.is-reported:focus-visible::after {
      opacity: 1;
      transform: translateY(0);
    }

    /* ---- report dialog: the change-password frame, plus a reason
       list and a note ---- */
    .report-player-card { max-height: calc(100vh - 32px); overflow-y: auto; }
    .rp-reasons {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
      margin-bottom: 14px;
    }
    .rp-reason {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
      padding: 10px 12px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(12, 17, 28, 0.9), rgba(8, 12, 20, 0.95));
      color: rgba(207, 228, 245, 0.78);
      font-size: 12px;
      cursor: pointer;
      transition: border-color var(--tr-transition-standard), color var(--tr-transition-standard);
    }
    .rp-reason input { margin: 0; accent-color: #9874ff; flex: 0 0 auto; }
    .rp-reason:hover { border-color: var(--panel-edge-hi); }
    .rp-reason:has(input:checked) { border-color: rgba(152, 116, 255, 0.45); color: #eee8ff; }
    .rp-note {
      width: 100%;
      box-sizing: border-box;
      resize: vertical;
      min-height: 64px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(12, 17, 28, 0.9), rgba(8, 12, 20, 0.95));
      border-radius: 12px;
      padding: 10px 12px;
      color: var(--tr-text-primary);
      font: inherit;
      font-size: 13px;
    }
    .rp-note:focus { outline: none; border-color: var(--panel-edge-hi); }
    #reportPlayerNoteHint { text-transform: none; letter-spacing: 0; opacity: .8; }
    .rp-message { margin: 0; font-size: 11.5px; line-height: 1.45; color: var(--tr-text-muted); }
    .rp-message:empty { display: none; }
    .rp-message.is-success { color: var(--tr-green); }
    .rp-message.is-error { color: #f0a3ad; }
    .report-player-card .cp-confirm-btn:disabled { opacity: .5; cursor: not-allowed; }
    @media (max-width: 380px) { .rp-reasons { grid-template-columns: 1fr; } }

    /* ---- Invite to Battle ----
       Violet like the game's other calls to action, gold only where
       money is shown. Flat washes and hairlines, no glow. */
    .pi-battle-btn[hidden] { display: none; }
    .pi-battle-btn {
      color: #d9ccff;
      border-color: rgba(152, 116, 255, 0.4);
      background: rgba(152, 116, 255, 0.14);
    }
    .pi-battle-btn:hover {
      color: #efe9ff;
      border-color: rgba(152, 116, 255, 0.62);
      background: rgba(152, 116, 255, 0.24);
    }
    .battle-card { max-height: calc(100vh - 32px); overflow-y: auto; }
    .battle-versus {
      display: grid;
      grid-template-columns: 1fr auto 1fr;
      align-items: center;
      gap: 10px;
      margin: 2px 0 12px;
    }
    .battle-name {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-size: 14px;
      font-weight: 700;
      color: var(--tr-text-primary);
    }
    .battle-versus .battle-name:first-child { text-align: right; }
    .battle-vs {
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .16em;
      color: #b9a6ff;
      padding: 3px 8px;
      border-radius: var(--tr-radius-pill);
      border: 1px solid rgba(152, 116, 255, 0.32);
    }
    .battle-cap { text-transform: none; letter-spacing: 0; opacity: .8; }
    .battle-bet-row { display: flex; align-items: center; gap: 8px; }
    .battle-bet-input {
      flex: 1;
      min-width: 0;
      box-sizing: border-box;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(12, 17, 28, 0.9), rgba(8, 12, 20, 0.95));
      border-radius: 12px;
      padding: 9px 12px;
      color: var(--tr-text-primary);
      font: inherit;
      font-size: 15px;
      font-variant-numeric: tabular-nums;
    }
    .battle-bet-input:focus { outline: none; border-color: var(--panel-edge-hi); }
    .battle-unit { font-size: 12px; font-weight: 700; color: var(--tr-text-muted); }
    .battle-max-btn {
      padding: 8px 12px;
      border-radius: 10px;
      border: 1px solid rgba(255, 216, 120, 0.3);
      background: rgba(255, 216, 120, 0.08);
      color: #f1d9a0;
      font: inherit;
      font-size: 12px;
      font-weight: 700;
      cursor: pointer;
    }
    .battle-max-btn:disabled { opacity: .4; cursor: not-allowed; }
    .battle-slider { width: 100%; margin: 12px 0 2px; accent-color: #9874ff; }
    .battle-pot {
      margin: 8px 0 12px;
      padding: 10px 12px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      text-align: center;
      font-size: 13px;
      font-weight: 600;
      color: var(--tr-text-secondary);
    }
    .battle-pot.is-bet {
      color: #f4dfaa;
      border-color: rgba(255, 216, 120, 0.3);
      background: rgba(255, 216, 120, 0.06);
    }
    .battle-waiting { display: grid; justify-items: center; gap: 10px; margin: 8px 0 12px; }
    .battle-waiting[hidden] { display: none; }
    .battle-waiting-ring {
      width: 34px;
      height: 34px;
      border-radius: 50%;
      border: 3px solid rgba(152, 116, 255, 0.2);
      border-top-color: #9874ff;
      animation: battleSpin 900ms linear infinite;
    }
    @keyframes battleSpin { to { transform: rotate(360deg); } }
    .battle-waiting-text { margin: 0; font-size: 13px; color: var(--tr-text-secondary); text-align: center; }
    .battle-timer {
      width: 100%;
      height: 4px;
      border-radius: 4px;
      background: rgba(151, 184, 222, 0.12);
      overflow: hidden;
    }
    .battle-timer-fill {
      height: 100%;
      width: 100%;
      background: #9874ff;
      transition: width 250ms linear;
    }
    .battle-card .cp-confirm-btn:disabled { opacity: .5; cursor: not-allowed; }
    .battle-card .cp-confirm-btn[hidden] { display: none; }

    /* the invite to this player, in the corner and off the board */
    .battle-incoming {
      position: fixed;
      right: 18px;
      bottom: 18px;
      z-index: 1400;
      width: min(340px, calc(100vw - 32px));
      box-sizing: border-box;
      display: grid;
      gap: 10px;
      padding: 14px 16px;
      border-radius: var(--tr-radius-medium);
      border: 1px solid rgba(152, 116, 255, 0.34);
      background: linear-gradient(180deg, rgba(16, 22, 38, 0.98), rgba(9, 13, 24, 0.99));
      box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
      opacity: 0;
      transform: translateY(12px);
      transition: opacity 220ms ease, transform 260ms cubic-bezier(.2, .8, .2, 1);
    }
    .battle-incoming[hidden] { display: none; }
    .battle-incoming.is-in { opacity: 1; transform: translateY(0); }
    .battle-incoming-head { display: flex; align-items: center; gap: 12px; }
    .battle-incoming-icon {
      flex: 0 0 auto;
      width: 36px;
      height: 36px;
      display: grid;
      place-items: center;
      border-radius: 50%;
      color: #cdbfff;
      background: rgba(152, 116, 255, 0.16);
      border: 1px solid rgba(152, 116, 255, 0.32);
    }
    .battle-incoming-icon svg { width: 18px; height: 18px; }
    .battle-incoming-copy { min-width: 0; }
    .battle-incoming-title {
      font-size: 14px;
      font-weight: 700;
      color: var(--tr-text-primary);
      overflow-wrap: anywhere;
    }
    .battle-incoming-sub { font-size: 11.5px; color: var(--tr-text-muted); }
    .battle-incoming-bet {
      padding: 8px 10px;
      border-radius: 10px;
      border: 1px solid var(--panel-edge);
      font-size: 12.5px;
      font-weight: 600;
      text-align: center;
      color: var(--tr-text-secondary);
    }
    .battle-incoming-bet.is-bet {
      color: #f4dfaa;
      border-color: rgba(255, 216, 120, 0.3);
      background: rgba(255, 216, 120, 0.06);
    }
    .battle-incoming-msg { margin: 0; font-size: 11.5px; color: #f0a3ad; }
    .battle-incoming-msg:empty { display: none; }
    .battle-incoming-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
    .battle-incoming-actions button { width: 100%; }
    .battle-incoming-actions button:disabled { opacity: .5; cursor: not-allowed; }

    .pi-score { display: grid; justify-items: center; gap: 4px; }
    .pi-score-label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .pi-rank {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 20px;
      font-weight: 800;
      line-height: 1;
      color: rgba(196, 220, 244, 0.95);
    }
    .pi-stars { display: flex; align-items: center; gap: 3px; height: 6px; }
    .pi-pip {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: rgba(151, 184, 222, 0.22);
    }
    /* the same gold the list and the match chip use, and the same
       violet at the ceiling, so one account reads the same in all
       three places */
    .pi-pip.is-on { background: #ffc860; }
    .pi-stars.is-capped { gap: 4px; height: auto; }
    .pi-stars.is-capped .pi-pip.is-on { background: #b79cff; }
    .pi-star-total {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 10px;
      font-weight: 700;
      color: #b79cff;
    }

    /* ---- the totals ---- */
    /* Six columns so the row can be split two ways: earnings and
       energy take half each and lead the grid, being the two figures
       that are true right now, and the seven lifetime counts under
       them run three to a row. */
    /* ---- the holdings colour scale --------------------------------
       One ramp, shared by every place a $TETRA figure is shown: the
       player card headline below, the account chip in the corner and
       the holdings row in the profile panel. Carried on
       data-holdings-tier, written by applyHoldingsTier, and the
       consumers only ever read the three tokens -- none of them
       knows what the colours are.

       Blue at the bottom of the board, gold at the top. It runs
       through a pale blue and a silver on the way, because that is
       the route between them that reads as climbing rather than as
       changing subject.

       The tiers are POSITIONAL -- where the account sits in the
       ordered list of holders -- not a table of token amounts. Fixed
       thresholds would be wrong the first time the supply or the
       price moved, and a single whale would flatten every other
       holder onto the bottom rung.

       Each step raises the edge, the wash and the ink, and the top
       adds one slow sheen. No bloom, no lit border, nothing that
       pulses: gold should read as expensive, which is the opposite
       of loud. */
    [data-holdings-tier] {
      --hold-ink: var(--tr-text-primary);
      --hold-edge: var(--panel-edge);
      --hold-wash: transparent;
      transition: color .35s ease, border-color .35s ease, background .35s ease;
    }
    /* off the scale: this account holds nothing */
    [data-holdings-tier="0"] {
      --hold-ink: rgba(151, 184, 222, 0.42);
      --hold-edge: var(--panel-edge);
      --hold-wash: transparent;
    }
    [data-holdings-tier="1"] {
      --hold-ink: #5fd4ff;
      --hold-edge: rgba(95, 212, 255, 0.28);
      --hold-wash: rgba(95, 212, 255, 0.07);
    }
    [data-holdings-tier="2"] {
      --hold-ink: #9fd9ea;
      --hold-edge: rgba(159, 217, 234, 0.28);
      --hold-wash: rgba(159, 217, 234, 0.07);
    }
    [data-holdings-tier="3"] {
      --hold-ink: #d7dce3;
      --hold-edge: rgba(215, 220, 227, 0.30);
      --hold-wash: rgba(215, 220, 227, 0.08);
    }
    [data-holdings-tier="4"] {
      --hold-ink: #f0cf84;
      --hold-edge: rgba(226, 183, 92, 0.42);
      --hold-wash: rgba(226, 183, 92, 0.13);
    }
    [data-holdings-tier="5"] {
      --hold-ink: #ffdf9a;
      --hold-edge: rgba(240, 205, 110, 0.55);
      --hold-wash: rgba(240, 205, 110, 0.18);
    }
    /* ---- the holdings headline ------------------------------------
       The panel's largest figure, across its full width, because it
       is the one number on this card that says where an account sits
       in the token rather than in the ladder. */
    .pi-holdings {
      position: relative;
      overflow: hidden;
      margin-top: 14px;
      padding: 15px 16px 16px;
      border-radius: 16px;
      border: 1px solid var(--hold-edge);
      background:
        linear-gradient(180deg, var(--hold-wash), transparent 70%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.96), rgba(9, 13, 22, 0.99));
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
      /* the card's full width, so centred like every other tile that
         takes a whole row */
      text-align: center;
    }
    /* The one row on this tile that is not centred: the label sits
       left and the standing sits right, at opposite ends, so the two
       read as a caption and a placing rather than as one phrase. The
       figure below them stays centred. */
    .pi-holdings-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      margin-bottom: 8px;
      text-align: left;
    }
    .pi-holdings-label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    /* The standing, as a pill rather than another line of text: it is
       a different kind of fact from the amount beside it. */
    .pi-holdings-rank {
      flex: 0 0 auto;
      padding: 3px 9px;
      border-radius: 999px;
      border: 1px solid var(--hold-edge);
      background: var(--hold-wash);
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .06em;
      white-space: nowrap;
      color: var(--hold-ink);
    }
    .pi-holdings-rank[hidden] { display: none; }
    /* Under the entry price: a statement, not a placing, so it drops
       the scale's colour entirely rather than wearing the bottom
       rung of it. */
    .pi-holdings-rank.is-ineligible {
      border-color: var(--panel-edge);
      background: rgba(151, 184, 222, 0.06);
      color: rgba(151, 184, 222, 0.55);
      font-weight: 700;
      letter-spacing: .04em;
      text-transform: none;
    }
    .pi-holdings-value {
      display: flex;
      align-items: baseline;
      justify-content: center;
      gap: 8px;
      min-width: 0;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      /* Smaller than it was, because the figure got longer. The card
         shows the grouped amount now rather than "10M", so this has
         to seat about thirteen characters plus the unit inside a
         444px tile -- at the old 52px ceiling a billion-token holder
         ran off the end and was ellipsed. */
      font-size: clamp(24px, 7vw, 44px);
      font-weight: 800;
      line-height: 1;
      letter-spacing: -.02em;
      color: var(--hold-ink);
    }
    .pi-holdings-value > span:first-child {
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .pi-holdings-unit {
      flex: 0 0 auto;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: .08em;
      color: var(--tr-text-muted);
    }
    /* The top of the board only, and once every eight seconds. A
       sheen that ran constantly would be an animation nobody can look
       away from on a panel people read. */
    .pi-holdings[data-holdings-tier="5"]::after {
      content: "";
      position: absolute;
      top: 0;
      bottom: 0;
      left: -60%;
      width: 45%;
      pointer-events: none;
      background: linear-gradient(105deg,
        transparent, rgba(255, 233, 180, 0.12), transparent);
      animation: piGoldSheen 8s ease-in-out infinite;
    }
    @keyframes piGoldSheen {
      0%, 72% { transform: translateX(0); opacity: 0; }
      76% { opacity: 1; }
      96%, 100% { transform: translateX(360%); opacity: 0; }
    }
    @media (prefers-reduced-motion: reduce) {
      .pi-holdings[data-holdings-tier="5"]::after { animation: none; }
    }
    /* The same scale on the account chip in the corner and on the
       holdings row in the profile panel, so one account is the same
       colour wherever its holding is shown. Only the ink and the
       edge here: those two are small, and washing their backgrounds
       as well would make a corner of the screen change colour every
       time the figure refreshed. */
    #holdingsChip[data-holdings-tier] {
      border-color: var(--hold-edge);
    }
    #holdingsChip[data-holdings-tier] .holdings-value,
    #holdingsChip[data-holdings-tier] .holdings-wallet {
      color: var(--hold-ink);
    }
    /* The tile's own colour, not the icon's: .up-icon strokes in
       currentColor and .up-stat-holdings has always set the block, so
       taking it here puts the icon on the scale without naming it. */
    .up-stat[data-holdings-tier] {
      border-color: var(--hold-edge);
      color: var(--hold-ink);
    }
    .up-stat[data-holdings-tier] .up-stat-value { color: var(--hold-ink); }
    /* A rule with the section's name sitting in it. What follows is
       not the match record above it, and without this the spin
       figures read as four more match statistics. */
    .pi-section-rule {
      display: flex;
      align-items: center;
      gap: 10px;
      margin: 16px 0 2px;
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .pi-section-rule::before,
    .pi-section-rule::after {
      content: "";
      flex: 1 1 auto;
      height: 1px;
      background: var(--panel-edge);
    }
    .pi-section-rule span { flex: 0 0 auto; }
    .pi-grid {
      margin-top: 14px;
      display: grid;
      grid-template-columns: repeat(6, minmax(0, 1fr));
      gap: 9px;
    }
    /* The hidden attribute needs saying again here.

       A browser hides [hidden] from its own stylesheet, and any
       author rule setting display beats it -- so `display: grid`
       above quietly cancelled it, and setting .hidden on the spin
       grid from script did nothing at all. Every block in this card
       that is toggled that way needs its own line. */
    .pi-grid[hidden],
    .pi-section-rule[hidden],
    .pi-history-empty[hidden] { display: none; }
    .pi-stat {
      grid-column: span 2;
      display: grid;
      align-content: start;
      gap: 6px;
      padding: 11px 12px 12px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.96), rgba(9, 13, 22, 0.99));
      min-width: 0;
    }
    /* The width modifiers are qualified by the grid so they outweigh
       .pi-stat rather than merely following it.

       This is not tidiness. They are plain class selectors, the same
       weight as .pi-stat, so whichever is declared LAST wins -- and
       .pi-stat-wide had sat above .pi-stat since it was written,
       which meant Earnings and Energy were never the half-row tiles
       they were meant to be, and .pi-stat-full inherited the same
       fate. The narrow breakpoint below then re-declares .pi-stat,
       putting it last again, so ordering alone would have broken
       these a second time at 420px. Specificity settles it in both
       places at once. */
    .pi-grid > .pi-stat-wide { grid-column: span 3; }
    /* Centred, and every full-width tile on this card is. A figure
       alone on a whole row reads as shoved into the corner when it
       is left aligned: there is no second column beside it for the
       eye to line it up against. */
    .pi-grid > .pi-stat-full {
      grid-column: span 6;
      text-align: center;
    }
    .pi-grid > .pi-stat-full .pi-stat-value { justify-content: center; }
    /* A full-width tile carrying the figure its row adds up to. Not
       as large as the holdings headline -- that one is the card's
       subject and this is a section's -- but clearly above the
       counts beside it. */
    .pi-grid > .pi-stat-headline { padding: 13px 16px 15px; }
    .pi-stat-headline .pi-stat-value { font-size: clamp(24px, 6vw, 34px); }
    .pi-stat-headline .pi-stat-unit { font-size: 11px; }
    .pi-stat-label {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .pi-stat-value {
      display: flex;
      align-items: baseline;
      gap: 5px;
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 18px;
      font-weight: 800;
      line-height: 1.1;
      color: var(--tr-text-primary);
      min-width: 0;
    }
    .pi-stat-value > span:first-child {
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .pi-stat-unit {
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .06em;
      color: var(--tr-text-muted);
      flex: 0 0 auto;
    }
    /* Two tints rather than two colours: a win and a loss are the one
       pair here worth telling apart at a glance, and they stay well
       short of the accent the rank and the pips carry. */
    .pi-value-win { color: #8fe0b0; }
    .pi-value-loss { color: #e8a0a8; }
    /* A placeholder rather than a figure -- see refreshPlayerInspect.
       Quieter than the label above it, so the eye passes over it
       instead of stopping to read a value that is not there. */
    .pi-stat-value.is-empty { color: rgba(151, 184, 222, 0.38); }
    .pi-energy-track {
      height: 5px;
      border-radius: 999px;
      overflow: hidden;
      background: rgba(151, 184, 222, 0.14);
    }
    .pi-energy-fill {
      height: 100%;
      width: 0%;
      border-radius: 999px;
      background: linear-gradient(90deg, rgba(95, 212, 255, 0.75), rgba(143, 224, 176, 0.8));
      transition: width var(--tr-transition-standard);
    }
    /* ---- recent matches ---- */
    .pi-history { margin-top: 16px; }
    .pi-history-head {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
      margin-bottom: 8px;
    }
    /* Capped and scrolling rather than growing the card: twenty rows
       would make the panel taller than most screens, and the totals
       above them are what the card is for. */
    .pi-history-list {
      display: grid;
      gap: 6px;
      max-height: 218px;
      overflow-y: auto;
      padding-right: 4px;
    }
    .pi-history-list::-webkit-scrollbar { width: 6px; }
    .pi-history-list::-webkit-scrollbar-track { background: transparent; }
    .pi-history-list::-webkit-scrollbar-thumb {
      border-radius: 999px;
      background: rgba(151, 184, 222, 0.18);
    }
    .pi-history-empty {
      padding: 14px 12px;
      border-radius: 12px;
      border: 1px dashed var(--panel-edge);
      font-size: 11px;
      color: rgba(151, 184, 222, 0.5);
      text-align: center;
    }
    .pi-history-row {
      display: grid;
      grid-template-columns: 46px minmax(0, 1fr) auto;
      align-items: center;
      gap: 10px;
      padding: 8px 10px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.96), rgba(9, 13, 22, 0.99));
    }
    /* The result is the first thing read down the column, so it
       carries the only colour in the row -- the same two tints the
       win and loss totals above use. */
    .pi-history-result {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .08em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .pi-history-row.is-win .pi-history-result { color: #8fe0b0; }
    .pi-history-row.is-loss .pi-history-result { color: #e8a0a8; }
    .pi-history-meta { min-width: 0; display: grid; gap: 2px; }
    .pi-history-vs {
      font-size: 12px;
      font-weight: 700;
      color: #e4eef8;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .pi-history-line {
      font-size: 10px;
      color: rgba(151, 184, 222, 0.55);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .pi-replay-btn {
      flex: 0 0 auto;
      padding: 5px 11px;
      border-radius: 9px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      color: rgba(207, 228, 245, 0.78);
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .pi-replay-btn::before { content: none; }
    .pi-replay-btn:hover {
      transform: none;
      filter: none;
      color: var(--tr-text-primary);
      border-color: var(--tr-border-cyan);
    }
    /* Still a chip so the column does not ragged itself when some
       rows have a button and some do not, but plainly not a control. */
    .pi-replay-btn.is-gone {
      cursor: default;
      color: rgba(151, 184, 222, 0.34);
      border-style: dashed;
    }
    .pi-replay-btn.is-gone:hover { color: rgba(151, 184, 222, 0.34); border-color: var(--panel-edge); }

    .pi-note {
      margin: 14px 0 0;
      font-size: 10px;
      letter-spacing: .04em;
      color: rgba(151, 184, 222, 0.5);
      text-align: center;
    }
    /* Three to a row is too tight for the counts once the card is at
       its narrowest, so they drop to two and the half-row tiles take
       a full row each. */
    @media (max-width: 420px) {
      .pi-stat { grid-column: span 3; }
      /* full width here too, so it centres here too */
      .pi-grid > .pi-stat-wide {
        grid-column: span 6;
        text-align: center;
      }
      .pi-grid > .pi-stat-wide .pi-stat-value { justify-content: center; }
      .pi-stat-value { font-size: 16px; }
      /* the rank pill wraps under the label rather than squeezing the
         headline figure it sits beside */
      .pi-holdings-head { flex-wrap: wrap; gap: 6px; }
    }

    /* =========================================================
       REPLAY -- a finished match, played back

       The spectator's two seats in a card of their own, with a
       transport bar under them. The seats carry the spectator's own
       classes, so everything about how a board looks is inherited
       rather than restated here; what this adds is the shell and the
       controls.

       Above the card it opens from, and above the spectator: a
       replay can be started while watching a live round, and it is
       the thing in front.
       ========================================================= */
    .replay-panel {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, .72);
      display: grid;
      place-items: center;
      z-index: 130;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease;
      padding: 16px;
    }
    .replay-panel.show { opacity: 1; pointer-events: auto; }
    .replay-card {
      width: min(96vw, 1000px);
      max-height: min(92vh, 900px);
      border-radius: var(--tr-radius-large);
      padding: 16px 18px 14px;
      color: var(--tr-text-primary);
      display: grid;
      /* head, boards, transport, note -- the boards take what is
         left, which is what lets the card sit inside the viewport
         on a laptop instead of running off the bottom of it */
      grid-template-rows: auto minmax(0, 1fr) auto auto;
      gap: 12px;
    }
    .replay-head {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: center;
      gap: 12px;
    }
    .replay-heading { min-width: 0; display: grid; gap: 2px; }
    .replay-title {
      font-size: 14px;
      font-weight: 800;
      letter-spacing: .04em;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .replay-subtitle {
      font-size: 10px;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .replay-card > .replay-head > .close-btn {
      position: relative;
      top: auto;
      right: auto;
      width: 32px;
      height: 32px;
      padding: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 17px;
      line-height: 1;
      cursor: pointer;
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--panel-edge);
      color: rgba(207, 228, 245, 0.7);
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .replay-card > .replay-head > .close-btn::before { content: none; }
    .replay-card > .replay-head > .close-btn:hover {
      transform: none;
      filter: none;
      color: #ffd2d6;
      border-color: rgba(255, 111, 130, 0.36);
    }

    /* ---- transport ---- */
    .replay-transport {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr) auto;
      align-items: center;
      gap: 12px;
      padding: 9px 12px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.96), rgba(9, 13, 22, 0.99));
    }
    .replay-play {
      min-width: 74px;
      padding: 7px 14px;
      border-radius: 9px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.04);
      color: rgba(207, 228, 245, 0.85);
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
      cursor: pointer;
      transition: color var(--tr-transition-standard), border-color var(--tr-transition-standard);
    }
    .replay-play::before { content: none; }
    .replay-play:hover {
      transform: none;
      filter: none;
      color: var(--tr-text-primary);
      border-color: var(--tr-border-cyan);
    }
    .replay-play.is-playing { border-color: var(--tr-border-cyan); color: var(--tr-text-primary); }
    /* The bar is drawn from scratch on both engines: a default range
       input brings its own platform look, which is the one control
       on screen that would not belong to this game. */
    .replay-scrub {
      -webkit-appearance: none;
      appearance: none;
      width: 100%;
      height: 6px;
      border-radius: 999px;
      background: rgba(151, 184, 222, 0.18);
      outline: none;
      cursor: pointer;
    }
    .replay-scrub::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 14px;
      height: 14px;
      border-radius: 50%;
      background: #cfe4f5;
      border: 1px solid rgba(9, 13, 22, 0.8);
      cursor: pointer;
    }
    .replay-scrub::-moz-range-thumb {
      width: 14px;
      height: 14px;
      border: 1px solid rgba(9, 13, 22, 0.8);
      border-radius: 50%;
      background: #cfe4f5;
      cursor: pointer;
    }
    .replay-time {
      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
      font-size: 11px;
      color: rgba(151, 184, 222, 0.72);
      white-space: nowrap;
    }
    .replay-note {
      margin: 0;
      font-size: 10px;
      letter-spacing: .04em;
      color: rgba(151, 184, 222, 0.45);
      text-align: center;
    }
    /* One board at a time once there is no room for two side by side,
       the same way the spectator stacks. */
    @media (max-width: 860px) {
      .replay-card { padding: 14px 14px 12px; }
      .replay-transport { grid-template-columns: auto minmax(0, 1fr); gap: 9px; }
      .replay-time { grid-column: 1 / -1; text-align: center; }
    }

    /* ---- save ---- */
    /* Logout beside Save: the rarer action on the left, in a muted red,
       Save taking the rest of the row. The note (why logging out is
       unavailable mid-match) spans under both when it shows. */
    .up-footer {
      margin-top: 18px;
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      gap: 10px;
    }
    .up-footer .up-logout-btn { grid-column: 1; grid-row: 1; }
    .up-footer .up-save-btn { grid-column: 2; grid-row: 1; }
    .up-logout-btn {
      padding: 13px 18px;
      border-radius: 12px;
      border: 1px solid rgba(255, 111, 130, 0.36);
      background: rgba(255, 111, 130, 0.08);
      color: #ff8f9c;
      font-size: 12px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      cursor: pointer;
      transition:
        border-color var(--tr-transition-standard),
        color var(--tr-transition-standard),
        background var(--tr-transition-standard);
    }
    .up-logout-btn:hover:not(:disabled) {
      border-color: rgba(255, 111, 130, 0.55);
      background: rgba(255, 111, 130, 0.14);
      color: #ffd2d6;
    }
    .up-logout-btn:focus-visible { outline: 2px solid var(--tr-violet-bright); outline-offset: 2px; }
    .up-logout-btn:disabled { opacity: .55; cursor: wait; }
    .up-logout-note {
      grid-column: 1 / -1;
      grid-row: 2;
      margin: 0;
      font-size: 11px;
      line-height: 1.5;
      color: #e8a0a8;
    }
    .up-logout-note[hidden] { display: none; }
    .up-save-btn {
      width: 100%;
      padding: 13px 18px;
      border-radius: 12px;
      cursor: pointer;
      border: 1px solid rgba(152, 116, 255, 0.28);
      background:
        radial-gradient(120% 150% at 50% 0%, rgba(152, 116, 255, 0.13), transparent 70%),
        linear-gradient(180deg, rgba(25, 29, 50, 0.99), rgba(12, 15, 27, 1));
      color: #e4dcff;
      font-size: 12px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.05),
        0 8px 20px rgba(0, 0, 0, 0.3);
      transition:
        border-color var(--tr-transition-standard),
        color var(--tr-transition-standard);
    }
    .up-save-btn:hover {
      border-color: rgba(152, 116, 255, 0.5);
      color: #f3eeff;
    }
    .up-save-btn:active { transform: translateY(1px); }

    @media (max-width: 520px) {
      .up-stats { grid-template-columns: 1fr; }
      .up-actions { grid-template-columns: 1fr; }
      .up-payout-row { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, .8fr) 54px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .user-profile-panel,
      .up-energy-fill { transition: none; }
      /* the chat slides sideways with the players panel; this stops that one
         movement without disabling the fades. It has to sit after the rules that
         declare the shorthand, or the shorthand would win on source order. */
      .game-chat-launcher,
      .game-chat-panel { transition-duration: 0s; }
    }

    /* =========================================================
       MATCH FOUND: VERSUS INTRO

       One element in two states rather than two elements crossfading. It is
       fixed, horizontally centred and docked under the gap between the boards;
       the intro lifts it to the middle of the screen at scale over a full
       screen backdrop, holds, then flies it back to the dock. Both states share
       the same translateX(-50%) and the element's own size never changes, so
       the whole trip is a single transform interpolation and the landing lines
       up with the CSS resting position exactly.
       ========================================================= */
    .match-versus-backdrop {
      position: fixed;
      inset: 0;
      z-index: 118;
      background: rgba(2, 5, 11, 0.84);
      backdrop-filter: blur(7px) saturate(0.9);
      -webkit-backdrop-filter: blur(7px) saturate(0.9);
      opacity: 0;
      pointer-events: none;
      transition: opacity .3s ease;
    }
    .match-versus-backdrop.is-visible { opacity: 1; }
    .match-versus {
      position: fixed;
      left: 50%;
      bottom: calc(86px + env(safe-area-inset-bottom, 0px));
      transform: translateX(-50%);
      transform-origin: 50% 50%;
      z-index: 44;
      display: none;
      align-items: center;
      gap: 14px;
      padding: 9px 15px;
      border-radius: 16px;
      border: 1px solid var(--panel-edge);
      background:
        radial-gradient(120% 90% at 50% 0%, rgba(95, 212, 255, 0.05), transparent 64%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.97), rgba(9, 13, 22, 0.99));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        0 14px 34px rgba(0, 0, 0, 0.42);
      white-space: nowrap;
      pointer-events: none;
    }
    .match-versus.is-visible { display: flex; }
    /* above the backdrop only while the intro is running */
    .match-versus.is-intro { z-index: 119; }

    /* ---- the prize at stake ---------------------------------------
       Same two states as the strip, and the same z-index rule: over
       the backdrop while the intro runs, back down to the strip's
       level once it is docked. `top` is written from script, so the
       resting spot is measured against the timer and the HOLD/NEXT
       columns rather than being a number that happens to fit one
       screen. Every transform keeps the translateX(-50%) the
       resting position depends on. */
    .match-prize {
      position: fixed;
      left: 50%;
      top: 84px;
      transform: translateX(-50%);
      transform-origin: 50% 50%;
      z-index: 44;
      display: none;
      align-items: center;
      gap: 9px;
      padding: 8px 15px;
      border-radius: 999px;
      border: 1px solid var(--panel-edge);
      background:
        radial-gradient(120% 90% at 50% 0%, rgba(153, 69, 255, 0.07), transparent 66%),
        linear-gradient(180deg, rgba(18, 25, 38, 0.97), rgba(9, 13, 22, 0.99));
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.035),
        0 14px 34px rgba(0, 0, 0, 0.42);
      white-space: nowrap;
      pointer-events: none;
    }
    .match-prize.is-visible { display: flex; }
    .match-prize.is-intro { z-index: 119; }
    /* The coins that fly to the account chip when a win is paid.
       Loose coins, not the panel: each one is its own element,
       positioned in page pixels and animated on its own clock, so
       they leave scattered and arrive as a handful. Body level and
       above everything, because they set off from the result screen,
       which is an opaque panel at 140. */
    .prize-coin {
      position: fixed;
      z-index: 200;
      width: 22px;
      height: 22px;
      pointer-events: none;
      filter: drop-shadow(0 4px 10px rgba(153, 69, 255, 0.45));
    }
    .match-prize-coin { width: 17px; height: 17px; display: block; flex: 0 0 auto; }
    /* a battle's coin says what it is: the two bets, not the pool */
    .match-prize-label { display: none; }
    .match-prize.is-battle .match-prize-label {
      display: block;
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: rgba(201, 178, 122, 0.9);
    }
    .match-prize-amount {
      font-size: 14px;
      font-weight: 900;
      letter-spacing: .03em;
      color: var(--tr-text-primary);
      font-variant-numeric: tabular-nums;
    }
    /* The drop. It overshoots and settles rather than stopping dead,
       which is what makes it read as something falling onto the pile
       rather than an element appearing. */
    .match-prize.is-dropping {
      animation: mpDrop .62s cubic-bezier(.2, .9, .25, 1) both;
    }
    @keyframes mpDrop {
      0% { transform: translateX(-50%) translateY(-110px) scale(.66); opacity: 0; }
      58% { transform: translateX(-50%) translateY(7px) scale(1.09); opacity: 1; }
      100% { transform: translateX(-50%) translateY(0) scale(1); opacity: 1; }
    }

    /* A win landing on the earnings nub. Solana purple, and a pulse
       that decays back to the nub's own colours -- it is bright for
       as long as the coins are arriving and the figure is counting
       up, then goes back to being a nub rather than wearing a
       permanent ring. */
    .earnings-chip.is-earning {
      animation: earningsEarn 1.8s cubic-bezier(.22, .75, .3, 1) both;
    }
    @keyframes earningsEarn {
      0% {
        border-color: var(--tr-border-subtle);
        box-shadow:
          var(--tr-inset-highlight),
          0 10px 24px rgba(0, 0, 0, 0.38);
      }
      16% {
        border-color: rgba(153, 69, 255, 0.72);
        box-shadow:
          var(--tr-inset-highlight),
          0 10px 24px rgba(0, 0, 0, 0.38),
          0 0 32px rgba(153, 69, 255, 0.42);
      }
      100% {
        border-color: var(--tr-border-subtle);
        box-shadow:
          var(--tr-inset-highlight),
          0 10px 24px rgba(0, 0, 0, 0.38);
      }
    }
    /* The figure itself swells and takes the purple while it runs, so
       the eye is pulled to the exact thing that is changing rather
       than to the nub around it, then hands it back.

       This has to live on the value and NOT on .earnings-chip: the
       three responsive blocks scale the nub with transform, and an
       animation beats a plain declaration, so a keyframe touching
       transform up here would snap the nub back to full size halfway
       through a win on every small screen. The value carries no
       responsive transform of its own, so it is free. */
    .earnings-value.is-counting {
      animation: earningsValuePop 1.6s cubic-bezier(.2, .8, .3, 1) both;
    }
    @keyframes earningsValuePop {
      0% { transform: scale(1); color: #f5f4ef; }
      14% { transform: scale(1.22); color: rgb(202, 158, 255); }
      68% { transform: scale(1.1); color: rgb(202, 158, 255); }
      100% { transform: scale(1); color: #f5f4ef; }
    }
    /* The gain rises off the side and fades. Slow enough to be read
       at a glance -- it is the part that actually says how much. */
    .earnings-delta.is-rising {
      animation: earningsDeltaRise 1.9s cubic-bezier(.2, .7, .3, 1) both;
    }
    @keyframes earningsDeltaRise {
      0% { opacity: 0; transform: translateY(-20%) scale(.7); }
      15% { opacity: 1; transform: translateY(-58%) scale(1.06); }
      30% { opacity: 1; transform: translateY(-50%) scale(1); }
      66% { opacity: 1; transform: translateY(-96%) scale(1); }
      100% { opacity: 0; transform: translateY(-170%) scale(.94); }
    }
    .mv-player {
      display: flex;
      align-items: center;
      gap: 10px;
      min-width: 0;
    }
    .mv-flag {
      display: block;
      flex: 0 0 auto;
      width: 30px;
      height: 21px;
      border-radius: 4px;
      overflow: hidden;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.05);
    }
    .mv-flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
    /* Nickname Flag VS Flag Nickname: the flags sit inboard against the VS and
       the names run outward, so each rank line hangs under its name on the
       side facing the middle. */
    .mv-meta { display: grid; gap: 2px; min-width: 0; }
    .mv-player-mine .mv-meta { justify-items: end; text-align: right; }
    .mv-player-theirs .mv-meta { justify-items: start; text-align: left; }
    .mv-name {
      max-width: 152px;
      overflow: hidden;
      text-overflow: ellipsis;
      font-size: 14px;
      font-weight: 900;
      letter-spacing: .01em;
      line-height: 1.1;
      color: #eaf4ff;
    }
    .mv-rank {
      font-size: 9px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      line-height: 1;
      color: rgba(151, 184, 222, 0.6);
    }
    .mv-vs {
      flex: 0 0 auto;
      padding: 0 2px;
      font-size: 13px;
      font-weight: 900;
      letter-spacing: .12em;
      color: rgba(183, 156, 255, 0.92);
    }
    .match-versus.is-intro .mv-player-mine { animation: mvEnterLeft .5s cubic-bezier(.2, .8, .3, 1) both; }
    .match-versus.is-intro .mv-player-theirs { animation: mvEnterRight .5s cubic-bezier(.2, .8, .3, 1) both; }
    .match-versus.is-intro .mv-vs { animation: mvPop .46s cubic-bezier(.2, 1.35, .4, 1) .1s both; }
    @keyframes mvEnterLeft {
      from { opacity: 0; transform: translateX(-26px); }
      to { opacity: 1; transform: none; }
    }
    @keyframes mvEnterRight {
      from { opacity: 0; transform: translateX(26px); }
      to { opacity: 1; transform: none; }
    }
    @keyframes mvPop {
      from { opacity: 0; transform: scale(.4); }
      to { opacity: 1; transform: scale(1); }
    }
    @media (max-width: 1300px), (max-height: 900px) {
      .match-versus { bottom: calc(74px + env(safe-area-inset-bottom, 0px)); }
    }
    @media (orientation: portrait), (max-width: 900px) {
      .match-versus {
        gap: 10px;
        padding: 8px 12px;
        bottom: calc(96px + env(safe-area-inset-bottom, 0px));
      }
      .mv-name { max-width: 92px; font-size: 12px; }
      .mv-flag { width: 24px; height: 17px; }
      .match-prize { gap: 7px; padding: 6px 12px; }
      .match-prize-coin { width: 15px; height: 15px; }
      .match-prize-amount { font-size: 12px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .match-versus-backdrop { transition: none; }
      .match-versus.is-intro .mv-player-mine,
      .match-versus.is-intro .mv-player-theirs,
      .match-versus.is-intro .mv-vs { animation: none; }
      /* The coin still appears and the nub still lights up and
         counts; they just do not travel to get there. */
      .match-prize.is-dropping { animation: none; }
      .earnings-chip.is-earning { animation: none; border-color: rgba(153, 69, 255, 0.72); }
      /* The gain is the part that says how much, so it is still shown
         -- it simply stands there for its two seconds instead of
         rising, and the figure takes the colour without the swell. */
      .earnings-delta.is-rising { animation: none; opacity: 1; }
      .earnings-value.is-counting { animation: none; color: rgb(202, 158, 255); }
    }

    /* =========================================================
       MATCH RESULT

       The one screen in the game that is allowed to shout. Everything colour
       comes from a single --mr-accent, swapped by .is-win / .is-lose on the
       panel, so the verdict, the flash, the card edge and the winning column
       all move together rather than each carrying its own green and red.
       ========================================================= */
    .match-result {
      --mr-accent: rgba(151, 184, 222, 0.8);
      --mr-accent-soft: rgba(151, 184, 222, 0.14);
      position: fixed;
      inset: 0;
      z-index: 140;
      display: grid;
      place-items: center;
      padding: 20px;
      background: rgba(2, 5, 11, 0.86);
      backdrop-filter: blur(8px) saturate(0.9);
      -webkit-backdrop-filter: blur(8px) saturate(0.9);
      opacity: 0;
      pointer-events: none;
      transition: opacity .22s ease;
    }
    .match-result.show { opacity: 1; pointer-events: auto; }
    .match-result.is-win {
      --mr-accent: rgba(105, 214, 154, 0.92);
      --mr-accent-soft: rgba(105, 214, 154, 0.16);
    }
    .match-result.is-lose {
      --mr-accent: rgba(255, 118, 128, 0.92);
      --mr-accent-soft: rgba(255, 111, 130, 0.16);
    }
    /* the flash: one pulse of the accent across the whole screen, behind the
       card, rather than a colour the panel then keeps wearing */
    .mr-flash {
      position: absolute;
      inset: 0;
      pointer-events: none;
      opacity: 0;
      background:
        radial-gradient(90% 70% at 50% 50%, var(--mr-accent-soft), transparent 70%);
    }
    .match-result.show .mr-flash { animation: mrFlash 1.1s ease-out 1; }
    @keyframes mrFlash {
      0% { opacity: 0; }
      12% { opacity: 1; }
      100% { opacity: 0.25; }
    }
    /* Spacing is the whole design here. One gap value separates the four
       blocks, the stats sit on their own surface rather than between two
       hairlines, and the card is wide enough that two long nicknames and a
       VS are not fighting over the middle. */
    .mr-card {
      position: relative;
      width: min(94vw, 500px);
      max-height: 92vh;
      overflow-y: auto;
      display: grid;
      justify-items: center;
      gap: 26px;
      padding: 38px 34px 30px;
      border-radius: 22px;
      border: 1px solid var(--panel-edge);
      background:
        radial-gradient(130% 90% at 50% 0%, var(--mr-accent-soft), transparent 66%),
        var(--tr-panel-gradient);
      box-shadow: var(--tr-inset-highlight), var(--tr-shadow-panel);
    }
    .mr-card::before {
      content: "";
      position: absolute;
      top: 0;
      left: 18%;
      right: 18%;
      height: 1px;
      pointer-events: none;
      background: linear-gradient(90deg, transparent, var(--mr-accent), transparent);
      opacity: .6;
    }
    .mr-verdict {
      font-size: 34px;
      font-weight: 900;
      letter-spacing: .12em;
      line-height: 1.1;
      text-transform: uppercase;
      text-align: center;
      /* the tracking is on the right of the last letter too, so without this
         the word reads a touch left of centre */
      text-indent: .12em;
      color: var(--mr-accent);
    }
    .match-result.show .mr-verdict { animation: mrVerdictIn .5s cubic-bezier(.2, 1.3, .4, 1) both; }
    @keyframes mrVerdictIn {
      from { opacity: 0; transform: scale(.72); }
      to { opacity: 1; transform: scale(1); }
    }
    .mr-heads {
      display: grid;
      grid-template-columns: 1fr auto 1fr;
      align-items: center;
      gap: 16px;
      width: 100%;
    }
    .mr-head {
      display: flex;
      align-items: center;
      gap: 11px;
      min-width: 0;
    }
    /* the opponent's half mirrors so both names run toward the VS */
    .mr-head-theirs { flex-direction: row-reverse; }
    .mr-flag {
      display: block;
      flex: 0 0 auto;
      width: 28px;
      height: 19px;
      border-radius: 4px;
      overflow: hidden;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.05);
    }
    .mr-flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .mr-name {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-size: 14px;
      font-weight: 900;
      letter-spacing: .01em;
      color: #eaf4ff;
    }
    .mr-head-theirs .mr-name { text-align: right; }
    .mr-vs {
      font-size: 10px;
      font-weight: 900;
      letter-spacing: .18em;
      text-indent: .18em;
      color: rgba(183, 156, 255, 0.7);
    }
    /* Its own quiet surface rather than a pair of hairlines: the two rows
       were reading as one four-figure block, and a boundary you can see
       does more for that than extra gap alone. */
    .mr-stats {
      display: grid;
      grid-template-columns: 1fr auto 1fr;
      align-items: center;
      gap: 20px 16px;
      width: 100%;
      padding: 22px 20px;
      border-radius: 16px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.022);
    }
    .mr-value {
      font-size: 27px;
      font-weight: 900;
      line-height: 1;
      /* 63 against 1 should sit on the same axis */
      font-variant-numeric: tabular-nums;
      color: #f0f6fc;
      text-align: center;
    }
    /* whichever column won each row is the one that gets the accent */
    .mr-value.is-ahead { color: var(--mr-accent); }
    .mr-label {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .18em;
      text-indent: .18em;
      text-transform: uppercase;
      white-space: nowrap;
      text-align: center;
      color: rgba(151, 184, 222, 0.62);
    }
    /* What the round paid, on its own quiet surface under the stats
       rather than squeezed into the grid with them -- it is one
       figure about you, not a pair to compare. The accent is the
       card's own --mr-accent, so a win shows it in the winning
       colour and a loss shows a neutral zero without a second rule
       being needed. */
    .mr-reward {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
      width: 100%;
      padding: 14px 20px;
      border-radius: 14px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.022);
    }
    .mr-reward-label {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--tr-text-muted);
    }
    .mr-reward-value {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      font-size: 20px;
      font-weight: 900;
      line-height: 1;
      font-variant-numeric: tabular-nums;
      color: var(--mr-accent);
    }
    .mr-reward-coin { width: 17px; height: 17px; display: block; flex: 0 0 auto; }
    .mr-close-btn {
      width: 100%;
      margin-top: 2px;
      padding: 15px 18px;
      border-radius: 12px;
      cursor: pointer;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(20, 27, 42, 0.99), rgba(10, 14, 24, 1));
      color: #dff0ff;
      font-size: 12px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      transition: border-color var(--tr-transition-standard), color var(--tr-transition-standard);
    }
    .mr-close-btn:hover { border-color: var(--panel-edge-hi); color: #f3fbff; }
    .mr-close-btn:active { transform: translateY(1px); }
    /* the air comes out proportionally rather than the type shrinking into
       the same cramped box it started in */
    @media (max-width: 420px) {
      .mr-card { gap: 20px; padding: 30px 22px 24px; }
      .mr-verdict { font-size: 26px; }
      .mr-stats { gap: 16px 12px; padding: 18px 14px; }
      .mr-value { font-size: 22px; }
      .mr-name { font-size: 12px; }
      .mr-flag { width: 24px; height: 16px; }
      .mr-reward { padding: 12px 14px; }
      .mr-reward-value { font-size: 17px; }
    }
    /* a short viewport is the case where the card can actually overflow */
    @media (max-height: 560px) {
      .mr-card { gap: 16px; padding: 24px 24px 20px; }
      .mr-verdict { font-size: 24px; }
      .mr-stats { gap: 14px 12px; padding: 16px 16px; }
      .mr-reward { padding: 10px 14px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .match-result { transition: none; }
      .match-result.show .mr-flash,
      .match-result.show .mr-verdict { animation: none; }
      .mr-flash { opacity: .25; }
    }

    /* =========================================================
       MOBILE

       Last in the file on purpose. Everything above it has spent
       thousands of lines arranging two boards, four preview
       columns, a score row and a dock, across three scaled
       breakpoints; restating all of that inside a phone query
       would mean keeping two layouts honest forever. This layer
       instead states one thing -- what a phone shows -- and turns
       the rest off.

       .is-mobile is set from script rather than by the media query
       alone so that one condition drives both the stylesheet and
       the behaviour, and so the header can be measured.

       What stays: who is online, the chat, and a way into any live
       match. What goes: the game, and everything that only exists
       to serve it.
       ========================================================= */
    .mobile-shell { display: none; }

    /* Compact on purpose. The header is read once and then sits over
       the one thing a phone is here for, the list under it, so every
       line it takes is a row of players it hides. It was 215px tall
       on a 390px phone -- logo, a three-line notice, two count boxes
       and a hint -- and is about 130 now. The hint went: every live
       row carries its own Watch button, which says it better. */
    body.is-mobile .mobile-shell {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      z-index: 34;
      display: grid;
      gap: 7px;
      padding: calc(env(safe-area-inset-top, 0px) + 9px) 12px 9px;
      background: linear-gradient(180deg, rgba(10, 15, 24, 0.98), rgba(10, 15, 24, 0.92));
      border-bottom: 1px solid var(--panel-edge, #253244);
    }
    /* The official logo in place of the old spaced-out TETRARANK
       text. A fixed height, so the header is the same size whether or
       not the image has loaded by the time it is measured. The edges
       fade into the header rather than ending in a hard box, and
       'lighten' lets the art's own near-black background take the
       header's colour. */
    .mobile-shell-logo {
      display: block;
      height: 32px;
      width: auto;
      aspect-ratio: 560 / 320;
      mix-blend-mode: lighten;
      -webkit-mask-image:
        linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%),
        linear-gradient(to bottom, transparent 0, #000 12%, #000 86%, transparent 100%);
      -webkit-mask-composite: source-in;
      mask-image:
        linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%),
        linear-gradient(to bottom, transparent 0, #000 12%, #000 86%, transparent 100%);
      mask-composite: intersect;
    }
    .mobile-shell-notice {
      margin: 0;
      font-size: 11px;
      line-height: 1.45;
      color: var(--tr-text-muted, #8c9aab);
    }
    .mobile-shell-notice strong { color: #e0b872; font-weight: 700; }
    .mobile-shell-counts {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 6px;
    }
    .mobile-shell-count {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 8px;
      padding: 5px 10px;
      border-radius: 9px;
      border: 1px solid var(--panel-edge, #253244);
      background: rgba(6, 11, 21, 0.6);
    }
    .mobile-shell-count-label {
      font-size: 9px;
      font-weight: 700;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted, #8c9aab);
    }
    .mobile-shell-count-value {
      font-size: 13px;
      font-weight: 900;
      color: #dff4ff;
      font-variant-numeric: tabular-nums;
    }

    /* ---- the bottom of the screen, stated once ----

       Three fixed things want the bottom edge: the chat bar, the
       Daily Spin button and the contract strip. Each used to pick
       its own offset from the bottom in a different part of this
       file, which is how the strip -- 999 in the stacking order
       against the chat bar's 44 -- came to sit on 39px of a 45px
       button, with the Medium tile, the X tile and Copy CA taking
       the taps that landed on it.

       They are one stack here instead: an action row on the floor
       and the strip on top of it, each measured off the row below.
       The two rows hold single lines of fixed-size controls, so
       unlike the header they cannot grow a line and can be named
       rather than measured. */
    body.is-mobile {
      --mobile-gutter: 10px;
      --mobile-gap: 6px;
      --mobile-floor: calc(var(--mobile-gutter) + env(safe-area-inset-bottom, 0px));
      --mobile-action-h: 40px;
      /* min-height on the strip holds it to exactly this, so the
         space reserved above it is the space it takes */
      --mobile-ca-h: 36px;
      /* Daily Spin takes the short side of the action row; the chat
         bar carries a longer label and an unread badge */
      --mobile-split: 42%;
      --mobile-ca-bottom: calc(var(--mobile-floor) + var(--mobile-action-h) + var(--mobile-gap));
      --mobile-bottom-h: calc(var(--mobile-ca-bottom) + var(--mobile-ca-h) + 8px);
    }

    /* ---- the players list becomes the page ----

       It was display:none on every narrow screen, because there
       was no room for a 292px rail beside two boards. With the
       boards gone the room is the whole width, and the list is one
       of the three things a phone is here for -- so it is switched
       back on and stretched across. */
    body.is-mobile .players-panel {
      display: flex !important;
      top: calc(var(--mobile-shell-h, 130px) + 8px);
      left: var(--mobile-gutter);
      right: var(--mobile-gutter);
      bottom: var(--mobile-bottom-h);
      width: auto;
      max-width: none;
      transform: none;
      z-index: 32;
      border-radius: 12px;
    }
    /* The collapse tab hangs off the left edge of a panel that now
       reaches both edges, so there is nowhere for it to hang and
       nothing to collapse into. */
    body.is-mobile .players-panel-tab { display: none !important; }
    body.is-mobile .players-panel.collapsed { transform: none; }

    /* Denser rows. The desktop row is sized to sit beside two boards
       and be glanced at; here it is the page, and the measure that
       matters is how many players fit before a scroll. 50px rows
       showed seven on a 390x844 phone; these are 40 and show thirteen.
       Every part keeps its place in the row -- only the air goes. */
    body.is-mobile .players-panel-head { padding: 8px 12px 7px; }
    body.is-mobile .players-panel-title { font-size: 10px; letter-spacing: .18em; }
    body.is-mobile .players-panel-list { padding: 6px; gap: 3px; }
    body.is-mobile .players-row {
      grid-template-columns: 24px minmax(0, 1fr) 30px 8px;
      gap: 8px;
      padding: 6px 8px;
      border-radius: 9px;
    }
    body.is-mobile .players-row.is-inmatch { row-gap: 5px; }
    body.is-mobile .players-flag { width: 24px; height: 16px; border-radius: 3px; }
    body.is-mobile .players-meta { gap: 1px; }
    body.is-mobile .players-name { font-size: 12.5px; }
    body.is-mobile .players-address { font-size: 9.5px; }
    body.is-mobile .players-energy-cell { width: 24px; height: 8px; }
    body.is-mobile .players-status { width: 8px; height: 8px; }
    body.is-mobile .players-live-pill { padding: 2px 7px; font-size: 8.5px; }
    body.is-mobile .players-live-vs { font-size: 9.5px; }
    /* still a comfortable thumb target: it is the way into a match */
    body.is-mobile .players-watch { padding: 5px 13px; font-size: 10px; }

    /* ---- the action row: Daily Spin, then chat ---- */
    body.is-mobile .game-chat-launcher {
      left: calc(var(--mobile-split) + (var(--mobile-gap) / 2));
      right: var(--mobile-gutter);
      bottom: var(--mobile-floor);
      height: var(--mobile-action-h);
      padding: 0 12px;
      gap: 9px;
      justify-content: center;
      border-radius: 14px;
      font-size: 12px;
      transform: none;
      z-index: 44;
    }
    body.is-mobile .game-chat-launcher .chat-icon { width: 22px; height: 17px; }
    /* Daily Spin comes out of the side menu into the action row.
       The store and the change log stay off, with the whole bottom-
       left dock: there is no board here to buy perks for or set the
       music for, and the reward split is a wide table. script.js
       leaves the menu's desktop layout classes off on a phone, so
       these are the only rules placing it. */
    body.is-mobile .side-menu {
      left: var(--mobile-gutter);
      right: auto;
      top: auto;
      bottom: var(--mobile-floor);
      width: calc(var(--mobile-split) - var(--mobile-gutter) - (var(--mobile-gap) / 2));
      gap: 0;
      z-index: 44;
    }
    body.is-mobile .side-menu-store,
    body.is-mobile .side-menu-logs,
    /* the phone's logo is in its own header */
    body.is-mobile .side-menu-brand,
    body.is-mobile .bottom-left-dock { display: none !important; }
    /* Tetranomics stays reachable on a phone: the owner counts it among
       the pages that matter there. It shares the row with Daily Spin,
       one half each, in the same compact shape. */
    body.is-mobile .side-menu {
      flex-direction: row;
      gap: 8px;
    }
    body.is-mobile .side-menu-spin,
    body.is-mobile .side-menu-tetranomics {
      flex: 1 1 0;
      min-width: 0;
    }
    body.is-mobile .side-menu-tetranomics {
      display: flex;
      height: var(--mobile-action-h);
      min-height: 0;
      padding: 0 10px;
      align-items: center;
      justify-content: center;
      gap: 8px;
      border-radius: 14px;
    }
    body.is-mobile .side-menu-tetranomics .side-menu-sub { display: none; }
    /* two to a half-width row leaves each too narrow for its words, and
       "D..." / "T..." reads as broken; the icons are the label here, and
       the words stay for screen readers */
    body.is-mobile .side-menu-spin .side-menu-text,
    body.is-mobile .side-menu-tetranomics .side-menu-text {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip: rect(0 0 0 0);
      white-space: nowrap;
    }
    body.is-mobile .side-menu-spin .side-menu-icon,
    body.is-mobile .side-menu-spin .side-menu-icon svg,
    body.is-mobile .side-menu-tetranomics .side-menu-icon,
    body.is-mobile .side-menu-tetranomics .side-menu-icon svg { width: 26px !important; height: 26px !important; }
    body.is-mobile .side-menu-tetranomics .side-menu-icon,
    body.is-mobile .side-menu-tetranomics .side-menu-icon svg { width: 22px; height: 22px; }
    body.is-mobile .side-menu-spin {
      height: var(--mobile-action-h);
      min-height: 0;
      padding: 0 10px;
      justify-content: center;
      gap: 8px;
      border-radius: 14px;
    }
    body.is-mobile .side-menu-spin .side-menu-icon,
    body.is-mobile .side-menu-spin .side-menu-icon svg { width: 22px; height: 22px; }
    body.is-mobile .side-menu-spin .side-menu-sub { display: none; }
    body.is-mobile .side-menu-spin .side-menu-label { font-size: 10.5px; letter-spacing: .1em; }
    /* Turning this back on has to lose to live mode, which clears
       the screen of everything a viewer cannot act on and says so
       with !important of its own at the same specificity. Without
       the :not() this block would win on source order alone,
       purely for being last in the file. */
    body.is-mobile:not(.is-live-mode) .account-chip { display: flex !important; }

    /* ---- the contract strip, off the chat button ----

       Full width on the row above rather than a centred pill, so
       it reads as one of the bars rather than something dropped on
       top of them, and so the address gets the width it needs. */
    body.is-mobile .ca-strip {
      left: var(--mobile-gutter);
      right: var(--mobile-gutter);
      bottom: var(--mobile-ca-bottom);
      width: auto;
      max-width: none;
      transform: none;
      z-index: 43;
    }
    /* The compact sizing is stated here rather than left to the
       portrait query: the stack reserves --mobile-ca-h for this bar,
       and a phone on its side is still a phone. Left to the wide
       sizing it came out 52px against the 40 reserved and pushed
       its way into the players list. */
    body.is-mobile .ca-strip-inner {
      width: 100%;
      max-width: none;
      min-height: var(--mobile-ca-h);
      padding: 4px 6px;
      gap: 6px;
      border-radius: 11px;
    }
    body.is-mobile .ca-social-link { width: 24px; height: 24px; border-radius: 7px; }
    body.is-mobile .ca-medium-link svg { width: 14px; height: 14px; }
    body.is-mobile .ca-x-link svg { width: 11px; height: 11px; }
    /* The portrait rules further up take these down to 7px, which a
       phone renders as a smudge; this is the smallest that still reads. */
    body.is-mobile .ca-strip-label { font-size: 8.5px; letter-spacing: .14em; padding-left: 9px; margin-left: 1px; }
    body.is-mobile .ca-strip-value { font-size: 10px; padding: 4px 7px; border-radius: 7px; }
    body.is-mobile .ca-copy-btn {
      min-width: 66px;
      padding: 6px 8px;
      font-size: 8.5px;
      letter-spacing: .1em;
      border-radius: 8px;
    }

    /* ---- who you are, on the brand line ----

       The register/login panel now opens by itself on a phone the
       same as anywhere else, and it can still be waved away -- so
       there has to be a way back to it. This chip is that way in,
       and the only thing on the page that says which account the
       room is seeing. The brand line is given a height and a right
       margin to hold it. */
    body.is-mobile .account-chip {
      top: calc(env(safe-area-inset-top, 0px) + 9px) !important;
      /* the chips are pinned hard to the left further up the sheet,
         with !important on both sides, so both have to be undone */
      left: auto !important;
      right: 12px !important;
      /* Sized, not scaled. It used to be the desktop chip at 75%,
         which took the address down to six pixels of type; a chip
         drawn at its real size is smaller and reads better. */
      transform: none;
      z-index: 40;
      /* the compact chip, stated for both orientations for the same
         reason as the strip: it has to fit the brand line either way */
      width: auto;
      max-width: 150px;
      height: 36px;
      gap: 7px;
      padding: 0 11px 0 5px;
      border-radius: 12px;
    }
    body.is-mobile .account-chip .account-flag { width: 26px; height: 26px; }
    body.is-mobile .account-chip .account-name { font-size: 11px; max-width: 100px; }
    body.is-mobile .account-chip .account-address { font-size: 9px; width: auto; max-width: 100px; }
    body.is-mobile .mobile-shell-brand {
      display: flex;
      align-items: center;
      min-height: 36px;
      /* room for the chip and the sound button to its left */
      padding-right: calc(12px + var(--mobile-chip-w, 150px) + 8px + 36px + 8px);
    }

    /* ---- sound, on the brand line ----
       The desktop's music controls live in the bottom-left dock, which
       a phone does not show. A single button, pinned against the left
       side of the account chip, opens the same two sliders. The chip
       is as wide as its name, so the button is placed by the chip's
       measured width (--mobile-chip-w, kept by script.js) rather than
       by a fixed margin. Closed it is one 36px button; the panel is
       display:none until opened. */
    .mobile-audio { display: none; }
    body.is-mobile .mobile-audio {
      display: block;
      position: fixed;
      top: calc(env(safe-area-inset-top, 0px) + 9px);
      right: calc(12px + var(--mobile-chip-w, 150px) + 8px);
      z-index: 40;
    }
    .mobile-audio-btn {
      display: grid;
      place-items: center;
      width: 36px;
      height: 36px;
      padding: 0;
      border-radius: 12px;
      border: 1px solid var(--panel-edge, #253244);
      background: rgba(6, 11, 21, 0.6);
      color: #cfe0f2;
      cursor: pointer;
    }
    .mobile-audio-btn svg { width: 18px; height: 18px; }
    .mobile-audio-btn .mobile-audio-off { display: none; }
    .mobile-audio-btn.is-muted { color: var(--tr-text-muted, #8c9aab); }
    .mobile-audio-btn.is-muted .mobile-audio-on { display: none; }
    .mobile-audio-btn.is-muted .mobile-audio-off { display: block; }
    .mobile-audio-btn[aria-expanded="true"] { border-color: rgba(159, 192, 228, 0.35); }
    /* Centred on the screen under the brand line, not hung off the
       button: the button sits mid-line, and a panel anchored to its
       right edge ran off the left side of a narrow phone. Fixed works
       here because the header is fixed and carries no transform. */
    .mobile-audio-pop {
      position: fixed;
      top: calc(env(safe-area-inset-top, 0px) + 9px + 36px + 8px);
      left: 50%;
      transform: translateX(-50%);
      width: min(300px, calc(100vw - 24px));
      box-sizing: border-box;
      display: grid;
      gap: 10px;
      padding: 12px;
      border-radius: 12px;
      border: 1px solid var(--panel-edge, #253244);
      background: var(--tr-bg-main, #0a0f18);
      box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
      z-index: 60;
    }
    .mobile-audio-pop[hidden] { display: none; }
    .mobile-audio-row {
      display: grid;
      grid-template-columns: 1fr auto;
      grid-template-areas: "label value" "slider slider";
      align-items: center;
      row-gap: 4px;
    }
    .mobile-audio-label {
      grid-area: label;
      font-size: 9.5px;
      font-weight: 700;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--tr-text-muted, #8c9aab);
    }
    .mobile-audio-value {
      grid-area: value;
      font-size: 11px;
      font-weight: 800;
      font-variant-numeric: tabular-nums;
      color: #dff4ff;
    }
    .mobile-audio-slider { grid-area: slider; width: 100%; margin: 0; accent-color: #9fc0e4; }
    /* on its side the header is one short line: the button still sits
       against the chip, and the panel comes down under that line,
       still centred */
    @media (orientation: landscape) and (max-height: 500px) {
      body.is-mobile .mobile-audio {
        top: calc(env(safe-area-inset-top, 0px) + 7px);
        right: calc(env(safe-area-inset-right, 0px) + 12px + var(--mobile-chip-w, 150px) + 8px);
      }
      body.is-mobile .mobile-audio-pop {
        top: calc(env(safe-area-inset-top, 0px) + 7px + 36px + 8px);
      }
    }

    /* ---- the register/login panel, on a screen it can outgrow ----

       The card is a column of six fields that has never had a
       height limit, because on a desktop it has never needed one.
       Now that it opens by itself on a phone, a handset held
       sideways is 390px tall against the card's 529 -- which put
       Confirm off the bottom of the screen with no way to reach
       it. The card gives way to the viewport here and scrolls
       inside itself. */
    body.is-mobile .profile-panel {
      padding:
        calc(env(safe-area-inset-top, 0px) + 12px) 12px
        calc(env(safe-area-inset-bottom, 0px) + 12px);
    }
    body.is-mobile .profile-card {
      max-height: 100%;
      overflow-y: auto;
      overscroll-behavior: contain;
    }

    /* ---- a phone on its side ----

       Half the height, so the header goes to a single line: the logo,
       the notice and the two counts side by side, with the account
       chip where it always is. The list keeps half the screen rather
       than the three rows a stacked header left it. */
    @media (orientation: landscape) and (max-height: 500px) {
      body.is-mobile .mobile-shell {
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: center;
        column-gap: 12px;
        padding:
          calc(env(safe-area-inset-top, 0px) + 7px)
          calc(env(safe-area-inset-right, 0px) + 12px + var(--mobile-chip-w, 150px) + 8px + 36px + 10px)
          7px
          calc(env(safe-area-inset-left, 0px) + 12px);
      }
      body.is-mobile .mobile-shell-brand { padding-right: 0; }
      body.is-mobile .mobile-shell-notice { font-size: 10.5px; }
      body.is-mobile .mobile-shell-counts { grid-template-columns: auto auto; }
      body.is-mobile .account-chip {
        top: calc(env(safe-area-inset-top, 0px) + 7px) !important;
        right: calc(env(safe-area-inset-right, 0px) + 12px) !important;
      }
      /* ...and the bottom is one row as well: Daily Spin, the contract
         strip, the chat. On its side a phone has the width for all
         three and not the height for two rows of them. */
      body.is-mobile {
        --mobile-split: 22%;
        --mobile-ca-h: var(--mobile-action-h);
        --mobile-ca-bottom: var(--mobile-floor);
        --mobile-bottom-h: calc(var(--mobile-floor) + var(--mobile-action-h) + 8px);
      }
      body.is-mobile .game-chat-launcher {
        left: calc(100% - var(--mobile-split) + (var(--mobile-gap) / 2));
      }
      body.is-mobile .ca-strip {
        left: calc(var(--mobile-split) + (var(--mobile-gap) / 2));
        right: calc(var(--mobile-split) + (var(--mobile-gap) / 2));
      }
    }
    /* Straight above the action row, over the contract strip in
       portrait: the chat is the thing being read while it is open, so
       it takes the strip's place rather than leaving a gap above it. */
    body.is-mobile .game-chat-panel {
      left: var(--mobile-gutter);
      right: var(--mobile-gutter);
      bottom: calc(var(--mobile-floor) + var(--mobile-action-h) + var(--mobile-gap));
      width: auto;
    }
    body.is-mobile .game-chat-card { padding: 10px; gap: 8px; border-radius: 16px; }
    body.is-mobile .game-chat-header { padding-bottom: 8px; }
    body.is-mobile .game-chat-title { font-size: 12px; }
    body.is-mobile .game-chat-close { width: 28px; height: 28px; font-size: 16px; }
    body.is-mobile .game-chat-messages { height: min(52vh, 420px); padding: 4px; }
    body.is-mobile .game-chat-message { font-size: 12.5px; column-gap: 8px; }

    /* ---- and the game itself steps aside ---- */
    body.is-mobile .game-layout,
    body.is-mobile .touch-controls,
    body.is-mobile .controls-visibility-toggle,
    body.is-mobile .holdings-chip,
    body.is-mobile .earnings-chip,
    body.is-mobile .match-status-overlay,
    body.is-mobile .piece-stack,
    body.is-mobile .live-hud { display: none !important; }
    /* Nothing is laid out in the flow any more, so the page has no
       height of its own and no reason to scroll. */
    body.is-mobile { overflow: hidden; }

    /* =========================================================
       MOBILE: WHAT A PHONE DOES NOT DRAW

       Measured, not guessed, and almost none of the cost was anything
       on screen: about thirty closed panels still laid out and
       composited under the lobby, most of them blurring what was
       behind them, one of them holding 24,000 elements. Every change
       to the page -- a row, a clock -- restyled all of it, and the
       frame rate fell by up to half while it did. script.js does its
       half: no board loop, no desktop-only listeners and no flag
       preload while body.is-mobile.
       ========================================================= */

    /* Frosted glass. Nearly every card in this file blurs whatever is
       behind it, and a blur is recomputed across its whole area
       whenever anything under or inside it changes -- a scrolling
       list, a board, a clock. Behind cards that are 97-99% opaque it
       barely shows, and on a phone's GPU it is among the most
       expensive things a page can ask for. */
    body.is-mobile *,
    body.is-mobile *::before,
    body.is-mobile *::after {
      -webkit-backdrop-filter: none !important;
      backdrop-filter: none !important;
    }
    /* The blur was also what hid the 1-3% of the lobby that comes
       through a card's gradient: without it, the players list reads as
       ghost text behind the chat and the player card. A solid base
       under the gradient closes that, in the gradient's own colour, so
       nothing changes but the leak. */
    body.is-mobile :is(.controls-card, .profile-card, .user-profile-card,
        .winners-card, .game-chat-card, .change-password-card,
        .security-alert-card, .player-inspect-card, .replay-card,
        .welcome-card, .spectate-card) {
      background-color: var(--tr-bg-main);
    }

    /* Closed means gone. Every overlay here closes to opacity 0, which
       on a desktop is what lets it fade -- and which also keeps it laid
       out and composited under the lobby: about thirty full-screen
       layers, several running animations nobody can see (the loading
       spinner, the result screen's mascot, the spectator's idle dots).
       On a phone a closed panel is display:none and costs nothing
       until it opens. It opens without the fade; nothing waits on one. */
    body.is-mobile :is(.welcome-panel, .profile-panel, .user-profile-panel,
        .player-inspect-panel, .replay-panel, .change-password-panel,
        .winners-panel, .security-block-panel, .loading-panel,
        .chat-image-viewer, .version-notice, .spectate-panel):not(.show),
    body.is-mobile .game-chat-panel:not(.open),
    body.is-mobile :is(.spectate-result, .spectate-idle):not(.is-visible) {
      display: none !important;
    }
    /* ...and what a phone never opens at all. The store, the change
       log, How To Play and the scoring sheet are reached from the side
       menu and the dock, which a phone does not show, and so is the
       reward split. The versus strip, the prize coin, the result
       screen and the perfect-clear celebration belong to a round of
       your own, and there is none here. Guest rooms are hosted from a
       desktop. */
    body.is-mobile :is(.controls-panel, .perk-price-info, .reward-distribution-panel,
        .match-versus, .match-versus-backdrop, .match-prize, .match-result,
        .pc-fx, .tour, #guestRoomPanel, #roomChatPanel) {
      display: none !important;
    }
    /* The mascot keeps his face and loses the motion: an idle float
       that never ends and a drop shadow drawn through a filter are two
       more things repainting on a screen that should be at rest. */
    body.is-mobile img[data-mascot].is-ready { animation: none !important; }
    body.is-mobile :is(.panel-mascot-img, .mz-tip-img, .mr-mascot-img, .tour-mascot, .mascot-reaction-img) {
      filter: none !important;
    }

    /* =========================================================
       MOBILE: WATCHING, BOTH BOARDS AT ONCE

       The spectator and the replay used to stack their seats on a
       narrow screen, one full-width board above the other, so a phone
       showed one player at a time and a round was followed by
       scrolling between them. Here the seats sit side by side: a 1v1
       is read by comparing the two boards, and a phone held upright
       has height to spare for two half-width boards and not the width
       for one full one.

       The board's width is solved from the boxes around it rather
       than guessed, so it lands exactly at every phone width:
         panel pad 6 + card border 1 + card pad 8     = 15 a side
         half the 6px gap between the seats          =  3
         seat border 1 + seat pad 6                  =  7 a side
         side column 22 + gap 5 + frame pad 5 + 5    = 37
       50vw - 15 - 3 - 14 - 37 = 50vw - 69px, less one for rounding.
       The height cap only bites on a phone held sideways.
       ========================================================= */
    body.is-mobile .spectate-panel {
      z-index: 1300;
      padding:
        calc(env(safe-area-inset-top, 0px) + 6px) 6px
        calc(env(safe-area-inset-bottom, 0px) + 6px);
      /* no blur behind it any more, so the lobby is dimmed a little
         harder to keep the boards the only thing being read */
      background: rgba(2, 5, 11, 0.93);
    }
    body.is-mobile .replay-panel {
      padding:
        calc(env(safe-area-inset-top, 0px) + 6px) 6px
        calc(env(safe-area-inset-bottom, 0px) + 6px);
      background: rgba(2, 5, 11, 0.9);
    }
    /* 100%, not the desktop's 96vw: against the panel's own padding
       that ran the replay card 16px off the right edge of a phone. */
    body.is-mobile :is(.spectate-card, .replay-card) {
      width: 100%;
      max-height: 100%;
      padding: 10px 8px;
      gap: 8px;
      border-radius: 16px;
    }
    body.is-mobile .spectate-boards {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 6px;
      padding: 0;
    }
    body.is-mobile .spectate-seat { gap: 6px; padding: 6px; border-radius: 12px; }
    body.is-mobile .spectate-player { grid-template-columns: 20px minmax(0, 1fr) auto; gap: 6px; }
    body.is-mobile .tr-plate-report { font-size: 9px; padding: 3px 6px; min-height: 22px; }
    body.is-mobile .spectate-flag { width: 20px; height: 14px; border-radius: 3px; }
    body.is-mobile .spectate-name { font-size: 11.5px; }
    body.is-mobile .spectate-rank { font-size: 9px; }
    /* Watching / Opponent: the watched seat already wears the violet
       edge, and at half width the tag would take half of the name. */
    body.is-mobile .spectate-seat-tag { display: none; }
    body.is-mobile .spectate-stage { gap: 5px; }
    body.is-mobile .spectate-side { gap: 3px; }
    body.is-mobile .spectate-side-label { font-size: 7px; letter-spacing: .1em; }
    body.is-mobile .spectate-mini { --spectate-mini: 22px; border-radius: 5px; }
    body.is-mobile .spectate-board-wrap {
      --frame-pad: 5px;
      padding: 5px;
      border-radius: 9px;
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        inset 0 0 0 1px rgba(0, 0, 0, 0.32);
    }
    body.is-mobile .spectate-board {
      --spectate-board: min(calc(50vw - 70px), calc((100vh - 252px) / 2));
    }
    body.is-mobile :is(.spectate-ko-flash, .spectate-board-note) { inset: 5px; border-radius: 6px; }
    body.is-mobile .spectate-board-note { padding: 0 4px; font-size: 8px; letter-spacing: .06em; text-align: center; }
    body.is-mobile .spectate-perfect-popup { padding: 6px 8px; font-size: 8px; }
    body.is-mobile .spectate-perfect-popup span:last-child { font-size: 11px; }
    body.is-mobile .spectate-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; }
    body.is-mobile .spectate-stat { padding: 4px 2px 5px; gap: 1px; border-radius: 8px; }
    body.is-mobile .spectate-stat-name { font-size: 7.5px; letter-spacing: .08em; }
    body.is-mobile .spectate-stat-value { font-size: 13px; }

    /* The header in two short rows rather than three tall ones: what
       is being watched and the way out, then the money and the clock
       side by side. */
    body.is-mobile .spectate-head {
      grid-template-columns: minmax(0, 1fr) auto;
      grid-template-areas: "left back" "mid mid";
      justify-items: stretch;
      gap: 6px 8px;
    }
    body.is-mobile .spectate-head-left { grid-area: left; gap: 8px; }
    body.is-mobile .spectate-head-mid {
      grid-area: mid;
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 6px;
    }
    body.is-mobile .spectate-live { gap: 6px; padding: 3px 9px; font-size: 9px; }
    body.is-mobile .spectate-band { font-size: 9.5px; }
    /* Label over figure, the desktop's own shape at a smaller size.
       Side by side ran out of width on a 360px phone, where the label
       either broke in two or lost its end. */
    body.is-mobile .spectate-meter { min-width: 0; gap: 2px; padding: 4px 8px; }
    body.is-mobile .spectate-meter-label { font-size: 8px; white-space: nowrap; }
    body.is-mobile .spectate-meter-value { gap: 4px; font-size: 12px; }
    body.is-mobile .spectate-sol { width: 11px; height: 11px; }
    /* The way out as a round arrow, the size of the replay's close.
       The words are hidden the accessible way, so a screen reader
       still hears "Back to your lobby". */
    body.is-mobile .spectate-back {
      grid-area: back;
      justify-self: end;
      position: relative;
      width: 32px;
      height: 32px;
      padding: 0;
      justify-content: center;
      font-size: 15px;
    }
    body.is-mobile .spectate-back-lobby {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    body.is-mobile .spectate-result-card { gap: 7px; padding: 16px 14px; max-width: calc(100% - 20px); }
    body.is-mobile .spectate-result-winner { font-size: 17px; }
    body.is-mobile .spectate-score { gap: 6px 10px; padding: 10px; }
    body.is-mobile .spectate-score-value { font-size: 20px; }

    /* The replay's own furniture, around the same two seats. */
    body.is-mobile .replay-head { gap: 8px; }
    body.is-mobile .replay-title { font-size: 12.5px; }
    body.is-mobile .replay-subtitle { font-size: 9px; }
    body.is-mobile .replay-card > .replay-head > .close-btn { width: 30px; height: 30px; font-size: 15px; }
    /* one line, the way it is on a desktop: the bar between the
       button and the clock, rather than the clock on a row of its own */
    body.is-mobile .replay-transport {
      grid-template-columns: auto minmax(0, 1fr) auto;
      gap: 8px;
      padding: 6px 8px;
      border-radius: 10px;
    }
    body.is-mobile .replay-time { grid-column: auto; font-size: 10px; }
    body.is-mobile .replay-play { min-width: 58px; padding: 6px 10px; font-size: 9px; }
    /* a thumb-sized handle on a bar that is dragged with a thumb */
    body.is-mobile .replay-scrub::-webkit-slider-thumb { width: 18px; height: 18px; }
    body.is-mobile .replay-scrub::-moz-range-thumb { width: 18px; height: 18px; }
    body.is-mobile .replay-note { font-size: 9px; }

    /* On its side a phone has width to spare and no height, so the
       header is one line and the four counters one row, and the
       boards keep what height there is. */
    @media (orientation: landscape) and (max-height: 500px) {
      body.is-mobile .spectate-head {
        grid-template-columns: auto minmax(0, 1fr) auto;
        grid-template-areas: "left mid back";
      }
      body.is-mobile .spectate-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
      body.is-mobile .spectate-board {
        --spectate-board: min(calc(50vw - 70px), calc((100vh - 176px) / 2));
      }
      body.is-mobile .replay-note { display: none; }
      body.is-mobile .replay-card .spectate-board {
        --spectate-board: min(calc(50vw - 70px), calc((100vh - 220px) / 2));
      }
    }

    /* =========================================================
       MOBILE: A PLAYER'S CARD

       The same card, closer together. At 420px and under the desktop
       rules put the counts two to a row and every half-width tile on a
       row of its own, so the record alone filled a phone's screen and
       the recent matches -- the way into a replay -- sat below it.
       Here the counts run three to a row and Earnings and Energy share
       one, and the recent matches are on the first screen.
       ========================================================= */
    body.is-mobile .player-inspect-panel {
      padding:
        calc(env(safe-area-inset-top, 0px) + 8px) 8px
        calc(env(safe-area-inset-bottom, 0px) + 8px);
    }
    body.is-mobile .player-inspect-card {
      width: 100%;
      max-height: 100%;
      padding: 14px 12px 12px;
      border-radius: 18px;
    }
    body.is-mobile .player-inspect-title { margin: 0 0 10px; font-size: 11px; }
    body.is-mobile .player-inspect-card .close-btn { top: 9px; right: 9px; width: 30px; height: 30px; }
    body.is-mobile .pi-identity {
      grid-template-columns: 36px minmax(0, 1fr) auto;
      gap: 10px;
      padding: 9px 11px;
      border-radius: 12px;
    }
    body.is-mobile .pi-flag { width: 36px; height: 24px; border-radius: 5px; }
    body.is-mobile .pi-nickname { font-size: 15px; }
    body.is-mobile .pi-rank { font-size: 17px; }
    body.is-mobile .pi-holdings { margin-top: 8px; padding: 10px 12px 11px; border-radius: 12px; }
    body.is-mobile .pi-holdings-head { margin-bottom: 5px; }
    body.is-mobile .pi-holdings-value { font-size: clamp(20px, 6.4vw, 30px); }
    body.is-mobile .pi-grid { margin-top: 8px; gap: 6px; }
    body.is-mobile .pi-grid > .pi-stat { grid-column: span 2; }
    body.is-mobile .pi-grid > .pi-stat-wide { grid-column: span 3; text-align: left; }
    body.is-mobile .pi-grid > .pi-stat-wide .pi-stat-value { justify-content: flex-start; }
    body.is-mobile .pi-grid > .pi-stat-full { grid-column: span 6; }
    body.is-mobile .pi-stat { gap: 3px; padding: 7px 9px 8px; border-radius: 10px; }
    body.is-mobile .pi-stat-label { font-size: 8px; }
    body.is-mobile .pi-stat-value { font-size: 15px; }
    body.is-mobile .pi-grid > .pi-stat-headline { padding: 9px 12px 10px; }
    body.is-mobile .pi-stat-headline .pi-stat-value { font-size: 20px; }
    body.is-mobile .pi-note { margin-top: 8px; font-size: 9px; }
    body.is-mobile .pi-section-rule { margin: 10px 0 0; }
    body.is-mobile .pi-history { margin-top: 10px; }
    body.is-mobile .pi-history-head { margin-bottom: 6px; }
    body.is-mobile .pi-history-row {
      grid-template-columns: 40px minmax(0, 1fr) auto;
      gap: 8px;
      padding: 6px 8px;
      border-radius: 10px;
    }
    body.is-mobile .pi-history-vs { font-size: 11.5px; }
    body.is-mobile .pi-replay-btn { padding: 6px 11px; }

    /* =========================================================
       MOBILE: GAME INFORMATION

       The rules without the keyboard. Nothing on a phone takes a key,
       so the Controls card is a list of instructions nobody here can
       follow; the areas are restated without it so it leaves no gap.
       ========================================================= */
    body.is-mobile .wi-area-keys { display: none; }
    body.is-mobile .wi-body {
      grid-template-columns: minmax(0, 1fr);
      grid-template-areas: "play" "score" "rew" "rank" "ener" "band";
      gap: 8px;
    }
    @media (min-width: 641px) {
      body.is-mobile .wi-body {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-areas: "play score" "rew rew" "rank ener" "band band";
      }
    }
    body.is-mobile .welcome-card { gap: 10px; padding: 14px 10px 10px; }
    body.is-mobile .wi-card { gap: 7px; padding: 10px 11px 11px; border-radius: 12px; }
    body.is-mobile :is(.wi-list, .wi-row dt, .wi-row dd) { font-size: 11.5px; }
    body.is-mobile .wi-note { font-size: 10.5px; }

    /* =========================================================
       THE MASCOT

       His art is transparent PNG cutouts in brand/mascot/. Every
       <img data-mascot> stays hidden until script.js has confirmed
       its file loads (.is-ready), and its host only changes look once
       it has (.has-mascot) -- so before the art exists, every place
       he appears looks exactly as it did without him.
       ========================================================= */
    img[data-mascot]:not(.is-ready) { display: none; }

    /* Greeting in a panel's top-left corner: the store and the change
       log. He pops up as the panel opens. */
    .panel-mascot {
      position: absolute;
      top: 10px;
      left: 18px;
      z-index: 2;
      display: flex;
      align-items: center;
      gap: 8px;
      pointer-events: none;
    }
    .panel-mascot:not(.has-mascot) { display: none; }
    /* the store's head row starts close under its title, so he sits a
       touch higher and smaller there to stay clear of the burn notice */
    .store-mascot { top: 4px; }
    .store-mascot .panel-mascot-img { width: 50px; height: 50px; }
    .panel-mascot-img {
      width: 58px;
      height: 58px;
      object-fit: contain;
      filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.45));
    }
    .panel-mascot-bubble {
      padding: 6px 10px;
      border-radius: 10px 10px 10px 3px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.98), rgba(9, 13, 22, 0.99));
      font-size: 10.5px;
      font-weight: 800;
      letter-spacing: .06em;
      white-space: nowrap;
      color: #e4eef8;
    }
    .controls-panel.show .panel-mascot { animation: mascot-pop .5s cubic-bezier(.2, .9, .3, 1.25) .12s both; }
    @keyframes mascot-pop {
      from { opacity: 0; transform: translateY(14px) scale(.85); }
      to { opacity: 1; transform: none; }
    }
    @media (max-width: 760px) {
      .panel-mascot-bubble { display: none; }
      .panel-mascot-img { width: 46px; height: 46px; }
    }

    /* Reactions during play, under the NEXT previews. */
    .player-side .next-column { position: relative; }
    .mascot-reaction {
      position: absolute;
      top: calc(100% + 14px);
      left: 50%;
      width: 140px;
      margin-left: -70px;
      display: grid;
      justify-items: center;
      gap: 6px;
      pointer-events: none;
      opacity: 0;
      transform: translateY(14px) scale(.9);
      transition: opacity .22s ease, transform .32s cubic-bezier(.2, .9, .3, 1.3);
    }
    .mascot-reaction:not(.has-mascot) { display: none; }
    .mascot-reaction.is-on { opacity: 1; transform: none; }
    .mascot-reaction-img {
      width: 96px;
      height: 96px;
      object-fit: contain;
      filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.45));
    }
    .mascot-reaction-bubble {
      padding: 6px 10px;
      border-radius: 10px;
      border: 1px solid var(--panel-edge);
      background: linear-gradient(180deg, rgba(18, 25, 38, 0.98), rgba(9, 13, 22, 0.99));
      font-size: 10.5px;
      font-weight: 800;
      letter-spacing: .08em;
      text-transform: uppercase;
      white-space: nowrap;
      color: #e4eef8;
    }
    .mascot-reaction-bubble:empty { display: none; }
    @media (max-width: 1250px) and (orientation: landscape) {
      .mascot-reaction-img { width: 72px; height: 72px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .mascot-reaction { transition: opacity .2s ease; transform: none; }
      .controls-panel.show .panel-mascot { animation: none; }
    }

    /* The match result: victory, sad or cool, above the verdict. */
    .mr-mascot {
      display: grid;
      place-items: center;
      margin: -4px auto 6px;
    }
    .mr-mascot:not(.has-mascot) { display: none; }
    .mr-mascot-img {
      width: 128px;
      height: 128px;
      object-fit: contain;
      filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.45));
    }

    /* =========================================================
       THE TOUR

       A spotlight cut out of a dim layer (the spot's own box-shadow
       is the dim), and a card beside it. The spot's edge is a quiet
       hairline that breathes, not a glow; the card is the standard
       panel frame. Above every panel it explains, below the page-load
       intro and the version notice.
       ========================================================= */
    .tour {
      position: fixed;
      inset: 0;
      z-index: 400;
    }
    .tour[hidden] { display: none; }
    .tour-spot {
      position: fixed;
      border-radius: 14px;
      border: 1px solid rgba(151, 184, 222, 0.42);
      box-shadow: 0 0 0 200vmax rgba(3, 6, 12, 0.74);
      pointer-events: none;
      transition:
        left .35s cubic-bezier(.2, .8, .2, 1),
        top .35s cubic-bezier(.2, .8, .2, 1),
        width .35s cubic-bezier(.2, .8, .2, 1),
        height .35s cubic-bezier(.2, .8, .2, 1);
      animation: tour-breathe 2.4s ease-in-out infinite;
    }
    .tour-spot.is-empty { border-color: transparent; animation: none; }
    @keyframes tour-breathe {
      0%, 100% { border-color: rgba(151, 184, 222, 0.30); }
      50% { border-color: rgba(151, 184, 222, 0.62); }
    }
    .tour-card {
      position: fixed;
      width: min(420px, calc(100vw - 32px));
      display: grid;
      grid-template-columns: auto minmax(0, 1fr);
      align-items: start;
      gap: 16px;
      padding: 20px 20px 18px;
      border-radius: 18px;
      border: 1px solid var(--tr-border-subtle);
      background: var(--tr-panel-gradient);
      box-shadow: var(--tr-inset-highlight), 0 18px 44px rgba(0, 0, 0, 0.5);
      color: var(--tr-text-primary);
      transition:
        left .35s cubic-bezier(.2, .8, .2, 1),
        top .35s cubic-bezier(.2, .8, .2, 1);
    }
    .tour-guide {
      position: relative;
      width: 88px;
      height: 88px;
      display: grid;
      place-items: center;
    }
    .tour-mascot {
      width: 100%;
      height: 100%;
      object-fit: contain;
      filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.45));
    }
    /* the crown, until his art exists */
    .tour-guide-fallback {
      width: 64px;
      height: 64px;
      border-radius: 16px;
      -webkit-mask-image: radial-gradient(closest-side, #000 72%, transparent 100%);
      mask-image: radial-gradient(closest-side, #000 72%, transparent 100%);
    }
    .tour-guide.has-mascot .tour-guide-fallback { display: none; }
    .tour-meta {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
    }
    .tour-step {
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--tr-cyan);
    }
    .tour-skip {
      padding: 4px 2px;
      border: 0;
      background: none;
      color: var(--tr-text-muted);
      font-size: 11px;
      font-weight: 700;
      letter-spacing: .04em;
      cursor: pointer;
    }
    .tour-skip:hover { color: var(--tr-text-primary); }
    .tour-skip:focus-visible { outline: 2px solid var(--tr-violet-bright); outline-offset: 2px; border-radius: 4px; }
    .tour-title {
      margin: 8px 0 6px;
      font-size: 17px;
      font-weight: 800;
      letter-spacing: .02em;
      color: #e4eef8;
    }
    .tour-text {
      margin: 0;
      font-size: 12.5px;
      line-height: 1.6;
      color: rgba(178, 202, 226, 0.8);
    }
    .tour-foot {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      margin-top: 16px;
    }
    /* one row, however many steps: the count above says where you are,
       these only show how far is left */
    .tour-dots { display: flex; flex-wrap: nowrap; gap: 3px; min-width: 0; overflow: hidden; }
    .tour-dots i {
      flex: none;
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: rgba(151, 184, 222, 0.2);
    }
    .tour-dots i.on { background: var(--tr-cyan); }
    .tour-actions { display: flex; gap: 8px; flex: none; }
    .tour-btn {
      padding: 9px 16px;
      border-radius: 10px;
      border: 1px solid var(--panel-edge);
      background: rgba(255, 255, 255, 0.03);
      color: var(--tr-text-secondary);
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
      cursor: pointer;
      transition: border-color var(--tr-transition-fast), color var(--tr-transition-fast), background var(--tr-transition-fast);
    }
    .tour-btn:hover { border-color: rgba(151, 184, 222, 0.32); color: var(--tr-text-primary); }
    .tour-btn:focus-visible { outline: 2px solid var(--tr-violet-bright); outline-offset: 2px; }
    .tour-btn[hidden] { display: none; }
    .tour-next {
      border-color: rgba(152, 116, 255, 0.36);
      background:
        radial-gradient(120% 150% at 50% 0%, rgba(152, 116, 255, 0.16), transparent 70%),
        linear-gradient(180deg, rgba(25, 29, 50, 0.99), rgba(12, 15, 27, 1));
      color: #e4dcff;
    }
    .tour-next:hover { border-color: rgba(152, 116, 255, 0.56); color: #f1ecff; }
    @media (prefers-reduced-motion: reduce) {
      .tour-spot, .tour-card { transition: none; }
      .tour-spot { animation: none; }
    }
    /* "Take the tour", under the handling settings. Desktop only, as
       the tour is. */
    .tour-replay {
      display: block;
      width: 100%;
      margin: 0 0 12px;
      padding: 11px 16px;
      border-radius: 12px;
      border: 1px solid rgba(95, 212, 255, 0.28);
      background: rgba(95, 212, 255, 0.05);
      color: #bfeaff;
      font-size: 11px;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      cursor: pointer;
      transition: border-color var(--tr-transition-fast), background var(--tr-transition-fast);
    }
    .tour-replay:hover { border-color: rgba(95, 212, 255, 0.46); background: rgba(95, 212, 255, 0.09); }
    .tour-replay:focus-visible { outline: 2px solid var(--tr-violet-bright); outline-offset: 2px; }
    body.is-mobile .tour-replay { display: none; }

    /* =========================================================
       THE MASCOT, MOVING

       His art is still PNGs; the motion is CSS, which keeps his
       outline clean on the dark UI (a GIF has no soft transparency)
       and runs at the screen's own frame rate. Each mood has its own
       entrance -- excited hops, angry shakes, sad droops, surprised
       jumps back, cool tilts, happy bobs -- and then he floats gently
       while he is on screen.
       ========================================================= */
    img[data-mascot].is-ready { transform-origin: 50% 85%; will-change: transform; }

    @keyframes mz-happy     { 0% { transform: translateY(10px) scale(.9); opacity: 0; } 55% { transform: translateY(-6px) scale(1.03); opacity: 1; } 100% { transform: none; } }
    @keyframes mz-excited   { 0% { transform: translateY(12px) scale(.85); opacity: 0; } 30% { transform: translateY(-14px) scale(1.06, .96); opacity: 1; } 50% { transform: translateY(0) scale(.97, 1.04); } 68% { transform: translateY(-7px); } 84% { transform: translateY(0) scale(1.02, .98); } 100% { transform: none; } }
    @keyframes mz-cool      { 0% { transform: translateX(-10px) rotate(-8deg); opacity: 0; } 60% { transform: translateX(2px) rotate(4deg); opacity: 1; } 100% { transform: rotate(0); } }
    @keyframes mz-sad       { 0% { transform: translateY(-6px); opacity: 0; } 60% { transform: translateY(5px) scale(1, .97); opacity: 1; } 100% { transform: translateY(2px); } }
    @keyframes mz-angry     { 0% { transform: scale(.9); opacity: 0; } 20% { transform: scale(1.04) translateX(-5px) rotate(-3deg); opacity: 1; } 35% { transform: translateX(5px) rotate(3deg); } 50% { transform: translateX(-4px) rotate(-2deg); } 65% { transform: translateX(3px) rotate(2deg); } 80% { transform: translateX(-1px); } 100% { transform: none; } }
    @keyframes mz-surprised { 0% { transform: scale(.8); opacity: 0; } 35% { transform: translateY(-12px) scale(1.1); opacity: 1; } 60% { transform: translateY(0) scale(.97); } 100% { transform: none; } }
    @keyframes mz-float     { 0%, 100% { translate: 0 0; } 50% { translate: 0 -4px; } }

    /* entrance by mood, then the idle float (translate, so it never
       fights the entrance's transform) */
    :is(.mascot-reaction.is-on, .panel-mascot, .tour-guide, .mr-mascot) img[data-mood].is-ready {
      animation-duration: .7s, 3.2s;
      animation-timing-function: cubic-bezier(.2, .8, .25, 1), ease-in-out;
      animation-delay: 0s, .7s;
      animation-iteration-count: 1, infinite;
      animation-fill-mode: both, none;
    }
    :is(.mascot-reaction.is-on, .panel-mascot, .tour-guide, .mr-mascot) img[data-mood="happy"].is-ready     { animation-name: mz-happy, mz-float; }
    :is(.mascot-reaction.is-on, .panel-mascot, .tour-guide, .mr-mascot) img[data-mood="excited"].is-ready   { animation-name: mz-excited, mz-float; animation-duration: .85s, 2.4s; }
    :is(.mascot-reaction.is-on, .panel-mascot, .tour-guide, .mr-mascot) img[data-mood="cool"].is-ready      { animation-name: mz-cool, mz-float; }
    :is(.mascot-reaction.is-on, .panel-mascot, .tour-guide, .mr-mascot) img[data-mood="sad"].is-ready       { animation-name: mz-sad, mz-float; animation-duration: .9s, 4s; }
    :is(.mascot-reaction.is-on, .panel-mascot, .tour-guide, .mr-mascot) img[data-mood="angry"].is-ready     { animation-name: mz-angry, mz-float; }
    :is(.mascot-reaction.is-on, .panel-mascot, .tour-guide, .mr-mascot) img[data-mood="surprised"].is-ready { animation-name: mz-surprised, mz-float; }

    @media (prefers-reduced-motion: reduce) {
      img[data-mascot].is-ready { animation: none !important; }
    }

    /* =========================================================
       THE MASCOT IN EVERY EXPLANATION

       Tooltips and explanatory notes (.mz-tip) carry him on the left,
       the words beside him, as if he were saying them. Only once his
       art has loaded (.has-mascot) does the tip make room for him, so
       without it every tip reads exactly as it did. Panels that explain
       the game greet you with him in the top-left corner (.panel-mascot),
       popping up whenever the panel opens.
       ========================================================= */
    .mz-tip { --mz: 40px; }
    /* no position here: the floating tips are already absolute, and
       making them relative dropped them into the page's layout. Only
       the notes that sit in a panel's flow need it (below). */
    .mz-tip.has-mascot {
      padding-left: calc(var(--mz) + 18px) !important;
      min-height: calc(var(--mz) + 12px);
    }
    .mz-tip-img {
      position: absolute;
      left: 8px;
      top: calc(50% - var(--mz) / 2);
      width: var(--mz);
      height: var(--mz);
      object-fit: contain;
      pointer-events: none;
      filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.45));
    }
    /* the notes inside panels have more room, so he is a size larger */
    .perks-explain-body .mz-tip,
    .ppi-note.mz-tip { --mz: 48px; position: relative; }
    .ppi-note-text { margin: 0; }
    .mz-tip img[data-mood].is-ready { animation: mz-float 3.2s ease-in-out infinite; }

    /* every panel's greeting pops up as it opens, not only the store's */
    .show .panel-mascot { animation: mascot-pop .5s cubic-bezier(.2, .9, .3, 1.25) .12s both; }
    .panel-mascot + .wi-head,
    .welcome-card .panel-mascot { z-index: 3; }
    @media (prefers-reduced-motion: reduce) {
      .show .panel-mascot { animation: none; }
    }

    /* The greeting's speech bubble says its line and goes: it sits over
       the panel's title, so it fades out 3 seconds after the panel opens.
       The mascot stays. It comes back each time the panel reopens,
       because the animation restarts whenever .show is added again. */
    @keyframes mz-bubble-out {
      to { opacity: 0; visibility: hidden; transform: translateX(-6px); }
    }
    .show .panel-mascot-bubble {
      animation: mz-bubble-out .45s ease-in 3s forwards;
    }
    @media (prefers-reduced-motion: reduce) {
      .show .panel-mascot-bubble { animation-duration: .01s; }
    }

    /* =========================================================
       THE GREETER, STAGED BESIDE THE PANEL

       Placed by placePanelMascot() in script.js: left and top come from
       the open card's measured edge, --pm-size from its height. Hidden
       until placed, so a panel with no room beside it shows no greeter
       rather than one standing on its content. His speech bubble sits
       above his head and still says its line and fades (mz-bubble-out).
       ========================================================= */
    .panel-mascot.is-stage {
      position: absolute;
      top: 0;
      left: 0;
      z-index: 2;
      display: none;
      flex-direction: column;
      align-items: center;
      gap: 10px;
      width: calc(var(--pm-size, 200px) * 1.1);
      pointer-events: none;
    }
    .panel-mascot.is-stage.has-mascot.is-placed { display: flex; }
    .panel-mascot.is-stage .panel-mascot-img {
      order: 2;
      width: 100%;
      height: var(--pm-size, 200px);
      object-fit: contain;
      object-position: bottom;
      filter: drop-shadow(0 18px 28px rgba(0, 0, 0, 0.55));
    }
    .panel-mascot.is-stage .panel-mascot-bubble {
      order: 1;
      max-width: 100%;
      padding: 9px 14px;
      border-radius: 14px;
      font-size: 13px;
      white-space: normal;
      text-align: center;
    }
    .panel-mascot.is-stage.is-left .panel-mascot-bubble { border-bottom-right-radius: 4px; }
    .panel-mascot.is-stage.is-right .panel-mascot-bubble { border-bottom-left-radius: 4px; }
    /* the in-card sizes and offsets no longer apply to him out here */
    .panel-mascot.is-stage.store-mascot { top: auto; }
    @media (max-width: 760px) {
      .panel-mascot.is-stage .panel-mascot-bubble { display: block; }
    }
