*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
  --bg:#04040e;--s1:rgba(255,255,255,0.04);--s2:rgba(255,255,255,0.08);
  --border:rgba(255,255,255,0.1);--border2:rgba(255,255,255,0.22);
  --cyan:#00f5ff;--pink:#ff0090;--lime:#39ff14;--gold:#ffd700;
  --purple:#a855f7;--orange:#ff6600;--red:#ff2442;
  /* Leaderboard: podium metals + the "that's you" blue */
  --silver:#c9d4e0;--bronze:#cd7f32;--blue:#3b82f6;
  --text:#fff;--dim:rgba(255,255,255,0.55);--dimmer:rgba(255,255,255,0.28);
  --fd:'Orbitron',monospace;--fb:'Rajdhani',sans-serif;--r:12px;--rl:18px;
  /* Reaction Time's state colors. Named by MEANING, not hue, so the
     colorblind-safe palette can repoint them without touching the game. */
  --rx-wait:var(--red);--rx-go:var(--lime);--rx-early:var(--orange);--rx-hit:var(--cyan)
}

/* ── 🎨 COLORBLIND-SAFE PALETTE ──
   Okabe-Ito derived: the red/green pair that carries meaning in this game
   becomes blue/orange, which stays distinguishable for all three common
   types of color vision deficiency. Neon character is kept where it's
   decorative; only the load-bearing signals are repointed. */
html.cb-safe{
  --cyan:#56b4e9;--pink:#cc79a7;--lime:#f0e442;--gold:#e69f00;
  --purple:#9a7fd1;--orange:#e69f00;--red:#0072b2;
  --rx-wait:#0072b2;--rx-go:#f0e442;--rx-early:#e69f00;--rx-hit:#56b4e9;
}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--fb);overflow:hidden;-webkit-font-smoothing:antialiased}

/* ── ⬍ SCROLLBARS ──
   The browser default (grey thumb, white track) is the one piece of chrome
   that ignores the palette, and it shows on every scrolling surface: .screen,
   .market-body, .lb-panel, .mp-body, .fb-modal, .fb-textarea. Same neon
   gradient + glow as the rest of the UI. Colors are palette vars, so the
   cb-safe overrides above repoint the scrollbar for free. */
:root{--sb-track:rgba(255,255,255,.04);--sb-glow:rgba(0,245,255,.4)}
html.cb-safe{--sb-glow:rgba(86,180,233,.4)}

/* Firefox has no ::-webkit-scrollbar, so it gets the flat-accent fallback.
   Scoped to browsers that lack the pseudo-element on purpose: Chrome ignores
   every ::-webkit-scrollbar rule on any element that sets scrollbar-width or
   scrollbar-color, and both properties inherit — setting them on <html> would
   silently kill the gradient everywhere. */
@supports not selector(::-webkit-scrollbar){
  html{scrollbar-width:thin;scrollbar-color:var(--cyan) var(--sb-track)}
  .fb-modal{scrollbar-color:var(--lime) var(--sb-track)}
}

::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:var(--sb-track);border-radius:10px;box-shadow:inset 0 0 0 1px var(--border)}
::-webkit-scrollbar-thumb{
  background:linear-gradient(180deg,var(--cyan),var(--purple));
  border:2px solid transparent;background-clip:padding-box;border-radius:10px;
  box-shadow:0 0 8px var(--sb-glow)
}
::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg,var(--pink),var(--cyan));background-clip:padding-box;box-shadow:0 0 14px var(--sb-glow)}
::-webkit-scrollbar-thumb:active{background:linear-gradient(180deg,var(--pink),var(--purple));background-clip:padding-box}
::-webkit-scrollbar-corner{background:transparent}

/* Form fields are small — a 10px bar swamps them. */
textarea::-webkit-scrollbar,select::-webkit-scrollbar,.input::-webkit-scrollbar{width:7px;height:7px}
textarea::-webkit-scrollbar-thumb,select::-webkit-scrollbar-thumb,.input::-webkit-scrollbar-thumb{border-width:1px}

/* The feedback modal is lime-accented, so its scrollbar follows suit. */
.fb-modal::-webkit-scrollbar-thumb{background:linear-gradient(180deg,var(--lime),var(--cyan));background-clip:padding-box;box-shadow:0 0 8px rgba(57,255,20,.35)}
.fb-modal::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg,var(--cyan),var(--lime));background-clip:padding-box;box-shadow:0 0 14px rgba(57,255,20,.5)}
#bg-canvas{position:fixed;inset:0;z-index:0;pointer-events:none}

/* ── SCREEN SYSTEM ── */
.screen{position:fixed;inset:0;z-index:10;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;overflow-y:auto;opacity:0;pointer-events:none;transition:opacity .3s ease,transform .3s ease;transform:translateY(10px)}
.screen.active{opacity:1;pointer-events:all;transform:translateY(0)}

/* ── TYPE ── */
.logo{font-family:var(--fd);font-weight:900;font-size:clamp(1.8rem,5vw,3.5rem);background:linear-gradient(90deg,var(--cyan),var(--pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:.05em;line-height:1;text-align:center}
.tagline{font-family:var(--fd);font-size:.55rem;letter-spacing:.3em;color:var(--dim);text-transform:uppercase;margin-top:14px;text-align:center}
.sec-label{font-family:var(--fd);font-size:.6rem;font-weight:700;color:var(--cyan);letter-spacing:.2em;text-transform:uppercase;margin-bottom:20px}

/* ── BUTTONS ── */
.btn{font-family:var(--fd);font-size:.7rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:11px 26px;border:none;border-radius:8px;cursor:pointer;transition:all .2s ease;white-space:nowrap}
.btn-primary{background:linear-gradient(135deg,var(--cyan),var(--purple));color:#000;box-shadow:0 0 20px rgba(0,245,255,.25)}
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 0 35px rgba(0,245,255,.45)}
.btn-primary:active{transform:scale(.96)}
.btn-secondary{background:var(--s2);color:var(--text);border:1px solid var(--border)}
.btn-secondary:hover{background:rgba(255,255,255,.14);border-color:var(--border2)}
.btn-full{width:100%}.btn-lg{padding:14px 36px;font-size:.8rem}.btn-sm{padding:7px 16px;font-size:.6rem}

/* ── CARD / INPUT ── */
.card{background:var(--s1);border:1px solid var(--border);border-radius:var(--rl);backdrop-filter:blur(20px);padding:24px}
.input{width:100%;background:var(--s2);border:1.5px solid var(--border);border-radius:8px;color:var(--text);font-family:var(--fb);font-size:1rem;font-weight:600;padding:11px 16px;outline:none;transition:border-color .2s,box-shadow .2s}
.input::placeholder{color:var(--dimmer)}
.input:focus{border-color:var(--cyan);box-shadow:0 0 0 3px rgba(0,245,255,.1)}
.input-label{display:block;font-size:.7rem;font-weight:600;color:var(--dim);letter-spacing:.12em;text-transform:uppercase;margin-bottom:9px;font-family:var(--fd)}
.field{margin-bottom:18px}

/* ── AUTH SCREEN ── */
#auth-screen .wrap{width:100%;max-width:400px;text-align:center}
.auth-icon{font-size:4rem;display:block;margin-bottom:16px;animation:float 3s ease-in-out infinite}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-14px)}}
.auth-tabs{display:flex;gap:8px;margin-bottom:22px}
.auth-tab{flex:1;font-family:var(--fd);font-size:.65rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:10px;background:var(--s2);border:1px solid var(--border);border-radius:8px;color:var(--dim);cursor:pointer;transition:all .2s}
.auth-tab.active{background:rgba(0,245,255,.1);border-color:var(--cyan);color:var(--cyan)}
.auth-err{min-height:20px;font-size:.8rem;color:var(--pink);font-weight:600;margin-top:10px;text-align:center}
.auth-divider{display:flex;align-items:center;gap:12px;margin:16px 0 14px;font-family:var(--fd);font-size:.55rem;font-weight:700;letter-spacing:.28em;text-transform:uppercase;color:var(--dimmer)}
.auth-divider::before,.auth-divider::after{content:'';flex:1;height:1px;background:var(--border)}
.btn-guest{background:rgba(57,255,20,.08);color:var(--lime);border:1px solid rgba(57,255,20,.45)}
.btn-guest:hover:not(:disabled){background:rgba(57,255,20,.16);border-color:var(--lime);transform:translateY(-2px);box-shadow:0 0 30px rgba(57,255,20,.25)}
.btn-guest:active:not(:disabled){transform:scale(.96)}
.btn-guest:disabled{opacity:.5;cursor:progress;transform:none;box-shadow:none}
.guest-note{font-size:.72rem;color:var(--dimmer);text-align:center;margin-top:10px;line-height:1.4}

/* ── OFFLINE / LOCAL ENTRY BUTTONS ──
   These two are built in JS, and their labels are longer than the ~350px the
   auth card gives them — "Continue Offline as <name>" carries a username of up
   to 20 characters. They inherit .btn's white-space:nowrap, so the label used
   to spill straight past the rounded border instead of stopping at it.
   Trimming the oversized .btn-lg side padding buys back room, and overflow
   +text-overflow turn whatever still doesn't fit into an ellipsis. */
#btn-offline-resume,#btn-local-play{padding-left:14px;padding-right:14px;overflow:hidden;text-overflow:ellipsis}
/* Laid out as a row so the two halves can be told apart: the static part keeps
   its full width and only the username is clipped, which means the button
   always still reads "CONTINUE OFFLINE AS …" rather than losing the verb. */
#btn-offline-resume{display:flex;align-items:center;justify-content:center;gap:.5ch}
#btn-offline-resume .ofr-label{flex:0 0 auto}
#btn-offline-resume .ofr-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hub-guest-tag{background:rgba(57,255,20,.1);border:1px solid rgba(57,255,20,.35);border-radius:20px;padding:4px 10px;font-family:var(--fd);font-size:.5rem;font-weight:700;letter-spacing:.12em;color:var(--lime)}

/* ── HUB SCREEN ── */
#hub-screen{justify-content:flex-start;padding-top:20px}
.hub-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;max-width:1060px;background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:12px 20px;margin-bottom:20px;flex-shrink:0}
.hub-logo-sm{font-family:var(--fd);font-size:.85rem;font-weight:900;background:linear-gradient(90deg,var(--cyan),var(--pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.hub-right{display:flex;align-items:center;gap:10px}
.hub-uname{font-family:var(--fd);font-size:.65rem;color:var(--cyan)}
.hub-pts{background:rgba(255,215,0,.1);border:1px solid rgba(255,215,0,.3);border-radius:20px;padding:5px 14px;font-family:var(--fd);font-size:.6rem;font-weight:700;color:var(--gold)}
.hub-credits{background:rgba(0,245,255,.08);border:1px solid rgba(0,245,255,.3);border-radius:20px;padding:5px 14px;font-family:var(--fd);font-size:.6rem;font-weight:700;color:var(--cyan)}

/* ── BLACK MARKET SCREEN ── */
#market-screen{justify-content:flex-start;padding-top:20px}
.market-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;max-width:1060px;background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:12px 20px;margin-bottom:16px;flex-shrink:0}
.market-title{font-family:var(--fd);font-size:.9rem;font-weight:900;letter-spacing:.08em;background:linear-gradient(90deg,var(--gold),var(--orange));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.market-bal{display:flex;gap:10px;font-family:var(--fd);font-size:.62rem;font-weight:700;color:var(--dim);flex-wrap:wrap}
.market-bal span{background:var(--s2);border:1px solid var(--border);border-radius:20px;padding:5px 14px}
.market-tabs{display:flex;gap:8px;width:100%;max-width:1060px;margin-bottom:16px;flex-wrap:wrap;flex-shrink:0}
.market-tab{font-family:var(--fd);font-size:.62rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:10px 18px;background:var(--s2);border:1px solid var(--border);border-radius:8px;color:var(--dim);cursor:pointer;transition:all .2s}
.market-tab:hover{border-color:var(--border2);color:var(--text)}
.market-tab.active{background:rgba(255,215,0,.1);border-color:var(--gold);color:var(--gold)}
.market-body{width:100%;max-width:1060px;flex:1;overflow-y:auto;padding-bottom:20px}
.market-panel{display:none}
.market-panel.active{display:block}
.convert-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px}
.convert-card{text-align:center}
.convert-rate{font-family:var(--fd);font-size:.6rem;color:var(--gold);letter-spacing:.1em;margin:12px 0 20px}
.convert-preview{font-family:var(--fd);font-size:1rem;color:var(--cyan);margin:16px 0;font-weight:700}
.shop-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:18px 14px}
.shop-card{position:relative;background:var(--s1);border:1px solid var(--border);border-radius:var(--rl);padding:20px 14px;text-align:center;transition:all .2s ease}
.shop-card.equipped{border-color:var(--gold);box-shadow:0 8px 24px rgba(255,215,0,.15)}
.shop-swatch{width:56px;height:56px;border-radius:50%;margin:0 auto 12px;border:2px solid rgba(255,255,255,.2)}
.shop-emoji{font-size:2.4rem;margin-bottom:10px}
.shop-name{font-family:var(--fd);font-size:.62rem;font-weight:700;margin-bottom:11px;letter-spacing:.04em}
.shop-tag{display:inline-block;font-family:var(--fd);font-size:.5rem;color:var(--gold);border:1px solid var(--gold);border-radius:20px;padding:2px 10px;margin-bottom:10px;letter-spacing:.1em}
.shop-btn{width:100%}

/* ── RARITY TIERS (pricier Black Market items get flashier treatment) ── */
.rarity-badge{display:inline-block;font-family:var(--fd);font-size:.48rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;padding:2px 10px;border-radius:20px;margin-bottom:8px}
.rarity-badge.rare{color:var(--cyan);border:1px solid var(--cyan);background:rgba(0,245,255,.08)}
.rarity-badge.epic{color:#e879f9;border:1px solid #e879f9;background:rgba(232,121,249,.08)}
/* LEGENDARY badge — the shimmer IS the text: the gradient is clipped to the
   glyphs, so this badge's `background-image` is not decoration that can be
   swapped out, it is the only thing painting the word. Two consequences the
   rules below have to respect:
     · the pill can have no background FILL — background-clip:text clips every
       background layer, colour included — so the tint arrives as an inset
       shadow, which paints over the background box and under the text.
     · any later rule that sets `background-image` on .rarity-badge erases the
       word entirely (see the mode-3d override further down).
   `color` is the safety net for engines without background-clip:text, where
   -webkit-text-fill-color is skipped and the flat gold shows instead. */
.rarity-badge.legendary{
  --legend-shimmer:linear-gradient(90deg,var(--gold),var(--pink),var(--cyan),var(--gold));
  position:relative;color:var(--gold);border:1px solid var(--gold);
  box-shadow:inset 0 0 0 20px rgba(255,215,0,.10);
  background-image:var(--legend-shimmer);background-size:300% 100%;
  -webkit-background-clip:text;background-clip:text;
  filter:drop-shadow(0 0 3px rgba(255,215,0,.5));
  animation:legendary-text-shift 3s linear infinite
}
@supports ((-webkit-background-clip:text) or (background-clip:text)){
  .rarity-badge.legendary{-webkit-text-fill-color:transparent}
}
@keyframes legendary-text-shift{0%{background-position:0% 50%}100%{background-position:300% 50%}}

/* RARE — gentle cyan breathing glow */
@keyframes pulse-rare{0%,100%{box-shadow:0 0 0 rgba(0,245,255,0)}50%{box-shadow:0 0 16px rgba(0,245,255,.4)}}
.shop-card.rarity-rare{animation:pulse-rare 2.6s ease-in-out infinite}

/* EPIC — stronger glow + a diagonal light sweep across the card */
@keyframes pulse-epic{0%,100%{box-shadow:0 0 0 rgba(232,121,249,0)}50%{box-shadow:0 0 24px rgba(232,121,249,.45)}}
.shop-card.rarity-epic{animation:pulse-epic 2.2s ease-in-out infinite;overflow:hidden}
.shop-card.rarity-epic::after{content:'';position:absolute;top:0;left:-150%;width:60%;height:100%;background:linear-gradient(100deg,transparent,rgba(255,255,255,.28),transparent);animation:shimmer-sweep 2.6s ease-in-out infinite;pointer-effects:none}
@keyframes shimmer-sweep{0%{left:-150%}60%,100%{left:150%}}

/* LEGENDARY — pulsing gold glow + colour-shifting badge text (no rotation) */
.shop-card.rarity-legendary{border-color:var(--gold);animation:pulse-legendary 2.2s ease-in-out infinite}
@keyframes pulse-legendary{0%,100%{box-shadow:0 0 20px rgba(255,215,0,.22)}50%{box-shadow:0 0 38px rgba(255,215,0,.5)}}
.shop-card.rarity-legendary .shop-emoji{animation:float-icon 2.4s ease-in-out infinite}
@keyframes float-icon{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-6px) scale(1.08)}}

/* Matrix Cascade — animated "digital rain" swatch, always legendary tier */
.shop-swatch.matrix-swatch{background:repeating-linear-gradient(0deg,rgba(0,255,65,.95) 0 2px,rgba(0,20,5,.95) 2px 6px);background-size:100% 24px;box-shadow:0 0 20px rgba(0,255,65,.6);animation:matrix-fall 1s linear infinite,pulse-legendary 2.2s ease-in-out infinite,float-icon 2.4s ease-in-out infinite}
@keyframes matrix-fall{0%{background-position:0 0}100%{background-position:0 24px}}

/* ── ANIMATED EQUIPPED CURSORS (epic & legendary tiers) ── */
#custom-cursor-fx{position:fixed;top:0;left:0;display:none;pointer-events:none;z-index:99999;transform:translate(-50%,-50%)}
.ccur-epic{font-size:26px;filter:drop-shadow(0 0 6px #e879f9)}
.ccur-epic .ccur-inner{display:inline-block;animation:ccur-epic-spin 1.2s ease-in-out infinite}
@keyframes ccur-epic-spin{0%,100%{transform:scale(1) rotate(0deg)}50%{transform:scale(1.3) rotate(180deg)}}
.ccur-legendary .ccur-code{font-family:var(--fd),monospace;font-size:12px;font-weight:700;letter-spacing:2px;color:#00ff41;text-shadow:0 0 6px #00ff41,0 0 14px #00ff41;white-space:nowrap;display:inline-block;animation:ccur-code-flicker 1.4s ease-in-out infinite}
@keyframes ccur-code-flicker{0%,100%{opacity:1;transform:translateY(0)}50%{opacity:.75;transform:translateY(-3px)}}

/* ── HUB BODY (Leaderboard & Games) ── */
.hub-body{display:grid;grid-template-columns:1fr 300px;gap:26px;width:100%;max-width:1060px}
.games-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:14px;align-content:start}
.game-card{background:var(--s1);border:1px solid var(--border);border-radius:var(--rl);padding:22px 16px;text-align:center;cursor:pointer;transition:transform .25s ease,border-color .25s ease,box-shadow .25s ease;position:relative;overflow:hidden}
.game-card:hover{transform:translateY(-5px) scale(1.02)}
.game-card[data-c="cyan"]:hover  {border-color:var(--cyan);  box-shadow:0 8px 30px rgba(0,245,255,.2)}
.game-card[data-c="pink"]:hover  {border-color:var(--pink);  box-shadow:0 8px 30px rgba(255,0,144,.2)}
.game-card[data-c="lime"]:hover  {border-color:var(--lime);  box-shadow:0 8px 30px rgba(57,255,20,.2)}
.game-card[data-c="gold"]:hover  {border-color:var(--gold);  box-shadow:0 8px 30px rgba(255,215,0,.2)}
.game-card[data-c="purple"]:hover{border-color:var(--purple);box-shadow:0 8px 30px rgba(168,85,247,.2)}
.game-card[data-c="orange"]:hover{border-color:var(--orange);box-shadow:0 8px 30px rgba(255,102,0,.2)}
.game-card[data-c="red"]:hover   {border-color:var(--red);   box-shadow:0 8px 30px rgba(255,36,66,.2)}
.gc-icon{font-size:2.8rem;display:block;margin-bottom:12px}
.gc-name{font-family:var(--fd);font-size:.6rem;font-weight:700;letter-spacing:.08em;margin-bottom:11px}
.gc-name[data-c="cyan"]  {color:var(--cyan)}
.gc-name[data-c="pink"]  {color:var(--pink)}
.gc-name[data-c="lime"]  {color:var(--lime)}
.gc-name[data-c="gold"]  {color:var(--gold)}
.gc-name[data-c="purple"]{color:var(--purple)}
.gc-name[data-c="orange"]{color:var(--orange)}
.gc-name[data-c="red"]   {color:var(--red)}
.gc-desc{font-size:.75rem;color:var(--dim);line-height:1.45;margin-bottom:15px}
.gc-pts{display:inline-block;padding:3px 12px;border-radius:20px;font-family:var(--fd);font-size:.5rem;font-weight:700;border:1px solid}
.gc-pts[data-c="cyan"]  {color:var(--cyan);  border-color:var(--cyan);  background:rgba(0,245,255,.06)}
.gc-pts[data-c="pink"]  {color:var(--pink);  border-color:var(--pink);  background:rgba(255,0,144,.06)}
.gc-pts[data-c="lime"]  {color:var(--lime);  border-color:var(--lime);  background:rgba(57,255,20,.06)}
.gc-pts[data-c="gold"]  {color:var(--gold);  border-color:var(--gold);  background:rgba(255,215,0,.06)}
.gc-pts[data-c="purple"]{color:var(--purple);border-color:var(--purple);background:rgba(168,85,247,.06)}
.gc-pts[data-c="orange"]{color:var(--orange);border-color:var(--orange);background:rgba(255,102,0,.06)}
.gc-pts[data-c="red"]   {color:var(--red);   border-color:var(--red);   background:rgba(255,36,66,.06)}

/* ── LEADERBOARD ── */
.lb-panel{background:var(--s1);border:1px solid var(--border);border-radius:var(--rl);padding:18px;overflow-y:auto;max-height:520px}
.lb-title{font-family:var(--fd);font-size:.65rem;font-weight:700;color:var(--gold);letter-spacing:.15em;text-transform:uppercase;margin-bottom:14px;text-align:center}
.lb-row{position:relative;display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:8px;border:1px solid var(--border);background:var(--s2);margin-bottom:7px}

/* "You" is declared before the podium tints on purpose: a logged-in player
   who is also on the podium keeps their medal colour on the row, and the blue
   shows as the edge marker, name and glow instead of overwriting it. */
.lb-row.me{border-color:var(--blue);background:rgba(59,130,246,.07)}
.lb-row.me .lb-name{color:var(--blue)}
/* The glow would swamp a medal tint, so only a player outside the top three
   gets it — on the podium the edge marker and blue name carry the signal. */
.lb-row.me:not(.r1):not(.r2):not(.r3){box-shadow:0 0 14px rgba(59,130,246,.18)}
.lb-row.me::before{content:'';position:absolute;left:0;top:7px;bottom:7px;width:3px;border-radius:0 3px 3px 0;background:var(--blue);box-shadow:0 0 8px var(--blue)}

/* Podium: gold, silver, bronze. */
.lb-row.r1{border-color:rgba(255,215,0,.55);background:rgba(255,215,0,.07)}
.lb-row.r2{border-color:rgba(201,212,224,.7);background:rgba(201,212,224,.09)}
.lb-row.r3{border-color:rgba(205,127,50,.6);background:rgba(205,127,50,.08)}
.lb-row.r1 .lb-score{color:var(--gold)}
.lb-row.r2 .lb-score{color:var(--silver)}
.lb-row.r3 .lb-score{color:var(--bronze)}

.lb-rank{font-family:var(--fd);font-size:.9rem;font-weight:900;width:26px;text-align:center;color:var(--dimmer)}
.lb-name{flex:1;font-weight:700;font-size:.82rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lb-score{font-family:var(--fd);font-size:.65rem;color:var(--gold)}
.lb-empty{color:var(--dim);font-size:.82rem;text-align:center;padding:30px 0}

/* ── DIFFICULTY SELECTOR ── */
.difficulty-selector{
  display:flex;
  align-items:center;
  gap:8px;
  margin-bottom:20px;
}
.difficulty-label{
  font-family:var(--fd);
  font-size:.65rem;
  color:var(--dim);
  text-transform:uppercase;
  letter-spacing:.1em;
}
.difficulty-options{
  display:flex;
  gap:8px;
}
.diff-btn{
  font-family:var(--fd);
  font-size:.65rem;
  font-weight:700;
  letter-spacing:.08em;
  text-transform:uppercase;
  padding:10px 16px;
  border:none;
  border-radius:6px;
  background:var(--s2);
  color:var(--text);
  cursor:pointer;
  transition:all .2s;
  position:relative;
  overflow:hidden;
}
.diff-btn:hover{
  background:rgba(255,255,255,.14);
  border-color:var(--border2);
  transform:translateY(-2px);
}
.diff-btn:active{
  transform:scale(0.96);
}
.diff-btn.stable-active{
  background:rgba(0,255,102,.1);
  color:#00ff66;
  box-shadow:0 0 10px #00ff66;
}
.diff-btn.stable-active:hover{
  box-shadow:0 0 15px #00ff66;
}
.diff-btn.overclock-active{
  background:rgba(255,204,0,.1);
  color:#ffcc00;
  box-shadow:0 0 10px #ffcc00;
}
.diff-btn.overclock-active:hover{
  box-shadow:0 0 15px #ffcc00;
}
.diff-btn.meltdown-active{
  background:rgba(255,0,51,.1);
  color:#ff0033;
  box-shadow:0 0 10px #ff0033;
  animation:pulse 1.5s infinite;
}
.diff-btn.meltdown-active:hover{
  box-shadow:0 0 15px #ff0033;
}
@keyframes pulse{
  0%,100%{box-shadow:0 0 0 rgba(255,0,51,.4);}
  50%{box-shadow:0 0 12px rgba(255,0,51,.6);}
}

/* Difficulty Selector (new naming) */
.diff-selector{
  display:flex;
  align-items:center;
  gap:12px;
  margin-bottom:20px;
  flex-direction:column;
  position:relative;
  isolation:isolate;
  background:var(--s1);
  border:1px solid var(--border);
  border-radius:var(--r);
  padding:12px 20px;
  transition:border-color .4s ease;
}
/* Ambient glow "emitted from behind" the panel — color follows the active tier */
.diff-selector::before{
  content:'';
  position:absolute;
  inset:-4px;
  z-index:-1;
  border-radius:inherit;
  background:transparent;
  filter:blur(22px);
  opacity:.45;
  transition:background .5s ease, opacity .3s ease;
  pointer-events:none;
}
.diff-selector.tier-stable{border-color:rgba(0,255,102,.4)}
.diff-selector.tier-stable::before{background:#00ff66}
.diff-selector.tier-overclocked{border-color:rgba(255,204,0,.4)}
.diff-selector.tier-overclocked::before{background:#ffcc00}
.diff-selector.tier-meltdown{border-color:rgba(255,0,51,.45)}
.diff-selector.tier-meltdown::before{background:#ff0033}
/* Brief brighter pulse the instant a tier button is clicked */
.diff-selector.flash::before{opacity:.9}
.diff-label{
  font-family:var(--fd);
  font-size:.65rem;
  color:var(--dim);
  text-transform:uppercase;
  letter-spacing:.1em;
}
.diff-btns{
  display:flex;
  gap:8px;
}
.diff-stable,
.diff-overclocked,
.diff-meltdown{
  /* inherit from .diff-btn */
}
/* Active glow now matches the specific tier, not a hardcoded green */
.diff-btn.diff-stable.active{
  background:rgba(0,255,102,.1);
  color:#00ff66;
  box-shadow:0 0 10px #00ff66;
}
.diff-btn.diff-stable.active:hover{
  box-shadow:0 0 15px #00ff66;
}
.diff-btn.diff-overclocked.active{
  background:rgba(255,204,0,.1);
  color:#ffcc00;
  box-shadow:0 0 10px #ffcc00;
}
.diff-btn.diff-overclocked.active:hover{
  box-shadow:0 0 15px #ffcc00;
}
.diff-btn.diff-meltdown.active{
  background:rgba(255,0,51,.1);
  color:#ff0033;
  box-shadow:0 0 10px #ff0033;
  animation:pulse 1.5s infinite;
}
.diff-btn.diff-meltdown.active:hover{
  box-shadow:0 0 15px #ff0033;
}
.diff-mult{
  font-family:var(--fd);
  font-size:.65rem;
  color:var(--gold);
  margin-top:6px;
}
/* A frozen dial: mid-round anywhere, and for the guest of a room — the round
   runs on the HOST'S tier, so the guest's panel is a readout, not a control.
   Dimmed rather than hidden, because the tier is the thing they most need to
   know before the countdown starts. */
.diff-selector.locked .diff-btn{cursor:not-allowed}
.diff-selector.locked .diff-btn:not(.active){opacity:.4}
/* The active tier keeps its full glow — the hover reset is only for the ones
   that are no longer choices. */
.diff-selector.locked .diff-btn:not(.active):hover{background:var(--s2);transform:none}
.diff-selector.locked .diff-btn:active{transform:none}
.diff-note{
  font-family:var(--fd);
  font-size:.5rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--dim);
  text-align:center;
  margin-top:2px;
}
.diff-note:empty{display:none}
/* The arena's copy is sized by its own content and centred, like the hub's —
   left to stretch it became a full-width banner over the duel grid and stopped
   reading as the same control. */
.mp-diff{width:max-content;max-width:100%;margin-left:auto;margin-right:auto}

/* ── GAME LAYOUT ENGINE ── */
/* Every pixel of chrome above the board comes straight off the play field,
   which is aspect-locked and therefore sized by the height left over. */
#game-screen{justify-content:flex-start;padding-top:10px}
/* The header used to be one space-between row: Quit · title · hint · pills.
   Every minigame supplies a different hint length, and Tetris adds a LEVEL
   pill, so the title landed in a different spot in each game — and in the
   crowded ones it ended up pressed against the Quit button. Between them
   those four items nearly fill the 700px row, so there is no width left to
   centre the title reliably. It gets its own line instead: dead-centre,
   identical in every game, with Quit and the pills on the row beneath. */
/* THREE tracks on the control row, not two: Quit, the readouts, and the action
   cluster. The cluster used to live inside .g-stats and therefore inside the
   readouts' track, which meant a long readout row could push it around, break
   it up, or shove it off the edge. Its own track cannot be reached into. */
.g-hdr{display:grid;grid-template-columns:auto 1fr auto;align-items:center;column-gap:16px;row-gap:12px;width:100%;max-width:max(var(--board-w,560px),560px);margin-bottom:12px}
.g-title{grid-column:1/-1;grid-row:1;text-align:center;font-family:var(--fd);font-size:.95rem;font-weight:700;letter-spacing:.05em}
.g-hdr > #btn-quit{grid-column:1;grid-row:2;justify-self:start}
.g-hdr > .g-actions{grid-column:3;grid-row:2;justify-self:end}
/* The reserved slots below cost this row width, and the busiest one it ever
   carries — LEVEL + ping + clock + score, in a Matrix Stack-Off duel — has only
   520px to live in (the header keeps a 20px notch inset each side). So the
   pills tighten by the few px the slots take back: the row then fits that duel
   at any score, which it did NOT before — a five-digit score used to push it
   ~18px past the board edge. */
.g-stats{grid-column:2;grid-row:2;justify-self:end;display:flex;align-items:center;gap:6px;
  /* Wrapping is allowed at EVERY width now, not only on a phone. The readouts
     are a variable-length list — Boss Rush and Endless each insert a pill at
     the front, Tetris adds LEVEL, a duel adds ping — so the row can outgrow
     its track on a desktop too, and a row that cannot wrap overflows the board
     edge instead. What made wrapping look broken was never the wrap: it was
     the BUTTONS wrapping one at a time. They are one item now (.g-actions), so
     a wrap can only ever put the pills on their own line. */
  flex-wrap:wrap;justify-content:flex-end;row-gap:6px}

/* ══ 📐 THE COMPACTION TIERS — applied by fitHud(), never by hand ══
   Three steps of increasingly aggressive tightening, added one at a time and
   cumulatively until the readout row fits on ONE line. See fitHud() in app.js
   for why the header cannot simply be made wider.
   Tier 1 takes only padding and gaps — nothing is lost, and it is enough on its
   own for the common case of a 560px board carrying a pace ghost. */
.g-hdr.hud-t1{column-gap:10px}
.g-hdr.hud-t1 .g-stats{gap:5px}
.g-hdr.hud-t1 .stat-pill{padding:4px 7px}
.g-hdr.hud-t1 .ghost-pace{padding:4px 8px;gap:5px}
.g-hdr.hud-t1 .g-actions{gap:5px}
.g-hdr.hud-t1 .g-actions .btn-sm{min-width:30px;min-height:30px;padding:4px 6px}

/* Tier 2 drops the pace ghost's TARGET — "PB 1,200". It is the only thing in
   the row that never changes during a round, so it is the only thing a player
   can afford to lose: the delta beside it still says everything actionable,
   and the target is on the results card at the end either way. */
.g-hdr.hud-t2 .gp-target{display:none}
.g-hdr.hud-t2 .ghost-pace{gap:4px}

/* Tier 3 is a genuinely full row on a genuinely narrow board — Boss Rush with a
   pace ghost, say. The ghost icon goes (the colour already says ahead/behind)
   and everything shrinks a step. The delta keeps a fixed slot, just a tighter
   one: it must not start moving again. */
.g-hdr.hud-t3 .gp-ico{display:none}
.g-hdr.hud-t3 .stat-pill,
.g-hdr.hud-t3 .ghost-pace{font-size:.53rem;padding:4px 6px}
.g-hdr.hud-t3 .gp-delta{min-width:8ch}

/* ⚙️ THE ACTION CLUSTER — audio, photo, fullscreen.
   One flex item, never broken up, never squeezed: these are the only controls
   on the screen the player NEEDS to be able to hit mid-round (mute, and the
   way back out of fullscreen), so they are the one thing in the header that is
   not allowed to move somewhere else when the readouts beside them grow. */
.g-actions{display:flex;align-items:center;gap:6px;flex-wrap:nowrap;flex-shrink:0}
/* A comfortable, EQUAL target for all three. They were sized by their glyphs
   before, so 🔊 / 📸 / ⛶ came out three different widths and the cluster read
   as an accident rather than a set. 34px is the smallest square that still
   looks deliberate beside a 24px pill; the phone block below takes it to 44. */
.g-actions .btn-sm{min-width:34px;min-height:34px;display:inline-flex;
  align-items:center;justify-content:center;padding:5px 8px;line-height:1}
.stat-pill{background:var(--s2);border:1px solid var(--border);border-radius:20px;padding:5px 10px;font-family:var(--fd);font-size:.6rem;font-weight:700;white-space:nowrap;
  /* Equal-width digits: without this a 1 is narrower than a 0 and the readout
     twitches on every tick even when the digit COUNT hasn't changed. */
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}
/* Every number in here is a live counter, and the row is right-aligned — so a
   score going 9 → 10 → 100 widened its pill and shoved every pill to its left
   along with it, mid-game. Each readout now owns a fixed slot wide enough for
   the biggest number it can reach, so the row is laid out once and then holds
   still no matter what the numbers do. */
.stat-pill em{font-style:normal;color:var(--cyan);display:inline-block;text-align:center}
#g-pts{min-width:5ch}        /* uncapped in Cyber Arena — five digits of room */
/* Every other game counts down from a two-digit clock (90s at most), so 3ch
   used to be plenty — but the Hover-Bike Run has no clock at all: elapsed just
   climbs for as long as the run survives, same shape as Cyber Arena's score.
   4ch covers that (and Battle Bots' fixed "M:SS", and the ∞ / — placeholders)
   without reserving Arena-sized room for a number that stays two digits. */
#g-time{min-width:4ch}
#tetris-lvl{min-width:2ch}
#bb-ram{min-width:3ch}
#mp-ping{min-width:5ch}      /* "999ms" — mpPing() clamps to keep it in the slot */
.prog-bar{width:100%;max-width:max(var(--board-w,560px),560px);height:5px;background:var(--s2);border-radius:3px;margin-bottom:12px;overflow:hidden;flex-shrink:0}
.prog-fill{height:100%;border-radius:3px;transition:width .2s ease-out;background:linear-gradient(90deg,var(--cyan),var(--purple))}
.g-area{width:100%;max-width:1060px;display:flex;flex-direction:column;align-items:center}

/* Game Controls Hint — a full-width caption on its own row, so a long hint
   ("DRAG = MOVE · TAP = SLASH · DOUBLE-TAP = ABILITY · ⚡ = DASH") can never
   squeeze the title or the Quit button. */
.g-controls{
  grid-column:1/-1;
  grid-row:3;
  text-align:center;
  font-family:var(--fd);
  font-size:.65rem;
  color:var(--dim);
  letter-spacing:.02em;
}
/* Games with no hint (Click Frenzy, Memory, Math) skip the row entirely. */
.g-controls:empty{display:none}

/* Canvas & Controls */
#arcade-canvas{background:#0d0d11;border:2px solid var(--border);border-radius:8px;display:block}
/* The pad spans the board rather than a fixed 400px, so it stays flush with
   the play field's edges now that the board grows to fill the screen. */
#arcade-controls{display:flex;width:100%;max-width:100%;gap:10px;margin-top:10px}
.ctrl-btn{flex:1;background:var(--s2);border:1px solid var(--border);color:#fff;padding:14px;font-family:var(--fd);border-radius:8px;cursor:pointer}

/* Marks a control that permanently commits something (Tetris's hard drop), so
   it reads as its own thing rather than another grey sibling of ◀ / ⟳ / ▶.
   Opt-in via setControls({accentDrop:true}) — Snake's ↓ is just an arrow. */
.ctrl-btn.accent{background:rgba(168,85,247,.16);border-color:rgba(168,85,247,.55);color:#d8b4fe;font-weight:700;letter-spacing:.04em}
.ctrl-btn.accent:active{background:rgba(168,85,247,.34);transform:scale(.96)}
/* Bare-glyph buttons (◀ ↑ ⟳) get a size of their own — at label size a lone
   arrow is a speck, while worded buttons still need the smaller type. */
.ctrl-btn.glyph{font-size:1.3rem;line-height:1}

/* Shrink-wraps the canvas so #tetris-next-wrap and #touch-hint position
   against the play field, not against the holder that also owns the pad. */
#board-frame{position:relative;width:fit-content;max-width:100%;margin:0 auto}

/* ── ⚔️ BATTLE BOTS — DEPLOY DECK ── */
/* The deploy deck under the board — the whole bot roster plus the EMP surge
   and the RAM upgrade. Tracks the board's width like .g-hdr and .prog-bar do,
   so the whole game reads as one column, and is allowed to wrap: fitCanvas()
   measures the deck's real height, so on a narrow screen a second row of
   cards costs the board height instead of running off the bottom.
   Built by startBattleBots() and emptied by stopGame(). */
#bb-deck{display:flex;flex-wrap:wrap;gap:10px;width:100%;max-width:max(var(--board-w,560px),560px);margin-top:10px;flex-shrink:0}
.bb-btn{
  position:relative;overflow:hidden;flex:1;min-width:0;
  display:flex;flex-direction:column;align-items:center;gap:3px;
  background:var(--s2);border:1.5px solid var(--bc);border-radius:10px;
  padding:9px 4px;cursor:pointer;color:#fff;font-family:var(--fd);
  box-shadow:0 0 14px -4px var(--bc),inset 0 0 18px -12px var(--bc);
  transition:transform .12s ease,box-shadow .2s ease,opacity .2s ease,border-color .2s ease
}
.bb-btn:hover:not(.broke):not(.maxed){transform:translateY(-2px);box-shadow:0 0 24px -4px var(--bc)}
.bb-btn:active{transform:scale(.96)}
.bb-ico{font-size:1.35rem;line-height:1}
.bb-name{font-size:.55rem;font-weight:700;letter-spacing:.1em;color:var(--bc);white-space:nowrap}
.bb-cost{font-size:.52rem;font-weight:700;letter-spacing:.06em;color:var(--dim);white-space:nowrap}
/* Unaffordable, but still clickable — the deny blip and the toast are how you
   find out how much more RAM you need. */
.bb-btn.broke{opacity:.4;border-color:var(--border);box-shadow:none}
.bb-btn.broke .bb-name{color:var(--dim)}
.bb-btn.maxed{opacity:.55;border-color:var(--border);box-shadow:none}
/* Deploy cooldown — a shade that recedes upward as the bay reloads. */
.bb-btn::after{content:'';position:absolute;left:0;right:0;top:0;height:calc(var(--cd,0) * 100%);background:rgba(4,4,14,.72);pointer-events:none;transition:height .08s linear}
/* …and the seconds left, over the top of it. The shade says how MUCH of the
   reload is left; this says how LONG, because "about a third of a bar" is not
   something you can plan the next deploy around, and the whole skill of the
   deck is knowing what will be back before the next wave lands.

   An attribute rather than a text node: paintDeck() runs every frame, and the
   DOM is only touched when the whole second changes. The rule keys off the
   attribute's presence, so a card that is ready simply has no data-cd. */
.bb-btn[data-cd]::before{
  content:attr(data-cd);
  position:absolute;inset:0;z-index:2;
  display:flex;align-items:center;justify-content:center;
  font-family:var(--fd);font-weight:900;font-size:1.2rem;letter-spacing:.02em;
  color:#fff;text-shadow:0 0 4px rgba(4,4,14,.95),0 0 12px rgba(4,4,14,.9);
  pointer-events:none
}

/* NEXT-piece preview (was inline styles; moved here so the mobile rules below
   can actually shrink it — inline wins over a stylesheet otherwise). */
#tetris-next-wrap{position:absolute;top:10px;right:10px;background:var(--s2);border:1px solid var(--border);border-radius:8px;padding:8px;text-align:center;pointer-events:none}
#tetris-next-wrap .next-label{font-family:var(--fd);font-size:.6rem;color:var(--cyan);margin-bottom:4px}
#nextCanvas{display:block;width:80px;height:80px}

/* ── TOUCH TUTORIAL OVERLAY ──
   One line of "here's how you play this with a finger", shown over the canvas
   for the first few seconds and dismissed by the first touch. */
#touch-hint{
  position:absolute;left:50%;bottom:18px;transform:translateX(-50%);
  /* Sized against the viewport, not the board — on a scaled-down board 88%
     of the frame wrapped one sentence into six lines and buried the game.
     Overflowing into the margin beside the board is fine and stays centred. */
  display:none;width:max-content;max-width:min(86vw,420px);padding:10px 18px;
  background:rgba(4,4,14,.82);border:1px solid rgba(0,245,255,.45);border-radius:24px;
  font-family:var(--fd);font-size:.58rem;line-height:1.7;letter-spacing:.08em;
  color:var(--cyan);text-align:center;text-shadow:0 0 10px rgba(0,245,255,.5);
  box-shadow:0 0 24px rgba(0,245,255,.18);
  pointer-events:none;z-index:5;opacity:0;transition:opacity .35s ease
}
#touch-hint.show{display:block;opacity:1}

/* ── Click Frenzy ── */
.click-wrap{display:flex;flex-direction:column;align-items:center;gap:16px}
/* Five-rem digits: an uneven 1 is very visible at this size, and Click Frenzy
   redraws this number several times a second. */
#click-count{font-family:var(--fd);font-size:5rem;font-weight:900;color:var(--cyan);line-height:1;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;min-width:4ch;text-align:center}
.click-label{color:var(--dim);font-weight:600;font-size:.8rem;letter-spacing:.2em;text-transform:uppercase}
#click-btn{width:180px;height:180px;border-radius:50%;border:none;background:linear-gradient(135deg,var(--cyan),var(--purple));font-size:3.5rem;cursor:pointer;box-shadow:0 0 50px rgba(0,245,255,.3)}
#click-btn:active{transform:scale(.9)}

/* ── Memory Match Layout ─ */
.memory-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;width:100%;max-width:340px}
.mem-card{height:75px;background:var(--s2);border:1px solid var(--border);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:2rem;cursor:pointer;user-select:none;transition:background .2s}
.mem-card.flipped{background:rgba(57,255,20,0.15);border-color:var(--lime)}

/* ── Math Blitz Layout ─ */
.math-wrap{text-align:center;width:100%;max-width:340px}
#math-question{font-family:var(--fd);font-size:3rem;margin-bottom:20px;color:var(--gold)}
#math-answer{width:100%;background:var(--s2);border:1px solid var(--border);color:#fff;padding:12px;font-size:1.5rem;text-align:center;border-radius:8px;margin-bottom:14px;outline:none}

/* ── Reaction Time Layout ─ */
.reaction-box{width:100%;max-width:400px;height:300px;border-radius:12px;border:2px solid var(--border);display:flex;align-items:center;justify-content:center;cursor:pointer;user-select:none;transition:background 0.1s}
#reaction-text{font-family:var(--fd);font-weight:700;font-size:1.5rem}

/* ── Node Hacker Layout ─ */
/* The keypad is aspect-locked to a square and sized by whichever runs out
   first — the column, 380px, or the viewport height. Same reasoning as
   fitCanvas(): a board whose bottom row is under the fold is unplayable. */
.hack-wrap{display:flex;flex-direction:column;align-items:center;gap:14px;width:100%;max-width:400px}
.hack-readout{width:100%;text-align:center;background:var(--s1);border:1px solid rgba(57,255,20,.28);border-radius:var(--r);padding:12px 14px}
.hack-status{font-family:var(--fd);font-size:.7rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--lime);text-shadow:0 0 10px rgba(57,255,20,.55);min-height:17px;line-height:1.4}
.hack-status.warn{color:var(--gold);text-shadow:0 0 10px rgba(255,215,0,.55)}
.hack-status.fail{color:var(--red);text-shadow:0 0 10px rgba(255,36,66,.6)}

/* One pip per node in the current sequence — fills left-to-right as you
   replicate it, so progress through a 9-long chain is readable at a glance. */
.hack-seq{display:flex;justify-content:center;flex-wrap:wrap;gap:5px;margin-top:10px;min-height:7px}
.hack-pip{width:16px;height:5px;border-radius:3px;background:var(--s2);border:1px solid var(--border);transition:background .18s,box-shadow .18s}
.hack-pip.done{background:var(--lime);border-color:var(--lime);box-shadow:0 0 8px rgba(57,255,20,.7)}

.hack-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;width:min(100%,380px,54vh);aspect-ratio:1/1}
/* --nc is the node's own neon, assigned per row in startHacker(). */
.hack-node{
  position:relative;display:flex;align-items:center;justify-content:center;
  background:var(--s2);border:1.5px solid var(--border);border-radius:10px;
  cursor:pointer;overflow:hidden;
  transition:background .12s ease,border-color .12s ease,box-shadow .12s ease,transform .12s ease
}
.hack-node::before{
  content:'';position:absolute;inset:0;border-radius:inherit;
  background:radial-gradient(circle at 50% 50%,var(--nc),transparent 70%);
  opacity:.07;transition:opacity .12s ease
}
.hack-node .hack-key{
  font-family:var(--fd);font-size:.62rem;font-weight:700;letter-spacing:.06em;
  color:var(--dimmer);position:relative;transition:color .12s ease,text-shadow .12s ease
}
/* Scoped to real pointers — on a phone :hover latches after a tap and would
   leave half the keypad glowing as if it were part of the sequence. */
@media (hover: hover){
  .hack-node:not(.lit):hover{border-color:var(--nc);transform:translateY(-2px)}
  .hack-node:not(.lit):hover .hack-key{color:var(--dim)}
}
/* Lit = the mainframe is broadcasting this node, or you just pressed it. */
/* --nc-soft/--nc-glow are pre-mixed rgba by startHacker() via hexToRgba(),
   rather than color-mix() — same result, no dependency on a newer colour
   function than the rest of the sheet assumes. */
.hack-node.lit{background:var(--nc-soft);border-color:var(--nc);box-shadow:0 0 22px var(--nc),inset 0 0 18px var(--nc-glow);transform:scale(1.05)}
.hack-node.lit::before{opacity:.5}
.hack-node.lit .hack-key{color:#fff;text-shadow:0 0 10px var(--nc)}
.hack-node.err{background:rgba(255,36,66,.24);border-color:var(--red);box-shadow:0 0 24px rgba(255,36,66,.75);animation:hackShake .4s ease}
.hack-node.err .hack-key{color:#fff;text-shadow:0 0 10px var(--red)}
@keyframes hackShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
/* Locked out while the sequence is playing back, so you can't queue taps. */
.hack-grid.locked .hack-node{cursor:default}
.hack-grid.locked{opacity:.94}

/* ── 🔌 OVERCLOCK PATH ──
   --pc is the board's neon, assigned by startOverclockPath() from the equipped
   Black Market colour; --pc-soft / --pc-glow are pre-mixed rgba of it, the same
   way Node Hacker mixes --nc, rather than color-mix(). */
.path-wrap{display:flex;flex-direction:column;align-items:center;gap:14px;width:100%;max-width:420px}
.path-readout{width:100%;text-align:center;background:var(--s1);border:1px solid rgba(168,85,247,.28);border-radius:var(--r);padding:12px 14px}
.path-status{font-family:var(--fd);font-size:.7rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--purple);text-shadow:0 0 10px rgba(168,85,247,.55);min-height:17px;line-height:1.4}
.path-status.warn{color:var(--gold);text-shadow:0 0 10px rgba(255,215,0,.55)}
/* The RECALIBRATING flash. steps(2) rather than a fade, because a clean blink
   reads as a fault light and a fade reads as a transition. */
.path-status.fail{color:var(--red);text-shadow:0 0 10px rgba(255,36,66,.6);animation:pathBlink .34s steps(2,end) 2}
.path-status.win{color:var(--lime);text-shadow:0 0 12px rgba(57,255,20,.65)}
@keyframes pathBlink{0%,100%{opacity:1}50%{opacity:.24}}

/* How much of the circuit is carrying current. */
.path-meter{margin-top:10px;height:5px;border-radius:3px;background:var(--s2);border:1px solid var(--border);overflow:hidden}
.path-meter-fill{height:100%;width:0;border-radius:3px;background:linear-gradient(90deg,var(--pc,var(--purple)),#fff);box-shadow:0 0 10px var(--pc,var(--purple));transition:width .18s ease}

/* The board is one square box with the grid and the trace stacked inside it,
   both pinned to its edges — that shared box is what lets the SVG be drawn in
   the grid's own pixel coordinates. */
.path-board{position:relative;width:min(100%,400px,52vh);aspect-ratio:1/1}
.path-grid{position:absolute;inset:0;z-index:1;display:grid;grid-template-columns:repeat(5,1fr);gap:9px;touch-action:none}
/* Above the nodes so the current reads as one continuous wire threading
   through them, and pointer-transparent so the nodes stay what you touch. */
.path-trace{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:visible}
.trace-glow{fill:none;stroke:var(--pc,var(--purple));stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:.20;filter:blur(3px)}
.trace-core{fill:none;stroke:var(--pc,var(--purple));stroke-width:5;stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 0 6px var(--pc,var(--purple)))}

.path-node{
  position:relative;display:flex;align-items:center;justify-content:center;
  background:var(--s2);border:1.5px solid var(--border);border-radius:10px;
  cursor:pointer;overflow:hidden;
  transition:background .12s ease,border-color .12s ease,box-shadow .12s ease,transform .12s ease
}
.path-node::before{
  content:'';position:absolute;inset:0;border-radius:inherit;
  background:radial-gradient(circle at 50% 50%,var(--pc,var(--purple)),transparent 70%);
  opacity:.06;transition:opacity .12s ease
}
.path-dot{width:9px;height:9px;border-radius:50%;background:var(--dimmer);position:relative;transition:background .12s ease,box-shadow .12s ease,transform .12s ease}
.path-glyph{font-family:var(--fd);font-size:.72rem;font-weight:900;letter-spacing:.06em;color:var(--dimmer);position:relative;transition:color .12s ease,text-shadow .12s ease}
/* Node A — where the current has to start, so it is lit before anything else is. */
.path-node.start{border-color:var(--pc,var(--purple));background:var(--pc-soft,rgba(168,85,247,.2))}
.path-node.start .path-glyph{color:var(--pc,var(--purple));text-shadow:0 0 10px var(--pc,var(--purple))}
/* Dead code — dashed and struck through, because "you cannot route here" has to
   survive being read at a glance with five seconds left. */
.path-node.dead{background:rgba(255,36,66,.10);border-color:rgba(255,36,66,.34);border-style:dashed;cursor:not-allowed}
.path-node.dead::before{background:radial-gradient(circle at 50% 50%,var(--red),transparent 70%);opacity:.05}
.path-node.dead .path-glyph{color:rgba(255,36,66,.7)}
.path-node.on{background:var(--pc-soft,rgba(168,85,247,.2));border-color:var(--pc,var(--purple));box-shadow:0 0 18px var(--pc-glow,rgba(168,85,247,.34)),inset 0 0 16px var(--pc-glow,rgba(168,85,247,.34))}
.path-node.on::before{opacity:.34}
.path-node.on .path-dot{background:#fff;box-shadow:0 0 10px var(--pc,var(--purple));transform:scale(1.15)}
.path-node.on .path-glyph{color:#fff;text-shadow:0 0 10px var(--pc,var(--purple))}
.path-node.head{box-shadow:0 0 26px var(--pc,var(--purple)),inset 0 0 18px var(--pc-glow,rgba(168,85,247,.34));transform:scale(1.06)}
/* Scoped to real pointers — on a phone :hover latches after a tap and would
   leave half the board looking energised. */
@media (hover: hover){
  .path-node:not(.dead):not(.on):hover{border-color:var(--pc,var(--purple));transform:translateY(-2px)}
  .path-node:not(.dead):not(.on):hover .path-dot{background:var(--dim)}
}
/* Invalid move: the whole board kicks red for the length of the lockout. */
.path-grid.recal{animation:pathRecal .5s ease}
.path-grid.recal .path-node:not(.dead){border-color:rgba(255,36,66,.55);background:rgba(255,36,66,.10)}
@keyframes pathRecal{0%,100%{transform:translateX(0)}20%{transform:translateX(-5px)}60%{transform:translateX(5px)}}
.path-grid.done{animation:pathDone .6s ease}
@keyframes pathDone{0%,100%{transform:scale(1)}45%{transform:scale(1.035)}}

/* ── 🎚️ FREQUENCY MODULATOR CONSOLE ──
   Matches #bb-deck's geometry: a full-width row under the board that tracks the
   board's own width rather than the screen's, so the sliders stay the same
   length as the scope they drive. */
#g-freq-ctl{display:flex;gap:14px;width:100%;max-width:max(var(--board-w,560px),560px);margin-top:10px;flex-shrink:0}
.freq-slider{flex:1;min-width:0;background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:9px 13px 12px}
.freq-lbl{display:flex;justify-content:space-between;align-items:baseline;gap:8px;font-family:var(--fd);font-size:.56rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);margin-bottom:8px}
.freq-lbl em{font-style:normal;font-size:.74rem;color:var(--cyan);text-shadow:0 0 8px rgba(0,245,255,.5)}
.freq-range{
  -webkit-appearance:none;appearance:none;display:block;width:100%;height:6px;
  border-radius:3px;background:linear-gradient(90deg,rgba(0,245,255,.30),rgba(255,0,144,.30));
  border:1px solid var(--border);outline:none;cursor:pointer
}
/* Both vendor thumbs, spelled out separately — a browser drops the WHOLE rule
   if it cannot parse one selector in the list, so these must not be grouped. */
.freq-range::-webkit-slider-thumb{
  -webkit-appearance:none;appearance:none;width:20px;height:20px;border-radius:50%;
  background:var(--cyan);border:2px solid #fff;box-shadow:0 0 12px var(--cyan);cursor:pointer
}
.freq-range::-moz-range-thumb{
  width:18px;height:18px;border-radius:50%;
  background:var(--cyan);border:2px solid #fff;box-shadow:0 0 12px var(--cyan);cursor:pointer
}
.freq-range:focus-visible{box-shadow:0 0 0 2px rgba(0,245,255,.45)}

/* ── 🗂️ PACKET SORT RULE STRIP ──
   Same geometry as the two consoles above: a full-width row under the board,
   tracking the BOARD's width rather than the screen's. It carries the one fact
   the mission re-writes every twenty seconds, so it is loud on purpose — the
   flip has to be noticed peripherally, while the player's eyes are on a packet
   halfway down a conveyor. .flip is a one-shot pulse the mission adds and
   removes; the transition on the text is what makes a changed rule read as an
   EVENT rather than as a value that was always that. */
#g-sort-rule{
  display:flex;align-items:center;gap:12px;width:100%;
  max-width:max(var(--board-w,560px),560px);margin-top:10px;flex-shrink:0;
  background:var(--s1);border:1px solid var(--border);border-radius:var(--r);
  padding:9px 14px;overflow:hidden
}
.sr-tag{
  font-family:var(--fd);font-size:.55rem;font-weight:700;letter-spacing:.12em;
  text-transform:uppercase;color:var(--dim);border:1px solid var(--border);
  border-radius:999px;padding:3px 9px;flex-shrink:0;white-space:nowrap
}
.sr-txt{
  flex:1;min-width:0;font-family:var(--fd);font-size:.86rem;font-weight:700;
  letter-spacing:.05em;text-transform:uppercase;color:var(--orange);
  text-shadow:0 0 12px rgba(255,102,0,.55);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  transition:color .18s ease,text-shadow .18s ease,transform .18s ease
}
.sr-next{
  font-family:var(--fd);font-size:.62rem;font-weight:700;letter-spacing:.08em;
  color:var(--dim);flex-shrink:0;white-space:nowrap
}
#g-sort-rule.flip{border-color:var(--orange);box-shadow:0 0 22px rgba(255,102,0,.30)}
#g-sort-rule.flip .sr-txt{color:#fff;text-shadow:0 0 18px var(--orange);transform:scale(1.04)}
#g-sort-rule.warn .sr-next{color:var(--gold)}

/* ── RESULTS SCREEN ── */
#results-screen .res-card{max-width:560px;width:100%;text-align:center}
.res-emoji{font-size:4.5rem;display:block;margin-bottom:18px}
.res-gname{font-family:var(--fd);font-size:.6rem;color:var(--dim);letter-spacing:.15em;text-transform:uppercase;margin-bottom:12px}
.res-bonus{display:inline-block;font-family:var(--fd);font-size:.62rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:5px 14px;border-radius:20px;margin:0 0 14px;border:1px solid var(--border);background:var(--s2);color:var(--dim)}
.res-bonus.res-bonus-stable{border-color:#00ff66;color:#00ff66;background:rgba(0,255,102,.08);box-shadow:0 0 10px rgba(0,255,102,.35)}
.res-bonus.res-bonus-overclocked{border-color:#ffcc00;color:#ffcc00;background:rgba(255,204,0,.08);box-shadow:0 0 10px rgba(255,204,0,.35)}
.res-bonus.res-bonus-meltdown{border-color:#ff0033;color:#ff0033;background:rgba(255,0,51,.08);box-shadow:0 0 10px rgba(255,0,51,.35);animation:pulse 1.5s infinite}
.res-pts{font-family:var(--fd);font-size:4.5rem;font-weight:900;line-height:1;background:linear-gradient(135deg,var(--gold),var(--orange));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:10px}
.res-pts-lbl{color:var(--dim);font-weight:600;font-size:.8rem;margin-bottom:24px}
.res-bd{background:var(--s2);border-radius:10px;padding:14px;margin-bottom:20px;text-align:left}
.res-row{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border);font-size:.85rem}
.res-row:last-child{border:none}
.res-row .rv{color:var(--cyan);font-weight:700}
.res-btns{display:flex;flex-wrap:wrap;gap:10px;justify-content:center}

/* Toast */
#toast{position:fixed;bottom:-60px;left:50%;transform:translateX(-50%);background:rgba(4,4,14,.9);border:1px solid var(--cyan);padding:12px 24px;border-radius:30px;font-family:var(--fd);font-size:.75rem;color:var(--cyan);z-index:9999;transition:bottom .3s ease, border-color .3s ease, color .3s ease, box-shadow .3s ease;box-shadow:0 10px 30px rgba(0,245,255,.2)}
#toast.show{bottom:30px}
/* A toast is a message, never a control, so it never takes a click. At the
   bottom of a desktop screen it lands squarely on #photo-bar (z 9999 over
   120): for the 3.6 seconds the "📸 PHOTO MODE" toast was up, RESUME,
   CAPTURE, HIDE HUD and RESET VIEW were all dead to the mouse. The phone
   layout below moves toasts to the top for the same reason; this covers every
   size at once. */
#toast{pointer-events:none}
/* Tier-tinted variants used for SYSTEM STABILITY change messages */
#toast.toast-stable{border-color:#00ff66;color:#00ff66;box-shadow:0 10px 30px rgba(0,255,102,.25)}
#toast.toast-overclocked{border-color:#ffcc00;color:#ffcc00;box-shadow:0 10px 30px rgba(255,204,0,.25)}
#toast.toast-meltdown{border-color:#ff0033;color:#ff0033;box-shadow:0 10px 30px rgba(255,0,51,.25)}

/* Countdown Overlay */
#cd-ov{position:fixed;inset:0;background:rgba(2,2,8,.85);backdrop-filter:blur(10px);z-index:999;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .3s ease}
#cd-ov.show{opacity:1;pointer-events:all}
#cd-num{font-family:var(--fd);font-size:7rem;font-weight:900;color:#fff}
.cd-pop{animation:cdPop .6s ease forwards}
@keyframes cdPop{0%{transform:scale(.4);opacity:0}20%{opacity:1}100%{transform:scale(1.4);opacity:0}}
/* ══════════════════════════════════════════════════════════════
   💬 FEEDBACK TERMINAL
   z-index sits above .screen (10) and #cd-ov (999) but below
   #toast (9999), so confirmation toasts stay readable.
   ══════════════════════════════════════════════════════════════ */
/* ── HUB BAR: ONE ROW ──
   The controls used to drop onto a second row the moment the bar ran out of
   width — Exit landed underneath the others and the bar stood two rows tall for
   the sake of one button. Two things keep it to a single row.

   First, room: tighter padding on the bar, its buttons and the stat pills gives
   back ~90px, which already covers the ~26px the signed-in layout was short by.

   Second, an order of sacrifice. Save Account only appears for local and guest
   sessions, and with six buttons up there the bar can still come up ~120px
   short. Wrapping is a flex container's first answer to that, but it is the
   wrong one here: it costs a whole row to save a few pixels. nowrap makes the
   overflow get absorbed by shrinking instead, and the shrink factors below
   decide what gives — the username first (it is the only part whose width is
   unbounded by its own label, and it stays legible as an ellipsis), the
   wordmark only once the username has hit its floor, and the controls never.
   (Save Account and Feedback have since moved behind the ⚙️ Settings gear, so
   the bar is two buttons lighter; the order of sacrifice still holds.) */
.hub-bar{padding-left:16px;padding-right:16px}
.hub-right{flex-wrap:nowrap;justify-content:flex-end;gap:8px;min-width:0}
.hub-right>*{flex:0 0 auto}
.hub-right .btn{padding-left:10px;padding-right:10px}
.hub-pts,.hub-credits{padding-left:10px;padding-right:10px}
.hub-logo-sm{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* shrink:100 — drained almost to the floor before the wordmark gives anything. */
.hub-uname{flex:0 100 auto;min-width:44px;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* The row cannot hold this many controls however hard it shrinks, and nowrap
   pushes the surplus out through the border instead — leftwards, because the
   row is justified to the end, so the overflow lands ON the wordmark rather
   than off the edge where it would at least be visible as a mistake.

   This used to be gated at max-width:900px. That was the wrong axis: the bar is
   capped at 1060px, so how much room the controls have barely changes above
   that — a guest session (which adds the GUEST tag and the long SAVE PROGRESS
   ONLINE button) overflowed by ~110px at 1440px just as it did at 1024px, and
   the media query never fired because the VIEWPORT was wide. The number of
   controls is what varies, not the width available to them.

   Unconditional wrapping fixes every width at once, and costs nothing at the
   sizes that already fit: flex-wrap only ever engages when the content
   genuinely does not fit, which is exactly the condition we want it on. */
.hub-right{flex-wrap:wrap;row-gap:8px}
.btn-feedback{color:var(--lime);border-color:rgba(57,255,20,.35)}
.btn-feedback:hover{background:rgba(57,255,20,.14);border-color:var(--lime);box-shadow:0 0 18px rgba(57,255,20,.22)}

/* ── 🔊 AUDIO TOGGLE ──
   One button, three states: effects+music → effects only → silent. The icon
   carries the state, the colour makes "you have muted something" readable at a
   glance without reading the glyph. Square-ish so it sits next to ⛶ evenly. */
.btn-sound{padding:7px 12px;color:var(--cyan);border-color:rgba(0,245,255,.35);font-size:.85rem;line-height:1}
.btn-sound:hover{background:rgba(0,245,255,.14);border-color:var(--cyan);box-shadow:0 0 18px rgba(0,245,255,.22)}
.btn-sound.snd-partial{color:var(--gold);border-color:rgba(255,215,0,.35)}
.btn-sound.snd-partial:hover{background:rgba(255,215,0,.14);border-color:var(--gold);box-shadow:0 0 18px rgba(255,215,0,.22)}
.btn-sound.snd-off{color:var(--dim);border-color:var(--border);opacity:.72}
.btn-sound.snd-off:hover{background:rgba(255,255,255,.1);border-color:var(--border2);box-shadow:none}

.fb-overlay{position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(2,2,10,.82);backdrop-filter:blur(7px);opacity:0;pointer-events:none;transition:opacity .22s ease}
/* 📴 The offline banner is position:fixed at bottom:0 with z-index 9000 — above
   every overlay — so a full-height modal ran its last ~60px underneath it. That
   is invisible on the overlays whose bottom row is just text, and not invisible
   at all on the briefing, whose bottom row is CLOSE and PLAY IT. The overlay
   gives the bar its own room instead of the modal having to know about it. */
html.is-offline .fb-overlay{padding-bottom:calc(20px + 3.4rem + env(safe-area-inset-bottom, 0px))}
.fb-overlay.show{opacity:1;pointer-events:auto}

.fb-modal{position:relative;width:100%;max-width:470px;max-height:90vh;overflow-y:auto;padding:28px 26px 26px;border:1px solid rgba(57,255,20,.4);border-radius:var(--rl);background:linear-gradient(160deg,rgba(11,14,30,.97),rgba(4,4,14,.99));box-shadow:0 0 70px rgba(57,255,20,.13),inset 0 0 0 1px rgba(255,255,255,.03);transform:translateY(18px) scale(.97);transition:transform .26s cubic-bezier(.2,.9,.3,1.25)}
.fb-overlay.show .fb-modal{transform:none}
/* Neon scan-line across the top edge */
.fb-modal::before{content:'';position:absolute;top:0;left:12%;right:12%;height:1px;background:linear-gradient(90deg,transparent,var(--lime),transparent);box-shadow:0 0 12px var(--lime)}

/* z-index is load-bearing. The ✕ comes BEFORE .fb-head in every modal, and the
   title is a full-width block under it. In the 3D interface the title carries a
   filter, which makes it a stacking context painted in DOM order with the
   positioned ✕ — later, so on top, and hit-testing on top: the ✕ could not be
   clicked in any modal until it was lifted explicitly. */
.fb-close{position:absolute;z-index:2;top:14px;right:14px;width:30px;height:30px;display:flex;align-items:center;justify-content:center;background:var(--s2);border:1px solid var(--border);border-radius:8px;color:var(--dim);font-size:.8rem;cursor:pointer;transition:all .2s}
.fb-close:hover{color:var(--pink);border-color:var(--pink);background:rgba(255,0,144,.1)}

.fb-head{text-align:center;margin-bottom:28px}
.fb-title{font-family:var(--fd);font-size:.95rem;font-weight:900;letter-spacing:.1em;background:linear-gradient(90deg,var(--lime),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.fb-sub{font-family:var(--fd);font-size:.5rem;letter-spacing:.26em;text-transform:uppercase;color:var(--dimmer);margin-top:13px}

.fb-opt{font-size:.85em;color:var(--dimmer);letter-spacing:.08em;text-transform:none}
.fb-select{appearance:none;cursor:pointer;background-image:linear-gradient(45deg,transparent 50%,var(--dim) 50%),linear-gradient(135deg,var(--dim) 50%,transparent 50%);background-position:calc(100% - 18px) 50%,calc(100% - 13px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:40px}
/* The one part of a <select> the page cannot paint is the open list: that is
   an OS window, and its frame, its scrollbar and its highlight bar are drawn
   from the colour scheme, not from CSS. Left at the default light, a
   dark-styled select dropped a white slab with a white scrollbar over the
   hub. The rows themselves need an OPAQUE colour for the same reason: .input
   paints var(--s2), rgba(255,255,255,.08), which Chrome cannot composite into
   a popup, so it fell back to white and left every white-texted label
   invisible. Keyed off the element, not one class, so the next select in the
   game is born themed. */
select{color-scheme:dark}
select option{background:#0b0e1e;color:var(--text)}
.fb-textarea{min-height:110px;resize:vertical;font-size:.92rem;line-height:1.5;font-family:var(--fb)}
.fb-textarea::placeholder{color:var(--dimmer);font-family:var(--fd);font-size:.62rem;letter-spacing:.06em;line-height:1.9}
.fb-count{font-family:var(--fd);font-size:.5rem;letter-spacing:.14em;color:var(--dimmer);text-align:right;margin-top:6px}
.fb-count.warn{color:var(--gold)}

/* ── Core node rating ── */
.fb-rating{display:flex;gap:10px}
.fb-node{flex:1;padding:11px 0;background:var(--s2);border:1px solid var(--border);border-radius:8px;color:var(--dimmer);font-size:1.05rem;line-height:1;cursor:pointer;transition:all .18s ease}
.fb-node:hover{border-color:rgba(57,255,20,.5);transform:translateY(-2px)}
.fb-node.lit{background:rgba(57,255,20,.12);border-color:var(--lime);color:var(--lime);text-shadow:0 0 10px var(--lime);box-shadow:0 0 16px rgba(57,255,20,.2)}
.fb-node:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
.fb-rating-lbl{font-family:var(--fd);font-size:.52rem;letter-spacing:.16em;text-transform:uppercase;color:var(--dimmer);margin-top:9px;min-height:12px}
.fb-rating-lbl.set{color:var(--lime)}

.fb-err{min-height:20px;font-size:.8rem;font-weight:600;color:var(--pink);text-align:center;margin-bottom:10px}

/* ── Success / terminal readout ── */
.fb-success{display:none;text-align:center;padding:14px 0 6px}
.fb-success.show{display:block}
.fb-success-icon{font-size:3rem;margin-bottom:18px;animation:fbPulse 1.4s ease-in-out infinite}
@keyframes fbPulse{0%,100%{transform:scale(1);opacity:.85}50%{transform:scale(1.12);opacity:1}}
.fb-term{background:rgba(0,0,0,.5);border:1px solid rgba(57,255,20,.28);border-radius:10px;padding:16px;text-align:left;font-family:var(--fd);font-size:.6rem;line-height:2;letter-spacing:.06em;color:var(--lime);min-height:104px;text-shadow:0 0 8px rgba(57,255,20,.5)}
.fb-term .ok{color:#fff;text-shadow:0 0 10px var(--lime)}
.fb-term .caret{animation:fbBlink 1s step-end infinite}
@keyframes fbBlink{50%{opacity:0}}

@media (max-width:520px){
  .fb-modal{padding:24px 18px}
  .fb-rating{gap:6px}
  .fb-node{font-size:.9rem;padding:9px 0}
}

/* ══════════════════════════════════════════════════════════════
   💾 SAVE ACCOUNT — guest → permanent upgrade
   Reuses the .fb-overlay/.fb-modal shell; .up-tint recolours it cyan.
   ══════════════════════════════════════════════════════════════ */
.btn-save-acct{background:rgba(57,255,20,.1);color:var(--lime);border:1px solid rgba(57,255,20,.5);animation:saveGlow 2.4s ease-in-out infinite}
.btn-save-acct:hover{background:rgba(57,255,20,.2);border-color:var(--lime);animation:none;box-shadow:0 0 24px rgba(57,255,20,.35)}
@keyframes saveGlow{0%,100%{box-shadow:0 0 0 rgba(57,255,20,0)}50%{box-shadow:0 0 18px rgba(57,255,20,.3)}}

.up-tint .fb-modal{border-color:rgba(0,245,255,.42);box-shadow:0 0 70px rgba(0,245,255,.14),inset 0 0 0 1px rgba(255,255,255,.03)}
.up-tint .fb-modal::before{background:linear-gradient(90deg,transparent,var(--cyan),transparent);box-shadow:0 0 12px var(--cyan)}
.up-tint .fb-title{background:linear-gradient(90deg,var(--cyan),var(--purple));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.up-tint .fb-term{border-color:rgba(0,245,255,.28);color:var(--cyan);text-shadow:0 0 8px rgba(0,245,255,.5)}
.up-tint .fb-term .ok{color:#fff;text-shadow:0 0 10px var(--cyan)}

.up-carry{background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.28);border-radius:10px;padding:12px 14px;margin-bottom:18px;text-align:center;font-family:var(--fd);font-size:.56rem;letter-spacing:.1em;color:var(--cyan);line-height:2}
.up-carry b{color:#fff;font-weight:700}
.up-hint{font-size:.68rem;color:var(--dimmer);margin-top:6px;letter-spacing:.02em}

/* ══════════════════════════════════════════════════════════════
   📱 MOBILE / TOUCH
   ══════════════════════════════════════════════════════════════ */

/* Double-tap-to-zoom is what made every button press zoom the page.
   touch-action:manipulation removes the double-tap gesture (and its
   ~300ms click delay) while LEAVING pinch-to-zoom intact, so the page
   stays accessible to anyone who needs to magnify it. */
button, .btn, .auth-tab, .market-tab, .diff-btn, .game-card, .shop-card,
.memory-card, .hack-node, .fb-node, .fb-close, select, #click-btn, #math-submit {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* Stop long-press selecting label text or raising the iOS callout menu
   mid-game. Inputs and the feedback textarea stay selectable. */
button, .btn, .auth-tab, .market-tab, .diff-btn, .game-card, .shop-card,
.memory-card, .hack-node, .path-node, .fb-node, .ctrl-btn, .hub-bar, .g-hdr, .market-bar {
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}
/* Deliberately NOT in the touch-action:manipulation list above: Overclock Path
   is drag-driven, and .path-grid claims touch-action:none so a finger tracing a
   route draws on the board rather than scrolling the page out from under it.
   The tap-highlight still has to go, or every node flashes grey as the drag
   crosses it. */
.path-node { -webkit-tap-highlight-color: transparent; }

/* Play surfaces: swallow every browser gesture so a drag or hold never
   scrolls, zooms, or pulls-to-refresh the page out from under the game. */
#arcade-canvas, .ctrl-btn, #click-btn, .reaction-box { touch-action: none; }
body { overscroll-behavior: none; }

/* A canvas game is driven entirely by finger-on-canvas — no page scrolling to
   preserve — so lock the screen down and let fitCanvas() size the board. */
#game-screen.canvas-game { overflow: hidden; }
#g-canvas-holder { max-width: 100%; }

/* iOS zooms the viewport when focusing any field under 16px. Everything
   the player can type into is pinned to 16px so that never fires. */
.input, .fb-textarea, .fb-select, #math-answer, input, textarea, select {
  font-size: 16px;
}
.fb-textarea { line-height: 1.5; }

/* Real viewport height on mobile — 100% ignores the collapsing browser
   chrome, leaving the hub cut off behind the address bar. */
@supports (height: 100dvh) {
  html, body { height: 100dvh; }
}

/* Keep the UI clear of notches and the home indicator. */
.hub-bar, .market-bar, .g-hdr {
  padding-left: max(20px, env(safe-area-inset-left));
  padding-right: max(20px, env(safe-area-inset-right));
}
.screen { padding-bottom: max(20px, env(safe-area-inset-bottom)); }

/* A finger has no hover, but tapping still latches :hover on most mobile
   browsers — cards stayed lifted and glowing long after you'd moved on. Drop
   the hover treatment entirely there and give taps a real press state. */
@media (hover: none) {
  .game-card:hover, .shop-card:hover, .btn-primary:hover, .diff-btn:hover,
  .fb-node:hover, .btn-guest:hover:not(:disabled), .btn-save-acct:hover {
    transform: none;
    box-shadow: none;
  }
  .game-card[data-c]:hover { border-color: var(--border); }
  .game-card:active { transform: scale(.97); border-color: var(--border2); }
  .shop-card:active, .btn:active { transform: scale(.97); }
  /* .lit is applied by the game itself, so it must survive the press state. */
  .hack-node:not(.lit):active { border-color: var(--nc); transform: scale(.96); }
  /* Same reasoning for the circuit board: .on and .dead are game state, and a
     press must not repaint a node the route is already running through. */
  .path-node:not(.on):not(.dead):active { border-color: var(--pc, var(--purple)); transform: scale(.96); }
  .btn-primary { box-shadow: 0 0 20px rgba(0,245,255,.25); }
}

/* Finger-sized targets on touch devices (44px is the accessibility floor). */
@media (max-width: 520px) {
  /* Seven cards can't share one phone-width row — a 22% basis breaks the
     deck into a 4 + 3 grid instead of squeezing every label to nothing. */
  #bb-deck { gap: 6px; }
  .bb-btn { padding: 7px 3px; gap: 2px; flex-basis: 22%; }
  .bb-ico { font-size: 1.15rem; }
  .bb-name { font-size: .5rem; letter-spacing: .06em; }
  .bb-cost { font-size: .48rem; }
  .bb-btn[data-cd]::before { font-size: 1rem; }
}

@media (pointer: coarse) {
  .btn-sm { padding: 11px 16px; min-height: 44px; }
  .btn { min-height: 44px; }
  .ctrl-btn { min-height: 68px; font-size: 1.05rem; }
  .auth-tab, .market-tab, .diff-btn { min-height: 44px; }
  .fb-node { padding: 15px 0; }
  .fb-close { width: 38px; height: 38px; }

  /* Tetris runs four buttons (◀ ROTATE DROP ▶) on a ~340px row — the 3-button
     sizing overflows there, so the 4-up layout gets its own scale. */
  #arcade-controls.four-up { gap: 7px; }
  #arcade-controls.four-up .ctrl-btn { font-size: .7rem; padding: 12px 2px; min-width: 0; }
  #arcade-controls.four-up .ctrl-btn.glyph { font-size: 1.15rem; }

  /* Games driven by finger-on-canvas keep one wide button instead of a pad. */
  #arcade-controls.solo-action .ctrl-btn { font-size: .82rem; }
}

/* ── Narrow screens ── */
@media (max-width: 700px) {
  /* The hub bar carries 5 buttons plus 3 stats — let it stack instead of
     overflowing off-screen. */
  .hub-bar, .market-bar {
    flex-direction: column; align-items: stretch; gap: 10px; padding: 12px 14px;
  }
  .hub-right { justify-content: center; gap: 8px; }
  .hub-logo-sm, .market-title { text-align: center; }
  /* Stacked, the leaderboard's label lands right under the last game card —
     widen the gap so the two section headings stay clearly separated. */
  .hub-body { grid-template-columns: 1fr; gap: 30px; }

  /* One full-bleed card per row meant scrolling past eleven screen-tall tiles
     to reach the last game. Two compact tiles per row makes the roster
     scannable; the blurb is clamped so every card is the same height. */
  .games-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .game-card { padding: 15px 10px; }
  .gc-icon { font-size: 1.9rem; margin-bottom: 8px; }
  .gc-name { font-size: .52rem; margin-bottom: 8px; }
  .gc-desc {
    font-size: .68rem; line-height: 1.35; margin-bottom: 11px;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .gc-pts { font-size: .44rem; padding: 2px 8px; }
  .lb-panel { max-height: 320px; }

  /* Same three-row header, just tighter — the phone needs the height. */
  .g-hdr { column-gap: 12px; row-gap: 8px; }
  .g-title { font-size: .8rem; }
  .g-controls { font-size: .52rem; line-height: 1.6; }
  /* Tighter than the desktop pill: the reserved slots are kept at full width
     here (a five-digit Cyber Arena score has to sit still on a phone too), so
     the room comes out of the padding instead. */
  .stat-pill { padding: 5px 8px; font-size: .55rem; }
  /* The pace ghost is an extra pill that shows up only once the mission has a
     personal best to race, and on a phone it is the one that used to push Audio
     and Fullscreen clean off the right edge: .g-stats is a flex line inside a
     grid track, and a grid item will not shrink below its content unless it is
     told to. Letting the row wrap fixed the overflow and created a worse
     problem — the three buttons wrapped INDIVIDUALLY, one per line, and a
     375×812 phone carrying a pace ghost got a four-row, 168px header with
     Fullscreen sitting alone on the last row. A fifth of the screen, spent on
     chrome, before the board got a pixel.
     Two things fix it and both are above: the delta owns a fixed slot so the
     pill stops resizing at all, and the buttons are ONE item so a wrap can
     only ever break between the pills and the cluster. Two rows, worst case.
     Nothing has to be re-measured for it — the board sizes itself from the
     header's real bottom edge, so a second line costs the play field exactly
     its own height and no more. */
  .g-hdr, .g-hdr > * { min-width: 0; }
  /* A phone cannot fit Quit, the readouts and the cluster on one line, so the
     header stops pretending: the two groups of CONTROLS share the row, and the
     READOUTS — which nobody taps — get the line beneath. Four rows in total,
     none of them ragged, and the tallest thing on the control row is a 44px
     thumb target instead of a 23px pill. */
  .g-hdr > .g-actions { grid-column: 3; grid-row: 2; }
  .g-stats { grid-column: 1 / -1; grid-row: 3; justify-content: flex-end; }
  .g-controls { grid-row: 4; }
  .g-stats > .stat-pill, .g-stats > .ghost-pace { flex-shrink: 0; }
  /* A real thumb target on the one row the player has to hit while playing. */
  .g-actions { gap: 8px; }
  .g-actions .btn-sm { min-width: 44px; min-height: 44px; font-size: .9rem; }
  .prog-bar { margin-bottom: 10px; }
  .g-area { max-width: 100%; }
  #arcade-controls { max-width: 100%; }

  /* The 560×500 board is width-bound in portrait — it fits the width and then
     leaves a third of the screen empty below the pad. So every pixel of side
     padding comes straight off the play field and buys nothing: at 20px a side
     the board landed at 335px on a 375px phone. The header carried its own
     20px on top of that, indenting Quit and the stat pills from the board edge
     they're supposed to line up with; both shrink to the notch inset here. */
  #game-screen {
    padding-top: 10px;
    padding-left: max(6px, env(safe-area-inset-left));
    padding-right: max(6px, env(safe-area-inset-right));
  }
  .g-hdr {
    padding-left: max(2px, env(safe-area-inset-left));
    padding-right: max(2px, env(safe-area-inset-right));
  }

  /* The NEXT box is a fixed 80px block sitting on top of a play field that is
     now only ~220–340px wide — it swallowed three of Tetris's ten columns.
     Shrink it and make it translucent so the stack underneath stays readable
     right up to the top-out. */
  #tetris-next-wrap {
    top: 5px; right: 5px; padding: 4px 5px;
    background: rgba(10,10,26,.45); border-color: rgba(255,255,255,.14);
  }
  #tetris-next-wrap .next-label { font-size: .42rem; margin-bottom: 1px; opacity: .75; }
  #nextCanvas { width: 46px; height: 46px; opacity: .78; }

  .convert-grid { grid-template-columns: 1fr; }
  .shop-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
  #click-btn { width: 150px; height: 150px; font-size: 3rem; }
}

/* Landscape phones: reclaim vertical space so the canvas still fits. */
@media (max-height: 500px) and (orientation: landscape) {
  #hub-screen, #market-screen { padding-top: 8px; }
  .hub-bar { margin-bottom: 10px; }
  .g-hdr { margin-bottom: 6px; }
  #arcade-controls { margin-top: 8px; }
  .ctrl-btn { min-height: 52px; padding: 10px; }

  /* Every header row here costs the board twice its height (it scales to fit).
     The gesture hint has already been shown as an on-board overlay, and the
     title is in the card you just tapped — drop both and give the space back. */
  #game-screen { padding-top: 6px; }
  .g-controls { display: none; }
  /* Back to a single row — height is the scarce thing here, and with the hint
     gone the title is the only flexible item. It sits in the middle track so
     it stays clear of Quit and the pills without ever overflowing the row. */
  .g-hdr { grid-template-columns: auto 1fr auto auto; column-gap: 12px; row-gap: 0; }
  .g-hdr > #btn-quit { grid-column: 1; grid-row: 1; }
  .g-title { grid-column: 2; grid-row: 1; font-size: .7rem; }
  .g-stats { grid-column: 3; grid-row: 1; }
  /* A fourth track, because the cluster is its own header item now. Landscape
     has the width for all four — it is height that is scarce here. */
  .g-hdr > .g-actions { grid-column: 4; grid-row: 1; }
  .prog-bar { margin-bottom: 6px; height: 4px; }

  /* Landscape has width to spare and no height at all, so the pad moves to the
     side of the board instead of underneath it — the board then gets the full
     column height. !important is needed because each game sets the holder's
     display inline when it mounts — which is also why this is scoped to
     .canvas-game: unscoped, it re-showed the holder (and a stale board) on
     top of Memory Match, Math Blitz and Reaction Time, which hide it. */
  #game-screen.canvas-game #g-canvas-holder { display: flex !important; align-items: center; gap: 12px; }
  #arcade-controls {
    flex-direction: column; width: auto; min-width: 96px; max-width: 130px;
    margin-top: 0; gap: 8px;
  }
  #arcade-controls .ctrl-btn { min-height: 46px; font-size: .68rem; padding: 8px 6px; }
  #arcade-controls .ctrl-btn.glyph { font-size: 1.05rem; }
  #arcade-controls.four-up .ctrl-btn { min-height: 38px; }

  /* The deck stays a full-width row under the board (it's a sibling of the
     holder, not a child), but flattens to one line so it costs the board as
     little height as possible. */
  #bb-deck { margin-top: 6px; gap: 6px; }
  .bb-btn { flex-direction: row; justify-content: center; align-items: baseline; gap: 4px; padding: 6px 3px; }
  .bb-ico { font-size: 1rem; }
  /* Seven cards across the one row — shave the labels so the widest names
     (FIREWALL, RAM SPEED) still fit a seventh of the board. */
  .bb-name { font-size: .48rem; letter-spacing: .04em; }
  .bb-cost { font-size: .44rem; }
  .bb-btn[data-cd]::before { font-size: .9rem; }

  /* The modulator console is a sibling of the holder too, so it also stays a
     full-width row under the scope — just a flatter one, since height is the
     scarce thing in landscape. */
  #g-freq-ctl { margin-top: 6px; gap: 8px; }
  .freq-slider { padding: 5px 10px 7px; }
  #g-sort-rule { margin-top: 6px; gap: 8px; padding: 6px 10px; }
  .sr-txt { font-size: .72rem; }
  .sr-next { font-size: .55rem; }
  .freq-lbl { margin-bottom: 5px; font-size: .5rem; }

  /* Overclock Path is not a canvas game, so it is laid out by .path-board's own
     clamp — which is capped against the viewport HEIGHT here for the same
     reason the board is: in landscape there is width to spare and none of it. */
  .path-wrap { gap: 8px; max-width: 340px; }
  .path-readout { padding: 7px 10px; }
  .path-board { width: min(100%, 340px, 58vh); }
  .path-grid { gap: 6px; }
}

/* Toasts sit at the bottom on desktop, which on a phone lands them right
   on top of the game control buttons. Flip them to the top on narrow
   screens so a confirmation can't swallow a tap mid-game. */
@media (max-width: 700px) {
  #toast {
    bottom: auto; top: -100px;
    max-width: calc(100vw - 32px);
    font-size: .62rem; padding: 10px 16px; text-align: center; line-height: 1.5;
    transition: top .3s ease, border-color .3s ease, color .3s ease, box-shadow .3s ease;
  }
  #toast.show { top: max(12px, env(safe-area-inset-top)); bottom: auto; }
}

/* ══════════════════════════════════════════════════════════════════════
   🌐 NETWORK ARENA — multiplayer banner, lobby, room and in-round HUD
   ══════════════════════════════════════════════════════════════════════ */

/* ── HUB BANNER ── */
.mp-banner{
  position:relative;overflow:hidden;display:flex;align-items:center;gap:16px;
  background:linear-gradient(120deg,rgba(0,245,255,.09),rgba(168,85,247,.09));
  border:1px solid rgba(0,245,255,.35);border-radius:var(--rl);
  padding:16px 20px;margin-bottom:20px;
  box-shadow:0 0 26px rgba(0,245,255,.1)
}
/* A slow light sweep marks it as the live thing on the page without animating
   anything the eye has to track while reading the mission grid. */
.mp-banner::after{
  content:'';position:absolute;top:0;left:-150%;width:55%;height:100%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.10),transparent);
  animation:mp-sweep 5.5s ease-in-out infinite;pointer-events:none
}
@keyframes mp-sweep{0%{left:-150%}55%,100%{left:160%}}
.mp-banner-icon{font-size:2.3rem;flex-shrink:0;animation:float 3.4s ease-in-out infinite}
.mp-banner-txt{flex:1;min-width:0}
.mp-banner-title{
  font-family:var(--fd);font-size:.72rem;font-weight:900;letter-spacing:.14em;
  background:linear-gradient(90deg,var(--cyan),var(--purple));
  -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
  margin-bottom:5px
}
.mp-banner-sub{font-size:.75rem;color:var(--dim);line-height:1.4}
.mp-banner .btn{flex-shrink:0;position:relative;z-index:1}

/* ── LOBBY SHELL ── */
#mp-screen{justify-content:flex-start;padding-top:20px}
.mp-bar{
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  width:100%;max-width:1060px;background:var(--s1);border:1px solid var(--border);
  border-radius:var(--r);padding:12px 20px;margin-bottom:18px;flex-shrink:0
}
.mp-title{
  font-family:var(--fd);font-size:.9rem;font-weight:900;letter-spacing:.08em;
  background:linear-gradient(90deg,var(--cyan),var(--purple));
  -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text
}
.mp-link{
  font-family:var(--fd);font-size:.55rem;font-weight:700;letter-spacing:.14em;
  color:var(--dimmer);background:var(--s2);border:1px solid var(--border);
  border-radius:20px;padding:5px 14px;white-space:nowrap
}
.mp-link.live{color:var(--lime);border-color:rgba(57,255,20,.45);background:rgba(57,255,20,.08)}
.mp-link.busy{color:var(--gold);border-color:rgba(255,215,0,.45);background:rgba(255,215,0,.08)}
.mp-link.down{color:var(--red);border-color:rgba(255,36,66,.45);background:rgba(255,36,66,.08)}

.mp-body{width:100%;max-width:1060px;flex:1;overflow-y:auto;padding-bottom:24px}
.mp-stage{display:none}
.mp-stage.active{display:block}
.mp-sec-2{margin-top:26px}

/* ── MODE PICKER ── */
/* Sixteen duels is more than fits on one screen, so the kinds are filterable.
   The chips read as a segmented control rather than as buttons — picking a
   category is a view change, not an action. */
.mp-filters{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
.mp-filter{
  font-family:var(--fd);font-size:.48rem;font-weight:700;letter-spacing:.12em;
  color:var(--dim);background:var(--s2);border:1px solid var(--border);
  border-radius:20px;padding:7px 15px;cursor:pointer;
  transition:color .2s ease,border-color .2s ease,background .2s ease
}
.mp-filter:hover{color:var(--txt);border-color:var(--border2)}
.mp-filter.on{
  color:var(--cyan);border-color:var(--cyan);background:rgba(0,245,255,.09);
  box-shadow:0 0 16px rgba(0,245,255,.18)
}
.mp-filter em{font-style:normal;opacity:.55;margin-left:6px}

.mp-modes{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
/* The duel's KIND, top-left, opposite the SELECTED tick. Live rounds share one
   simulation over the wire; races run two independent boards and compare. */
.mp-mode-kind{
  position:absolute;top:10px;left:12px;
  font-family:var(--fd);font-size:.42rem;font-weight:700;letter-spacing:.12em
}
.mp-mode-kind.k-live{color:var(--pink)}
.mp-mode-kind.k-race{color:var(--lime)}
.mp-modes .mp-mode{padding-top:30px}
.mp-mode{
  position:relative;background:var(--s1);border:1px solid var(--border);
  border-radius:var(--rl);padding:20px 16px;text-align:center;cursor:pointer;
  transition:transform .22s ease,border-color .22s ease,box-shadow .22s ease
}
.mp-mode:hover{transform:translateY(-4px);border-color:var(--border2)}
.mp-mode.sel{border-color:var(--cyan);background:rgba(0,245,255,.07);box-shadow:0 8px 30px rgba(0,245,255,.18)}
.mp-mode.sel::before{
  content:'\2714 SELECTED';position:absolute;top:10px;right:10px;
  font-family:var(--fd);font-size:.44rem;font-weight:700;letter-spacing:.12em;
  color:var(--cyan)
}
.mp-mode-icon{font-size:2.5rem;display:block;margin-bottom:10px}
.mp-mode-name{font-family:var(--fd);font-size:.6rem;font-weight:700;letter-spacing:.08em;color:var(--cyan);margin-bottom:9px}
.mp-mode-desc{font-size:.74rem;color:var(--dim);line-height:1.45;margin-bottom:12px}
.mp-mode-meta{
  display:inline-block;font-family:var(--fd);font-size:.46rem;font-weight:700;
  letter-spacing:.1em;padding:3px 11px;border-radius:20px;
  color:var(--gold);border:1px solid var(--gold);background:rgba(255,215,0,.06)
}

/* ── CONNECT CARDS ── */
.mp-connect{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px}
.mp-conn-card{display:flex;flex-direction:column;text-align:center;padding:22px 18px}
.mp-conn-icon{font-size:2rem;margin-bottom:10px}
.mp-conn-name{font-family:var(--fd);font-size:.62rem;font-weight:700;letter-spacing:.1em;margin-bottom:10px}
.mp-conn-desc{font-size:.74rem;color:var(--dim);line-height:1.45;margin-bottom:16px;flex:1}
/* The tracking pads the right edge only, so the text-indent pulls the glyphs
   back level and the four characters sit centred in the field. */
.mp-code-input{
  text-align:center;font-family:var(--fd);font-weight:900;
  letter-spacing:.42em;text-indent:.42em;
  text-transform:uppercase;margin-bottom:10px
}
.mp-err{min-height:22px;margin-top:16px;text-align:center;font-size:.82rem;font-weight:600;color:var(--pink)}

/* ── 🤖 DROID OPPONENT ── */
.mp-bot-card{border-color:rgba(168,85,247,.4);background:linear-gradient(160deg,rgba(168,85,247,.09),transparent 70%)}
.mp-bot-card .mp-conn-name{color:var(--purple)}
.mp-bot-lvls{display:flex;gap:6px;margin-bottom:14px}
.mp-bot-lvl{
  flex:1;min-width:0;font-family:var(--fd);font-size:.44rem;font-weight:700;
  letter-spacing:.08em;color:var(--dim);background:var(--s2);
  border:1px solid var(--border);border-radius:var(--r);padding:9px 4px;
  cursor:pointer;transition:all .2s ease;white-space:nowrap;overflow:hidden;text-overflow:ellipsis
}
.mp-bot-lvl:hover{color:var(--txt);border-color:var(--border2)}
.mp-bot-lvl.on{color:#fff;border-color:var(--purple);background:rgba(168,85,247,.18)}
/* Each rung carries its own warning colour when chosen, so the risk is legible
   before the round rather than after it. */
.mp-bot-lvl[data-lvl="rookie"].on{border-color:var(--lime);background:rgba(57,255,20,.16)}
.mp-bot-lvl[data-lvl="elite"].on{border-color:var(--red);background:rgba(255,36,66,.18)}

/* The droid's seat in the room, and its tag in the versus HUD. */
.mp-seat.bot{border-color:rgba(168,85,247,.45);background:rgba(168,85,247,.07)}
.mp-seat.bot .mp-seat-tag{color:var(--purple)}
.mp-seat-bot-chip{
  display:inline-block;margin-top:7px;font-family:var(--fd);font-size:.4rem;
  font-weight:700;letter-spacing:.14em;color:var(--purple);
  border:1px solid rgba(168,85,247,.5);border-radius:20px;padding:2px 8px
}
#btn-mp-addbot{display:none}

/* Waiting on the rival's board to finish: the score race's one idle state. */
.mp-wait-score{
  display:block;margin-top:10px;font-family:var(--fd);font-size:.62rem;
  letter-spacing:.1em;color:var(--cyan)
}

/* ── ROOM ── */
.mp-room{max-width:560px;margin:0 auto}
.mp-room-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:6px}
.mp-room-lbl{font-family:var(--fd);font-size:.5rem;letter-spacing:.2em;color:var(--dim);margin-bottom:6px}
.mp-room-code{
  font-family:var(--fd);font-size:2.6rem;font-weight:900;letter-spacing:.22em;
  line-height:1;color:var(--cyan);text-shadow:0 0 24px rgba(0,245,255,.45)
}
.mp-room-game{
  font-family:var(--fd);font-size:.58rem;letter-spacing:.14em;color:var(--dim);
  padding-bottom:18px;border-bottom:1px solid var(--border);margin-bottom:18px
}
/* stretch, not center: a name that wraps to two lines would otherwise leave the
   two seats at different heights with a ragged gap between them */
.mp-seats{display:flex;align-items:stretch;gap:12px;margin-bottom:18px}
.mp-seat{
  flex:1;min-width:0;background:var(--s2);border:1px solid var(--border);
  border-radius:var(--r);padding:16px 12px;text-align:center;transition:all .25s ease
}
.mp-seat.filled{border-color:rgba(0,245,255,.4);background:rgba(0,245,255,.05)}
.mp-seat.ready{border-color:var(--lime);background:rgba(57,255,20,.07);box-shadow:0 0 18px rgba(57,255,20,.15)}
.mp-seat.empty{border-style:dashed;color:var(--dimmer)}
.mp-seat-avatar{font-size:1.9rem;display:block;margin-bottom:8px}
.mp-seat-name{
  font-family:var(--fd);font-size:.6rem;font-weight:700;margin-bottom:7px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap
}
.mp-seat-tag{font-family:var(--fd);font-size:.46rem;letter-spacing:.12em;color:var(--dim)}
.mp-seat.ready .mp-seat-tag{color:var(--lime)}
.mp-vs{
  font-family:var(--fd);font-size:.8rem;font-weight:900;color:var(--pink);
  text-shadow:0 0 14px rgba(255,0,144,.5);flex-shrink:0;align-self:center
}
.mp-room-status{
  text-align:center;font-size:.8rem;color:var(--dim);font-weight:600;
  min-height:22px;margin-bottom:18px
}
.mp-room-btns{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
.mp-room-btns .btn[disabled]{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
.btn-mp-unready{background:rgba(57,255,20,.12);color:var(--lime);border:1px solid rgba(57,255,20,.5)}

/* ── IN-ROUND VERSUS STRIP ── */
#mp-hud{
  display:flex;align-items:center;gap:12px;width:100%;
  max-width:max(var(--board-w,560px),560px);margin-bottom:12px;flex-shrink:0
}
.mp-hud-side{display:flex;align-items:baseline;gap:8px;min-width:0;flex:0 0 auto}
.mp-hud-right{justify-content:flex-end}
.mp-hud-name{
  font-family:var(--fd);font-size:.52rem;letter-spacing:.1em;color:var(--dim);
  max-width:11ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap
}
/* Same problem as the stat pills, and worse here: the bar between the two
   scores is the flexible element, so every digit either side gained pulled the
   bar — and with it the cyan/pink boundary you are reading — sideways. A fixed
   slot per score keeps the bar's ends nailed down; each score grows into its
   own room instead of into the bar's. */
.mp-hud-score{
  font-family:var(--fd);font-size:1.05rem;font-weight:900;line-height:1;color:var(--cyan);
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  display:inline-block;min-width:5ch;text-align:right
}
/* Mirrored: each score stays hard against the bar it belongs to. */
.mp-hud-right .mp-hud-score{text-align:left}
.mp-hud-right .mp-hud-score{color:var(--pink)}
.mp-hud-bar{
  flex:1;height:7px;border-radius:4px;overflow:hidden;background:rgba(255,0,144,.35)
}
/* The fill is "your share" of the bar, so the cyan/pink boundary IS the score
   difference — readable at a glance while your eyes are on the ball. */
.mp-hud-fill{
  height:100%;width:50%;border-radius:4px;
  background:linear-gradient(90deg,var(--cyan),var(--purple));
  box-shadow:0 0 12px rgba(0,245,255,.5);transition:width .28s ease-out
}

/* ── BOARD OVERLAY (waiting / dropped link / shared countdown) ── */
#mp-overlay{
  position:absolute;inset:0;display:none;align-items:center;justify-content:center;
  padding:24px;border-radius:8px;background:rgba(4,4,14,.78);backdrop-filter:blur(3px);
  z-index:5;pointer-events:none;text-align:center
}
#mp-overlay.show{display:flex}
#mp-overlay-txt{
  font-family:var(--fd);font-size:.85rem;font-weight:700;letter-spacing:.1em;
  line-height:1.7;color:var(--cyan);text-shadow:0 0 18px rgba(0,245,255,.5)
}
#mp-overlay.warn #mp-overlay-txt{color:var(--red);text-shadow:0 0 18px rgba(255,36,66,.5)}
#mp-overlay .mp-ov-sub{
  display:block;margin-top:10px;font-size:.6rem;letter-spacing:.14em;
  color:var(--dim);text-shadow:none
}

/* ── RESULT BADGES ── */
.res-bonus.res-bonus-net{border-color:var(--cyan);color:var(--cyan);background:rgba(0,245,255,.08);box-shadow:0 0 10px rgba(0,245,255,.35)}
.res-bonus.res-bonus-win{border-color:var(--lime);color:var(--lime);background:rgba(57,255,20,.08);box-shadow:0 0 10px rgba(57,255,20,.35)}
.res-bonus.res-bonus-loss{border-color:var(--red);color:var(--red);background:rgba(255,36,66,.08);box-shadow:0 0 10px rgba(255,36,66,.35)}

/* ── NARROW SCREENS ── */
@media (max-width: 700px){
  .mp-bar{flex-direction:column;align-items:stretch;gap:10px;padding:12px 14px}
  .mp-title{text-align:center}
  .mp-link{text-align:center}
  .mp-banner{flex-direction:column;text-align:center;gap:10px;padding:16px 14px}
  .mp-banner .btn{width:100%}
  .mp-modes{grid-template-columns:1fr}
  .mp-mode{padding:16px 14px}
  .mp-mode-icon{font-size:2rem}
  .mp-room-code{font-size:2rem}
  .mp-seats{gap:8px}
  .mp-seat{padding:12px 8px}
  /* The reserved slot rides the font size down with it — a five-digit score
     still has to leave the bar where it was. */
  .mp-hud-score{font-size:.9rem}
  .mp-hud-name{font-size:.46rem;max-width:8ch}
  #mp-overlay-txt{font-size:.7rem}
}

/* Landscape phones: the versus strip costs the board height it can't spare,
   so it flattens to the bar and the two scores. */
@media (max-height: 500px) and (orientation: landscape){
  #mp-hud{margin-bottom:6px;gap:8px}
  .mp-hud-name{display:none}
  .mp-hud-score{font-size:.8rem}
}

/* ── 🔊 AUDIO POPOVER ── */
#audio-pop{position:fixed;z-index:400;min-width:230px;background:rgba(8,8,24,.97);border:1px solid var(--border2);border-radius:var(--r);padding:14px;box-shadow:0 8px 40px rgba(0,0,0,.6),0 0 24px rgba(0,245,255,.08);opacity:0;pointer-events:none;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease}
#audio-pop.open{opacity:1;pointer-events:all;transform:translateY(0)}
.ap-sec{font-family:var(--fd);font-size:.55rem;font-weight:700;letter-spacing:.25em;color:var(--cyan);margin-bottom:10px}
.ap-sec + .ap-sec, .ap-vol-row + .ap-sec{margin-top:14px}
.ap-modes{display:flex;gap:6px;margin-bottom:12px}
.ap-mode{flex:1;font-family:var(--fd);font-size:.6rem;font-weight:700;letter-spacing:.05em;padding:8px 4px;border-radius:8px;border:1px solid var(--border);background:var(--s1);color:var(--dim);cursor:pointer;transition:all .15s ease}
.ap-mode:hover{background:var(--s2);color:var(--text)}
.ap-mode.active{background:rgba(0,245,255,.12);border-color:var(--cyan);color:var(--cyan);box-shadow:0 0 12px rgba(0,245,255,.2)}
.ap-vol-row{display:flex;align-items:center;gap:10px}
.ap-vol-num{font-family:var(--fd);font-size:.65rem;font-weight:700;color:var(--text);min-width:3ch;text-align:right}
#ap-vol{flex:1;appearance:none;-webkit-appearance:none;height:4px;border-radius:2px;background:linear-gradient(90deg,var(--cyan),var(--purple));outline:none;cursor:pointer}
#ap-vol::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--cyan);border:2px solid #04040e;box-shadow:0 0 10px rgba(0,245,255,.6);cursor:pointer}
#ap-vol::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--cyan);border:2px solid #04040e;box-shadow:0 0 10px rgba(0,245,255,.6);cursor:pointer}

/* ── 👁️ REDUCED MOTION ──
   Transitions that only decorate are cut to near-zero; the screen system keeps
   a short fade so navigation doesn't jump-cut. Looping/attention-grabbing
   animations stop entirely — those are the ones that cause trouble. */
html.reduced-motion *,
html.reduced-motion *::before,
html.reduced-motion *::after{
  animation-duration:.001ms !important;
  animation-iteration-count:1 !important;
  transition-duration:.08s !important;
  scroll-behavior:auto !important;
}
html.reduced-motion .screen{transition:opacity .12s ease !important;transform:none !important}
html.reduced-motion .screen.active{transform:none !important}
/* Hover lifts and pulses read as motion on a big card grid — flatten them. */
html.reduced-motion .btn-primary:hover,
html.reduced-motion .btn-secondary:hover,
html.reduced-motion .game-card:hover,
html.reduced-motion .shop-card:hover{transform:none !important}

/* ── 👁️ VISUAL COMFORT POPOVER (shares #audio-pop shell) ── */
.ap-toggle{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 0;cursor:pointer;user-select:none}
.ap-toggle-lbl{font-family:var(--fb);font-size:.82rem;font-weight:600;color:var(--text)}
.ap-switch{position:relative;flex:0 0 38px;height:20px;border-radius:10px;background:var(--s2);border:1px solid var(--border);transition:background .18s ease,border-color .18s ease}
.ap-switch::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--dim);transition:transform .18s ease,background .18s ease}
.ap-toggle.on .ap-switch{background:rgba(0,245,255,.18);border-color:var(--cyan)}
.ap-toggle.on .ap-switch::after{transform:translateX(18px);background:var(--cyan)}
.ap-divider{height:1px;background:var(--border);margin:12px 0 0}

/* ── 🔥 STREAK CHIP ── */
.hub-streak{font-family:var(--fd);font-size:.62rem;font-weight:700;letter-spacing:.06em;padding:5px 10px;border-radius:20px;background:rgba(255,102,0,.14);border:1px solid rgba(255,102,0,.5);color:var(--orange);white-space:nowrap}

/* ── 🏅 BADGE SHELF ── */
.badge-label{margin-top:24px}
#badge-shelf{background:var(--s1);border:1px solid var(--border);border-radius:var(--rl);padding:14px}
.badge-count{font-family:var(--fd);font-size:.55rem;font-weight:700;letter-spacing:.18em;color:var(--dim);margin-bottom:12px}
.badge-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(38px,1fr));gap:8px}
.badge{display:flex;align-items:center;justify-content:center;aspect-ratio:1;border-radius:10px;background:var(--s1);border:1px solid var(--border);font-size:1.05rem;filter:grayscale(1);opacity:.32;cursor:help;transition:transform .18s ease,opacity .18s ease,filter .18s ease}
.badge.has{filter:none;opacity:1;border-color:rgba(255,215,0,.55);background:rgba(255,215,0,.09);box-shadow:0 0 14px rgba(255,215,0,.14)}
.badge.has:hover{transform:translateY(-2px) scale(1.06)}
.badge-ico{line-height:1}

/* ── 📈 MISSION CARD SPARKLINE ── */
.gc-spark{display:flex;align-items:center;gap:8px;margin-top:10px;padding-top:9px;border-top:1px solid var(--border)}
.gc-spark .spark{flex:1;height:22px;min-width:0}
.spark-best{font-family:var(--fd);font-size:.5rem;font-weight:700;letter-spacing:.08em;color:var(--dim);white-space:nowrap}

/* ── 🏆 LEADERBOARD SCOPE TABS ── */
#lb-tabs{display:flex;gap:6px;margin-bottom:12px}
.lb-tab{flex:1;font-family:var(--fd);font-size:.52rem;font-weight:700;letter-spacing:.08em;padding:8px 4px;border-radius:8px;border:1px solid var(--border);background:var(--s1);color:var(--dim);cursor:pointer;white-space:nowrap;transition:all .15s ease}
.lb-tab:hover{background:var(--s2);color:var(--text)}
.lb-tab.on{background:rgba(0,245,255,.12);border-color:var(--cyan);color:var(--cyan)}

/* ── ★ RIVAL STAR ── */
.lb-fr{background:none;border:none;color:var(--dimmer);font-size:.95rem;line-height:1;padding:0 6px;cursor:pointer;transition:color .15s ease,transform .15s ease}
.lb-fr:hover{color:var(--gold);transform:scale(1.2)}
.lb-fr.on{color:var(--gold)}

/* ── ⏳ FEATURED ROTATION ── */
#shop-rotation{font-family:var(--fd);font-size:.55rem;font-weight:700;letter-spacing:.14em;color:var(--dim);text-align:center;padding:10px 0 2px}
#shop-rotation em{font-style:normal;color:var(--gold)}
.shop-card.featured{border-color:rgba(255,215,0,.5);box-shadow:0 0 22px rgba(255,215,0,.12)}
.shop-feat{position:absolute;top:8px;left:8px;font-family:var(--fd);font-size:.46rem;font-weight:700;letter-spacing:.1em;padding:3px 7px;border-radius:20px;background:var(--gold);color:#000}
.shop-was{opacity:.55;margin-right:2px}

/* ── 🔥 BOSS RUSH BANNER ── */
.br-banner{border-color:rgba(255,102,0,.42);background:linear-gradient(120deg,rgba(255,102,0,.12),rgba(255,36,66,.07))}
.br-banner .mp-banner-title{color:var(--orange)}

/* ── 👁️ SPECTATOR ── */
.spec-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:16px}
.spec-code{font-family:var(--fd);font-size:.6rem;font-weight:700;letter-spacing:.2em;color:var(--dim)}
.spec-mode{font-family:var(--fd);font-size:.72rem;font-weight:700;color:var(--cyan)}
.spec-board{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:14px;margin-bottom:16px}
.spec-side{background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:14px 10px;text-align:center;min-width:0}
.spec-side.spec-empty{color:var(--dimmer);font-family:var(--fd);font-size:.58rem;letter-spacing:.14em}
.spec-name{font-family:var(--fd);font-size:.64rem;font-weight:700;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.spec-score{font-family:var(--fd);font-size:1.7rem;font-weight:900;color:var(--cyan);margin-top:6px;line-height:1}
.spec-vs{font-family:var(--fd);font-size:.66rem;font-weight:900;color:var(--pink);letter-spacing:.1em}
.spec-status{text-align:center;font-family:var(--fb);font-size:.9rem;color:var(--dim)}
.spec-result{text-align:center;font-family:var(--fd);font-size:.72rem;font-weight:700;color:var(--gold);margin-top:10px}
.spec-note{text-align:center;font-family:var(--fb);font-size:.76rem;color:var(--dimmer);margin-top:14px}

/* ══════════════════════════════════════════════════════════════
   📴 OFFLINE BANNER
   Fixed to the bottom rather than the top: the hub bar, the game
   header and the results card all own the top edge already.
   ══════════════════════════════════════════════════════════════ */
#offline-bar{
  display:none;
  position:fixed;
  left:0; right:0; bottom:0;
  z-index:9000;
  padding:8px 14px;
  padding-bottom:calc(8px + env(safe-area-inset-bottom, 0px));
  background:linear-gradient(90deg, rgba(255,145,0,.94), rgba(255,80,0,.94));
  color:#1a0d00;
  font-family:'Rajdhani',sans-serif;
  font-weight:700;
  font-size:.82rem;
  letter-spacing:.03em;
  text-align:center;
  text-shadow:none;
  box-shadow:0 -4px 18px rgba(255,120,0,.4);
  pointer-events:none;
}
#offline-bar b{ letter-spacing:.08em; }

/* The network-only doors stay visible but read as unavailable, so the hub
   does not appear to lose features while the connection is down. */
html.is-offline #btn-market,
html.is-offline #btn-save-acct,
html.is-offline #btn-mp-open,
html.is-offline #btn-boss-rush{
  opacity:.45;
  filter:grayscale(.7);
  cursor:not-allowed;
}

/* ══════════════════════════════════════════════════════════════════════
   ✨ FEATURE SUITE — chaos, squads, levels, kit, drives, exits, terminal
   ══════════════════════════════════════════════════════════════════════
   Everything below reuses the palette vars and the .card / .btn / .lb-row
   furniture already defined above, so a repointed --cyan (an equipped
   colour, or the colourblind-safe palette) carries through all of it. */

/* A third way into the matrix, on the shelf that already names it. */
.badge-label{cursor:pointer;transition:color .16s ease}
.badge-label:hover{color:var(--gold)}
.badge-open{font-style:normal;font-size:.5rem;color:var(--dimmer);letter-spacing:.14em;margin-left:6px}
.badge-label:hover .badge-open{color:var(--gold)}

/* ── 📈 OPERATIVE RAIL ── */
.xp-rail{
  display:flex;align-items:center;gap:14px;flex-wrap:wrap;
  padding:9px 18px;margin:0 auto 18px;max-width:1400px;
  border:1px solid var(--border);border-radius:var(--r);
  background:linear-gradient(90deg,rgba(0,245,255,.05),rgba(255,255,255,.02));
}
.xp-rail.has-points{border-color:rgba(57,255,20,.4);box-shadow:0 0 18px rgba(57,255,20,.1)}
.xp-lvl{font-family:var(--fd);font-size:.68rem;font-weight:900;color:var(--cyan);letter-spacing:.14em;white-space:nowrap}
.xp-track{flex:1;min-width:110px;height:7px;border-radius:7px;background:var(--s2);overflow:hidden;box-shadow:inset 0 0 0 1px var(--border)}
.xp-fill{height:100%;border-radius:7px;background:linear-gradient(90deg,var(--cyan),var(--lime));box-shadow:0 0 12px var(--cyan);transition:width .5s cubic-bezier(.2,.9,.3,1)}
.xp-num{font-family:var(--fd);font-size:.55rem;color:var(--dim);letter-spacing:.1em;white-space:nowrap}
.xp-perk-btn{animation:perkPulse 2s ease-in-out infinite}
@keyframes perkPulse{0%,100%{box-shadow:0 0 0 rgba(57,255,20,0)}50%{box-shadow:0 0 18px rgba(57,255,20,.55)}}

/* ── 📅 DAILY HACK ── */
.dh-banner{border-color:rgba(255,215,0,.38)!important}
.dh-banner .mp-banner-icon{filter:drop-shadow(0 0 10px var(--gold))}
.dh-banner.done{opacity:.82}
.dh-lb-label{margin-top:26px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.dh-reset{font-style:normal;font-size:.5rem;color:var(--dimmer);letter-spacing:.14em}
.dh-lb-panel{max-height:230px;overflow-y:auto}
.dh-row .lb-score{color:var(--gold)}
.dh-note{padding:12px 0 4px!important;font-size:.66rem!important}

/* ── 📟 GRID TERMINAL ── */
.chat-panel{
  border:1px solid var(--border);border-radius:var(--r);background:var(--s1);
  padding:12px;display:flex;flex-direction:column;gap:10px;
}
.chat-log{
  height:190px;overflow-y:auto;display:flex;flex-direction:column;gap:5px;
  font-family:var(--fd);font-size:.56rem;line-height:1.55;letter-spacing:.04em;
}
.chat-line{display:flex;gap:8px;align-items:baseline;animation:chatIn .3s ease}
@keyframes chatIn{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:none}}
.chat-time{color:var(--dimmer);flex:0 0 auto;font-size:.5rem}
.chat-body{flex:1;min-width:0;word-break:break-word;color:var(--dim)}
.chat-line.said .chat-body{color:var(--text)}
.chat-who{color:var(--cyan);font-weight:700}
.chat-sys{color:var(--lime);opacity:.82}
.chat-input-row{display:flex;gap:8px}
.chat-input{flex:1;min-width:0;font-size:.72rem;padding:8px 11px}

/* ── 📥 REPLAY ── */
.replay-panel{border:1px solid var(--border);border-radius:var(--r);background:var(--s1);padding:12px;display:flex;flex-direction:column;gap:10px}
.replay-row{display:flex;gap:8px}
.replay-input{flex:1;min-width:0;font-family:var(--fd);font-size:.62rem;padding:8px 11px;letter-spacing:.04em}
.replay-out:empty{display:none}
.replay-err{color:var(--red);font-size:.74rem;line-height:1.5}
.replay-card{border:1px solid var(--border2);border-radius:var(--r);background:var(--s2);padding:12px;margin-bottom:10px}
.replay-head{display:flex;align-items:center;gap:11px;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid var(--border)}
.replay-emoji{font-size:1.7rem;line-height:1}
.replay-title{font-family:var(--fd);font-size:.66rem;font-weight:900;letter-spacing:.1em}
.replay-by{font-size:.66rem;color:var(--dim)}
.replay-score{margin-left:auto;font-family:var(--fd);font-size:1.15rem;font-weight:900;color:var(--gold);text-align:right;line-height:1}
.replay-score em{display:block;font-style:normal;font-size:.44rem;color:var(--dimmer);letter-spacing:.16em}
.replay-card .rv.better{color:var(--lime)}
.replay-card .rv.worse{color:var(--red)}
.replay-try{width:100%}
/* The code itself: selectable, wrapping, and monospaced so a retyped
   character can be checked against the one it replaced. */
.res-replay:empty{display:none}
.res-replay{margin-top:14px}
.replay-code{
  font-family:var(--fd);font-size:.56rem;letter-spacing:.06em;line-height:1.6;
  word-break:break-all;user-select:all;-webkit-user-select:all;
  padding:10px 12px;border:1px dashed rgba(0,245,255,.4);border-radius:var(--r);
  background:rgba(0,245,255,.05);color:var(--cyan);text-align:left;
}
.replay-hint{margin-top:6px;font-size:.66rem;color:var(--dim)}

/* ── 🌀 CHAOS MODIFIER ── */
#chaos-banner{
  display:flex;align-items:center;gap:14px;
  margin:10px auto 0;max-width:660px;padding:12px 18px;
  border:1px solid rgba(255,36,66,.5);border-radius:var(--r);
  background:linear-gradient(100deg,rgba(255,36,66,.14),rgba(168,85,247,.1));
  box-shadow:0 0 26px rgba(255,36,66,.22);
  transition:padding .35s ease,gap .35s ease,max-width .35s ease;
}
#chaos-banner.big{animation:chaosIn .45s cubic-bezier(.2,.9,.3,1.35)}
@keyframes chaosIn{from{opacity:0;transform:translateY(-14px) scale(.94)}to{opacity:1;transform:none}}
/* Shrunk to a chip once the round is under way — still legible, out of the way. */
#chaos-banner.chip{max-width:340px;padding:5px 12px;gap:9px}
#chaos-banner.chip .chaos-desc{display:none}
#chaos-banner.chip .chaos-icon{font-size:1rem}
#chaos-banner.chip .chaos-name{font-size:.54rem}
.chaos-icon{font-size:1.7rem;line-height:1;filter:drop-shadow(0 0 10px var(--red))}
.chaos-txt{min-width:0}
.chaos-name{font-family:var(--fd);font-size:.7rem;font-weight:900;color:var(--red);letter-spacing:.16em}
.chaos-desc{font-size:.74rem;color:var(--dim);margin-top:2px}
/* 🕶️ BLIND TERMINAL. Only the play area blanks — the header, the clock and the
   score stay up, so the round remains legible as a round while the board is
   gone. Short and linear on purpose: this should read as a dropped feed, not
   as a fade. */
.g-area{transition:opacity .12s linear}
.g-area.blind-out{opacity:0}
/* ⏳ TIME DILATOR: a single cyan pulse over the board where the seconds landed. */
.g-area.dilated{animation:dilate .7s ease}
@keyframes dilate{0%{filter:none}30%{filter:brightness(1.5) drop-shadow(0 0 22px var(--cyan))}100%{filter:none}}

/* ── ⚡ POWER-UP DOCK ── */
#pu-dock{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:8px auto 0;padding:0 12px}
.pu-btn{
  display:flex;align-items:center;gap:7px;padding:6px 12px;
  border:1px solid rgba(57,255,20,.42);border-radius:99px;
  background:rgba(57,255,20,.08);color:var(--text);cursor:pointer;
  font-family:var(--fd);font-size:.52rem;letter-spacing:.12em;
  transition:transform .12s ease,box-shadow .2s ease,background .2s ease;
}
.pu-btn:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 0 18px rgba(57,255,20,.35);background:rgba(57,255,20,.15)}
.pu-btn:active:not(:disabled){transform:translateY(0)}
.pu-btn.passive{cursor:default;border-color:rgba(0,245,255,.42);background:rgba(0,245,255,.08);opacity:.9}
.pu-emoji{font-size:.95rem;line-height:1}
.pu-count{
  font-weight:900;min-width:17px;height:17px;line-height:17px;text-align:center;
  border-radius:99px;background:rgba(255,255,255,.14);font-size:.5rem;
}
.pu-btn.fired{animation:puFire .55s ease}
@keyframes puFire{0%{box-shadow:0 0 0 rgba(57,255,20,0)}25%{box-shadow:0 0 30px var(--lime);transform:scale(1.14)}100%{box-shadow:none;transform:none}}

/* ── 🛒 MARKET: consumables, drives, exit states ── */
.pu-kit{margin-bottom:20px}
.pu-kit-lbl{font-family:var(--fd);font-size:.56rem;color:var(--lime);letter-spacing:.16em;margin-bottom:9px}
.pu-kit-lbl.empty{color:var(--dim);letter-spacing:.08em;line-height:1.6;font-family:var(--fb);font-size:.78rem}
.pu-kit-row{display:flex;gap:9px;flex-wrap:wrap}
.pu-chip{
  display:inline-flex;align-items:center;gap:6px;padding:6px 12px;
  border:1px solid rgba(57,255,20,.35);border-radius:99px;background:rgba(57,255,20,.07);
  font-size:.74rem;
}
.pu-chip em{font-style:normal;font-family:var(--fd);font-size:.56rem;color:var(--lime)}
/* A drive or an exit state cannot be judged from a name and a price, so those
   cards carry a sentence — which makes them taller than a colour swatch. */
.shop-desc{font-size:.72rem;color:var(--dim);line-height:1.5;margin:6px 0 10px;text-align:center}
.shop-held{font-family:var(--fd);font-size:.5rem;color:var(--dimmer);letter-spacing:.14em;margin-bottom:9px}
.shop-held em{font-style:normal;color:var(--lime)}
.shop-card.consumable{border-color:rgba(57,255,20,.3)}

/* ── 👥 SQUAD SIZE PICKER ── */
.mp-teamsize{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:16px;padding:9px 14px;border:1px solid var(--border);border-radius:var(--r);background:var(--s1)}
.mp-teamsize.squad{border-color:rgba(168,85,247,.42);background:rgba(168,85,247,.06)}
.mp-teamsize-lbl{font-family:var(--fd);font-size:.55rem;color:var(--purple);letter-spacing:.16em}
.mp-size-btn{
  font-family:var(--fd);font-size:.52rem;letter-spacing:.12em;padding:6px 13px;
  border:1px solid var(--border2);border-radius:99px;background:transparent;
  color:var(--dim);cursor:pointer;transition:all .18s ease;
}
.mp-size-btn:hover{color:var(--text);border-color:var(--purple)}
.mp-size-btn.on{background:rgba(168,85,247,.18);border-color:var(--purple);color:var(--text);box-shadow:0 0 14px rgba(168,85,247,.3)}
.mp-teamsize-note{flex:1;min-width:180px;font-size:.72rem;color:var(--dim)}
/* A duel that cannot be played 2v2 is greyed rather than removed — a mode
   vanishing from the grid reads as a bug. */
.mp-mode.barred{opacity:.4;filter:grayscale(.75);cursor:not-allowed}
.mp-mode.barred:hover{transform:none}
.mp-mode.barred .mp-mode-meta{color:var(--purple)}

/* ── 👥 2v2 CO-OP GRID ── */
.mp-teams{display:grid;grid-template-columns:1fr auto 1fr;gap:14px;align-items:stretch;margin:18px 0}
.mp-team{display:flex;flex-direction:column;gap:9px;padding:12px;border:1px solid var(--border);border-radius:var(--r);background:var(--s1)}
.mp-team[data-team="alpha"]{border-color:rgba(0,245,255,.3)}
.mp-team[data-team="bravo"]{border-color:rgba(255,0,144,.3)}
/* Your own side is the one you should be able to find without reading. */
.mp-team.mine{background:rgba(255,255,255,.05);box-shadow:inset 0 0 0 1px var(--border2)}
.mp-team.mine .mp-team-name::after{content:' · YOUR SQUAD';color:var(--dimmer);font-size:.44rem}
.mp-team-hdr{display:flex;align-items:baseline;justify-content:space-between;gap:8px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.mp-team-name{font-family:var(--fd);font-size:.56rem;font-weight:900;letter-spacing:.14em}
.mp-team[data-team="alpha"] .mp-team-name{color:var(--cyan)}
.mp-team[data-team="bravo"] .mp-team-name{color:var(--pink)}
/* The side's headcount, not a score — a lobby has no round to report yet. It
   goes amber while a side is a seat short, which is the state a waiting player
   is actually scanning for. */
.mp-team-score{font-family:var(--fd);font-size:.7rem;font-weight:900;color:var(--lime);letter-spacing:.08em}
.mp-team-score.short{color:var(--gold)}
.mp-vs-teams{align-self:center}

/* ── 🏆 ACHIEVEMENT MATRIX ── */
.ach-tint .fb-modal{max-width:760px;border-color:rgba(255,215,0,.42);box-shadow:0 0 70px rgba(255,215,0,.13),inset 0 0 0 1px rgba(255,255,255,.03)}
.ach-tint .fb-modal::before{background:linear-gradient(90deg,transparent,var(--gold),transparent);box-shadow:0 0 12px var(--gold)}
.ach-progress{height:6px;border-radius:6px;background:var(--s2);overflow:hidden;margin-bottom:20px;box-shadow:inset 0 0 0 1px var(--border)}
.ach-progress-fill{height:100%;border-radius:6px;background:linear-gradient(90deg,var(--gold),var(--lime));box-shadow:0 0 12px var(--gold);transition:width .6s cubic-bezier(.2,.9,.3,1)}
.ach-matrix{display:grid;grid-template-columns:repeat(auto-fill,minmax(178px,1fr));gap:11px}
.ach-cell{
  display:flex;flex-direction:column;gap:5px;padding:13px 12px;
  border:1px solid var(--border);border-radius:var(--r);background:var(--s1);
  transition:transform .16s ease,box-shadow .2s ease;
}
.ach-cell:hover{transform:translateY(-2px)}
/* Locked = dim grey, unlocked = neon green. The state is carried by the border,
   the icon and the text together rather than by colour alone, so it survives
   the colourblind-safe palette and a greyscale print alike. */
.ach-cell.locked{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}
.ach-cell.locked .ach-ico{filter:grayscale(1);opacity:.34}
.ach-cell.locked .ach-name{color:#8a8a96}
.ach-cell.locked .ach-desc,.ach-cell.locked .ach-cr{color:rgba(255,255,255,.22)}
.ach-cell.locked .ach-state{color:#8a8a96}
.ach-cell.unlocked{border-color:rgba(57,255,20,.5);background:rgba(57,255,20,.07);box-shadow:0 0 20px rgba(57,255,20,.1)}
.ach-cell.unlocked .ach-name{color:var(--lime)}
.ach-cell.unlocked .ach-state{color:var(--lime)}
.ach-cell.unlocked:hover{box-shadow:0 0 30px rgba(57,255,20,.22)}
.ach-ico{font-size:1.6rem;line-height:1}
.ach-name{font-family:var(--fd);font-size:.58rem;font-weight:900;letter-spacing:.1em}
.ach-desc{font-size:.72rem;color:var(--dim);line-height:1.5;flex:1}
.ach-foot{display:flex;justify-content:space-between;gap:8px;align-items:baseline;margin-top:4px;padding-top:7px;border-top:1px solid var(--border)}
.ach-cr{font-size:.66rem;color:var(--gold)}
.ach-state{font-family:var(--fd);font-size:.44rem;letter-spacing:.14em}

/* ── 🌳 PERK TREE ── */
.perk-tint .fb-modal{max-width:600px;border-color:rgba(57,255,20,.45)}
.perk-tree{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
.perk-node{
  display:flex;flex-direction:column;gap:6px;padding:16px 14px;
  border:1px solid var(--border);border-radius:var(--rl);background:var(--s1);
  position:relative;transition:transform .16s ease,box-shadow .2s ease;
}
.perk-node.open{border-color:rgba(57,255,20,.45);box-shadow:0 0 22px rgba(57,255,20,.12)}
.perk-node.open:hover{transform:translateY(-3px)}
.perk-node.owned{border-color:var(--lime);background:rgba(57,255,20,.08)}
.perk-node.locked{opacity:.55}
.perk-ico{font-size:1.7rem;line-height:1}
.perk-name{font-family:var(--fd);font-size:.66rem;font-weight:900;letter-spacing:.1em}
.perk-effect{font-family:var(--fd);font-size:.52rem;color:var(--lime);letter-spacing:.1em}
.perk-desc{font-size:.74rem;color:var(--dim);line-height:1.55;flex:1;margin-bottom:6px}
.perk-tag{font-family:var(--fd);font-size:.5rem;color:var(--lime);letter-spacing:.16em}

/* ══════════════════════════════════════════════════════════════
   💀 EXIT STATES
   One stage, three looks. It sits above every screen and below the
   toast, so the flourish covers the board it belongs to without
   hiding a message that outlives it.
   ══════════════════════════════════════════════════════════════ */
#exit-fx{position:fixed;inset:0;z-index:2500;display:none;pointer-events:none;overflow:hidden}
#exit-fx.show{display:block}
#exit-fx-body{position:absolute;inset:0}

/* 🟦 FAKE BSOD — the real proportions: a lot of blue, a small block of text
   pinned about a sixth of the way down, and a percentage that never quite
   explains itself. */
#exit-fx.fx-bsod{background:#0078d7;animation:bsodIn .16s steps(2)}
@keyframes bsodIn{from{opacity:0}to{opacity:1}}
#exit-fx.fx-bsod #exit-fx-body{
  inset:auto 0 0 0;top:16%;padding:0 8vw;max-width:1000px;margin:0 auto;
  font-family:'Segoe UI',var(--fb),sans-serif;color:#fff;text-align:left;
}
.bsod-face{font-size:6.5rem;line-height:1;font-weight:300;margin-bottom:26px}
.bsod-head{font-size:1.55rem;font-weight:400;line-height:1.35;margin-bottom:14px}
.bsod-sub{font-size:1rem;opacity:.92;margin-bottom:26px;line-height:1.5}
.bsod-dump{
  font-family:Consolas,var(--fd),monospace;font-size:.72rem;line-height:1.75;
  white-space:pre;overflow-x:auto;opacity:.9;
  padding:12px 14px;background:rgba(0,0,0,.16);border-radius:4px;margin-bottom:18px;
}
.bsod-pct{font-size:1.15rem;font-variant-numeric:tabular-nums}

/* 🟩 MATRIX DISSOLVE — columns of glyphs falling past a darkening board. */
#exit-fx.fx-matrix{background:rgba(2,6,4,.55);animation:mxFade 1.5s ease forwards}
@keyframes mxFade{0%{background:rgba(2,6,4,0)}45%{background:rgba(2,6,4,.72)}100%{background:rgba(2,6,4,.95)}}
.mx-col{
  position:absolute;top:-115%;font-family:var(--fd),monospace;font-size:1rem;
  line-height:1.05;color:#00ff41;text-shadow:0 0 9px #00ff41;
  opacity:.82;white-space:nowrap;
  animation-name:mxFall;animation-timing-function:linear;animation-fill-mode:forwards;
}
@keyframes mxFall{
  0%{transform:translateY(0);opacity:0}
  12%{opacity:.9}
  100%{transform:translateY(230vh);opacity:0}
}

/* 📺 STATIC EXPLOSION — a CRT flash, a burst of noise, one roll bar. */
#exit-fx.fx-static{background:#000}
.crt-flash{position:absolute;inset:0;background:#fff;animation:crtFlash .9s ease-out forwards}
@keyframes crtFlash{0%{opacity:1}14%{opacity:.15}30%{opacity:.6}100%{opacity:0}}
/* Repeating gradients rather than an image: no asset to load, and it scales to
   any viewport without resampling. */
.crt-noise{
  position:absolute;inset:0;opacity:.72;mix-blend-mode:screen;
  background:
    repeating-linear-gradient(0deg,rgba(255,255,255,.16) 0 1px,transparent 1px 3px),
    repeating-linear-gradient(90deg,rgba(255,255,255,.09) 0 2px,transparent 2px 5px);
  animation:crtNoise .07s steps(2) infinite;
}
@keyframes crtNoise{0%{transform:translate(0,0)}50%{transform:translate(-3px,2px)}100%{transform:translate(2px,-2px)}}
.crt-roll{
  position:absolute;left:0;right:0;height:22vh;
  background:linear-gradient(180deg,transparent,rgba(255,255,255,.22),transparent);
  animation:crtRoll .9s linear forwards;
}
@keyframes crtRoll{from{top:-25vh}to{top:105vh}}

/* ── 👁️ REDUCED MOTION ──
   The blanket html.reduced-motion rule further up already flattens every
   animation and transition added above, and playExitFx() declines to run at
   all when the preference is set — so the only thing left is the one effect
   that is GAMEPLAY rather than decoration.

   🕶️ BLIND TERMINAL is a repeating full-screen blackout, which is precisely
   the pattern the preference exists to avoid. It is not simply switched off —
   that would hand a player an easier round for their accessibility setting —
   so it becomes a heavy dim instead: the board still goes unreadable for the
   same 0.3s, without the strobe. */
html.reduced-motion .g-area.blind-out{opacity:.12}

/* ── 📱 NARROW SCREENS ──
   The canvas is sized by fitCanvas() against the space left over, so anything
   that adds height above the board costs play area on a phone. The two
   round-time additions shrink first. */
@media(max-width:760px){
  .xp-rail{gap:9px;padding:8px 12px;margin-bottom:14px}
  .xp-num{display:none}
  .mp-teams{grid-template-columns:1fr;gap:10px}
  .mp-vs-teams{padding:2px 0}
  .ach-matrix{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:9px}
  .perk-tree{grid-template-columns:1fr}
  .chat-log{height:150px}
  #chaos-banner{margin-top:6px;padding:8px 12px;gap:10px}
  #chaos-banner .chaos-desc{font-size:.68rem}
  .bsod-face{font-size:4rem;margin-bottom:16px}
  .bsod-head{font-size:1.15rem}
  .bsod-sub{font-size:.86rem;margin-bottom:16px}
  .bsod-dump{font-size:.6rem}
  #exit-fx.fx-bsod #exit-fx-body{top:12%;padding:0 7vw}
}
/* Landscape phones have no vertical room at all: the chaos banner collapses to
   its chip immediately and the dock tightens, rather than pushing the board
   off the bottom of the screen. */
@media(max-height:560px) and (orientation:landscape){
  #chaos-banner{max-width:300px;padding:4px 10px;gap:8px;margin-top:4px}
  #chaos-banner .chaos-desc{display:none}
  #chaos-banner .chaos-icon{font-size:.95rem}
  #chaos-banner .chaos-name{font-size:.5rem}
  #pu-dock{margin-top:4px}
  .pu-btn{padding:4px 9px;font-size:.46rem}
  .xp-rail{display:none}
}

/* ══════════════════════════════════════════════════════════════════════
   🧊 3D MODE
   ══════════════════════════════════════════════════════════════════════ */

/* ── LOGIN-SCREEN RENDER PICKER ──
   Two cards above the sign-in form. Deliberately sits OUTSIDE .card so it
   reads as a decision about the whole arcade rather than a field on the
   login form — you are choosing how the game looks, not how you sign in. */
.mode-picker{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:18px 0 16px}
.mode-card{
  display:flex;flex-direction:column;align-items:center;gap:5px;
  padding:14px 10px;cursor:pointer;text-align:center;
  background:var(--s1);border:1px solid var(--border);border-radius:var(--r);
  color:#fff;font-family:var(--fb);
  transition:border-color .18s,background .18s,transform .12s,box-shadow .18s
}
.mode-card:hover{background:var(--s2);border-color:var(--border2);transform:translateY(-2px)}
.mode-card:active{transform:translateY(0) scale(.98)}
.mode-ico{font-size:1.7rem;line-height:1}
.mode-name{font-family:var(--fd);font-weight:700;font-size:.78rem;letter-spacing:.06em}
.mode-note{font-size:.7rem;opacity:.6;line-height:1.3}
/* The selected card is lit from its own edge — same visual grammar the
   difficulty tiers use, so "this one is armed" reads the same everywhere. */
.mode-card.on{
  background:rgba(0,245,255,.10);border-color:rgba(0,245,255,.6);
  box-shadow:0 0 0 1px rgba(0,245,255,.25),0 6px 24px -8px rgba(0,245,255,.5)
}
.mode-card.on .mode-name{color:var(--cyan);text-shadow:0 0 14px rgba(0,245,255,.6)}
.mode-card.on .mode-note{opacity:.8}
/* No WebGL2 here. Still clickable so the click can explain itself, but it
   never pretends to be an option. */
.mode-card.unsupported{opacity:.42;cursor:not-allowed}
.mode-card.unsupported .mode-note{color:var(--orange);opacity:.9}

/* ── THE GL SURFACE ──
   Layered exactly over #arcade-canvas inside #board-frame. pointer-events is
   off so every tap and drag falls through to the 2D canvas underneath, which
   is what lets bindCanvasDrag() keep serving both renderers with one code
   path. The border and radius are copied so the two elements occupy the same
   rectangle to the pixel. */
#gl-canvas{
  position:absolute;left:0;top:0;display:none;
  pointer-events:none;
  border:2px solid var(--border);border-radius:8px;
  background:#04040e
}
/* Floating combat text, projected from world space each frame. Lives above
   the GL canvas but below #touch-hint and #mp-overlay. */
#gl-fx{
  position:absolute;left:0;top:0;display:none;
  pointer-events:none;overflow:hidden;border-radius:8px
}
/* ── VIRTUAL THUMBSTICK ──
   Cyber Arena's steering ring. The 2D build paints it straight onto the board
   with the rest of the frame; in 3D the board is an opaque GL surface, so it
   rides in the fx layer instead — same ring, same knob, same place under the
   finger, drawn in the board's own pixel space. `transition:none` for the same
   reason .gl-label has it: this is repositioned every frame. */
.gl-stick,.gl-stick-knob{
  position:absolute;left:0;top:0;border-radius:50%;
  border:2px solid var(--cyan);
  box-shadow:0 0 12px rgba(0,245,255,.55);
  transition:none;pointer-events:none;display:none
}
.gl-stick{opacity:.45}
.gl-stick-knob{opacity:.8;background:rgba(0,245,255,.25)}
.gl-pop{
  position:absolute;left:0;top:0;
  font-family:var(--fd);font-weight:700;font-size:15px;letter-spacing:.04em;
  white-space:nowrap;text-shadow:0 0 12px currentColor,0 2px 6px rgba(0,0,0,.9);
  will-change:transform,opacity
}
/* The 2D canvas is still the input surface in 3D mode, so it stays in the
   layout — it just stops painting a visible board behind the GL one. */
#arcade-canvas.behind-gl{background:transparent;border-color:transparent}

/* ── PERSISTENT WORLD LABELS ──
   .gl-pop is a firework: it fades and dies. Some 3D missions need a caption
   PINNED to an object for the whole round — a status line over the mainframe
   keypad, the match percentage over a scope. Same projected div, no lifetime.
   `transition:none` matters: these are re-positioned every frame, and a
   transition on transform would smear them a frame behind the object. */
.gl-label{transition:none;text-align:center}
.gl-status{font-size:13px;opacity:.95;letter-spacing:.12em;text-transform:uppercase}
.gl-big{font-size:26px;font-weight:900;letter-spacing:.06em}

/* 🔢 MATH BLITZ in 3D keeps its console but not its question readout — the sum
   is an object in the room now, and two of them would be one too many. */
.math-wrap.math-3d{padding-top:6px}
.math-wrap.math-3d #math-question{display:none}

/* A quiet badge on the hub so the player can always tell which renderer a
   session is running, without opening the login screen to check. */
body.mode-3d .hub-bar::after{
  content:'🧊 3D';
  font-family:var(--fd);font-size:.6rem;letter-spacing:.1em;
  color:var(--cyan);opacity:.75;align-self:center;
  padding:3px 8px;border:1px solid rgba(0,245,255,.4);border-radius:20px
}

@media (max-width:420px){
  .mode-picker{gap:8px}
  .mode-card{padding:11px 7px}
  .mode-note{display:none}
}


/* ══════════════════════════════════════════════════════════════════════
   § 10 — THE LANES: endless, anomaly, loadouts, tutorial, photo mode
   ══════════════════════════════════════════════════════════════════════
   Everything here reuses furniture that already exists — .mp-banner for the two
   new lanes, .lb-row for their boards, .fb-overlay for their modals — so the
   arcade gains four screens and the eye has nothing new to learn. Only the
   accents are new. The 3D chrome block at the end of this file picks the new
   selectors up by adding them to the lists that were already there. */

/* ── ♾️ 🌒 THE TWO LANE BANNERS ── */
.en-banner{border-color:rgba(0,245,255,.34);background:linear-gradient(135deg,rgba(0,245,255,.08),rgba(168,85,247,.05))}
.en-banner .mp-banner-title{color:var(--cyan)}
.an-banner{border-color:rgba(168,85,247,.36);background:linear-gradient(135deg,rgba(168,85,247,.10),rgba(255,0,144,.05))}
.an-banner .mp-banner-title{color:var(--purple)}
.en-banner.done,.an-banner.done{opacity:.78}
#btn-endless:disabled{opacity:.45;cursor:not-allowed}
.lb-tag{font-style:normal;font-size:.62rem;opacity:.62;letter-spacing:.06em;margin-left:6px}

/* ── ♾️ THE ENDLESS PILL ── the integrity bar Boss Rush already models. */
.en-pill em{color:var(--cyan)}
.res-bonus-endless{color:var(--cyan);border-color:var(--cyan);background:rgba(0,245,255,.08)}

/* ── ♾️ THE PICKER ── */
.en-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:10px}
.en-card{
  display:flex;flex-direction:column;align-items:flex-start;gap:4px;
  padding:12px 14px;text-align:left;cursor:pointer;
  background:var(--s1);border:1px solid var(--border);border-radius:var(--r);
  color:var(--txt);font-family:var(--fb);transition:border-color .16s,transform .16s
}
.en-card:hover:not(.locked){border-color:var(--cyan);transform:translateY(-2px)}
.en-card.locked{opacity:.4;cursor:not-allowed}
.en-ico{font-size:1.5rem;line-height:1}
.en-name{font-family:var(--fd);font-size:.72rem;font-weight:700;letter-spacing:.06em}
.en-best{font-size:.66rem;color:var(--dim);letter-spacing:.04em}

/* ── 🎒 THE LOADOUT RAIL ── */
.loadout-rail{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  max-width:1060px;margin:0 auto 10px;padding:8px 14px;
  background:var(--s1);border:1px solid var(--border);border-radius:var(--r)
}
.lo-lbl{font-family:var(--fd);font-size:.58rem;font-weight:700;letter-spacing:.12em;color:var(--dim)}
.lo-none{font-size:.72rem;color:var(--dim)}
.lo-chip{
  font-family:var(--fd);font-size:.62rem;font-weight:700;letter-spacing:.08em;
  padding:5px 12px;border-radius:999px;cursor:pointer;
  background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--txt);
  transition:border-color .15s,color .15s,background .15s
}
.lo-chip:hover{border-color:var(--cyan);color:var(--cyan)}
.lo-chip.on{border-color:var(--lime);color:var(--lime);background:rgba(57,255,20,.09)}
.loadout-rail .btn{margin-left:auto}

/* ── 🎒 THE SHELF ── */
.lo-new{display:flex;gap:10px;margin-bottom:10px;flex-wrap:wrap}
.lo-name{flex:1;min-width:180px;text-transform:uppercase;letter-spacing:.08em}
.lo-now{
  font-size:.72rem;color:var(--dim);padding:8px 12px;margin-bottom:14px;
  background:rgba(255,255,255,.03);border:1px dashed var(--border);border-radius:var(--r)
}
.lo-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px}
.lo-card{background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:12px 14px}
.lo-card-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:6px}
.lo-card-name{font-family:var(--fd);font-size:.78rem;font-weight:700;letter-spacing:.07em;color:var(--cyan)}
.lo-card-body{font-size:.7rem;color:var(--dim);line-height:1.5;margin-bottom:8px}
.lo-warn{
  font-size:.66rem;color:var(--gold);line-height:1.45;margin-bottom:8px;
  padding:6px 8px;background:rgba(255,215,0,.07);border-left:2px solid var(--gold);border-radius:4px
}
.lo-card-btns{display:flex;gap:8px}

/* ── 🎓 THE HOW FACE ──
   A second face on the card, not a second element beside it, so the 3D chrome's
   extruded card treatment applies to it unchanged. .game-card gets
   transform-style:preserve-3d in the 3D block; here the flip is a plain
   rotateY, which reads correctly flat as well. */
/* The flip needs a perspective on the CARD, in both chromes. perspective is a
   property of the parent and does not need preserve-3d on it, so this works
   under the base .game-card overflow:hidden as well as under the 3D block that
   re-declares it visible. */
.game-card{position:relative;perspective:900px}
/* ⚠️ THE CARD'S OWN TEXT MUST NOT EAT THE BADGE'S CLICKS.
   Every one of these is decorative — an emoji, a name, a blurb, a points chip.
   None of them has a handler; the CARD does. But .gc-icon is a full-width block
   68px tall, and in the 3D chrome it carries translateZ(26px) while the badge
   sat at translateZ(0) — and inside a preserve-3d parent, elements are sorted
   by their position in SPACE, not by z-index. So the icon rendered and
   hit-tested ON TOP of the badge and covered 67% of it: measured with
   elementFromPoint, the badge's own CENTRE returned .gc-icon, and only a ~8px
   sliver along its top edge was actually clickable. That is the whole of "the
   ⓘ is very hard to click", and no amount of z-index would have fixed it.
   Clicks fall through to the card, which is what should have been handling
   them all along. */
.game-card > .gc-icon,
.game-card > .gc-name,
.game-card > .gc-desc,
.game-card > .gc-pts,
.game-card > .gc-lock,
.game-card > .gc-spark{ pointer-events:none }

.gc-howbtn{
  position:absolute;top:8px;right:8px;z-index:3;
  width:30px;height:30px;line-height:28px;padding:0;text-align:center;
  border-radius:50%;cursor:pointer;font-size:1rem;
  background:rgba(255,255,255,.06);border:1px solid var(--border);color:var(--dim);
  transition:color .15s,border-color .15s,background .15s
}
/* The target is bigger than the circle. A 30px dot in a card corner is a small
   thing to hit with a mouse and a hopeless one with a thumb, and growing the
   VISIBLE dot enough to fix that would make it the loudest thing on the card. */
.gc-howbtn::after{content:'';position:absolute;inset:-9px;border-radius:50%}
.gc-howbtn:hover{color:var(--cyan);border-color:var(--cyan);background:rgba(0,245,255,.1)}
/* A real target on a phone. The badge is the only way into the briefing that
   does not require holding the card down for half a second, and at 24px it was
   below every touch-target guideline there is — which is exactly why the long
   press had to exist. It still does, but it is now a shortcut rather than the
   thing that rescues a missed tap. */
@media (pointer: coarse){
  .gc-howbtn{width:40px;height:40px;line-height:38px;font-size:1.15rem;top:6px;right:6px}
  .gc-howbtn::after{inset:-6px}
}

/* ══════════════════════════════════════════════
   📖 MISSION BRIEFING
   The overlay the ⓘ opens. It replaced a flip on the card face, which could
   hold one paragraph and no controls — see buildHowFaces(). Sections are
   ordered summary → controls → steps → tips so a first-timer can stop reading
   at the steps and a returning player can scroll straight to the tips.
   ══════════════════════════════════════════════ */
.brief-modal{max-width:560px;border-color:rgba(0,245,255,.42);
  box-shadow:0 0 70px rgba(0,245,255,.13),inset 0 0 0 1px rgba(255,255,255,.03)}
.brief-modal::before{background:linear-gradient(90deg,transparent,var(--cyan),transparent);
  box-shadow:0 0 12px var(--cyan)}
.brief-body{display:flex;flex-direction:column;gap:16px;text-align:left}

.brf-hero{display:flex;align-items:center;gap:14px}
.brf-ico{font-size:2.4rem;line-height:1;flex-shrink:0}
.brf-name{font-family:var(--fd);font-size:1rem;font-weight:900;letter-spacing:.08em;color:var(--cyan)}
.brf-hook{font-size:.8rem;line-height:1.45;color:var(--dim);margin-top:3px}
/* line-height is explicit because the display face has tall caps and this line
   wraps to two of them on a phone — at the UA default the descenders of the
   second line sat on the border. */
.brf-locked{font-family:var(--fd);font-size:.58rem;letter-spacing:.1em;line-height:1.6;color:var(--gold);
  background:rgba(255,215,0,.08);border:1px solid rgba(255,215,0,.3);border-radius:8px;padding:9px 11px}

.brf-sec{display:flex;flex-direction:column;gap:8px}
.brf-h{font-family:var(--fd);font-size:.6rem;font-weight:700;letter-spacing:.16em;color:var(--dimmer)}
.brf-p{font-size:.86rem;line-height:1.6;color:var(--txt);margin:0}

.brf-ctrls{display:flex;flex-direction:column;gap:7px}
/* Grid, not flex: the two tags must share ONE column width so the control text
   starts at the same x on both rows. Auto-width tags put "TOUCH" and "KEYS" a
   few px apart and the block read as two unrelated lines. */
.brf-ctrl{display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:baseline;
  background:var(--s2);border:1px solid var(--border);border-radius:9px;padding:9px 12px}
.brf-ctag{font-family:var(--fd);font-size:.5rem;letter-spacing:.12em;color:var(--dimmer);white-space:nowrap}
.brf-ctxt{font-size:.78rem;line-height:1.45;color:var(--txt)}

.brf-steps{margin:0;padding-left:0;list-style:none;counter-reset:brf;
  display:flex;flex-direction:column;gap:9px}
.brf-steps li{counter-increment:brf;position:relative;padding-left:32px;
  font-size:.84rem;line-height:1.55;color:var(--txt)}
.brf-steps li::before{content:counter(brf);position:absolute;left:0;top:-1px;
  width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  font-family:var(--fd);font-size:.6rem;font-weight:700;
  background:rgba(0,245,255,.12);border:1px solid rgba(0,245,255,.45);color:var(--cyan)}

.brf-tips{margin:0;padding-left:0;list-style:none;display:flex;flex-direction:column;gap:9px}
.brf-tips li{position:relative;padding-left:24px;font-size:.82rem;line-height:1.55;color:var(--dim)}
.brf-tips li::before{content:'▸';position:absolute;left:6px;top:0;color:var(--lime)}

.brf-foot{font-family:var(--fd);font-size:.58rem;letter-spacing:.1em;color:var(--dimmer);
  border-top:1px solid var(--border);padding-top:13px}
.brf-foot em{font-style:normal;color:var(--gold)}
.brf-actions{display:flex;gap:10px}
.brf-actions .btn{flex:1;min-height:42px}

@media (max-width: 640px){
  .brief-modal{padding:22px 16px 18px}
  .brf-ico{font-size:1.9rem}
  .brf-name{font-size:.85rem}
  /* The tag goes above the text on a narrow screen — side by side, a long key
     list wrapped to three ragged lines beside a one-word label. */
  .brf-ctrl{grid-template-columns:1fr;gap:4px}
}

/* ── 🎓 ONBOARDING ── */
.ob-modal{max-width:520px;text-align:center}
.ob-body-wrap{padding:6px 4px 2px}
.ob-ico{font-size:3rem;line-height:1;margin-bottom:10px}
.ob-title{font-family:var(--fd);font-size:1.05rem;font-weight:900;letter-spacing:.1em;color:var(--cyan);margin-bottom:10px}
.ob-body{font-size:.88rem;line-height:1.65;color:var(--txt);margin-bottom:18px}
.ob-dots{display:flex;gap:7px;justify-content:center;margin-bottom:18px}
.ob-dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,.16);transition:background .2s,transform .2s}
.ob-dot.on{background:var(--cyan);transform:scale(1.35)}
.ob-btns{display:flex;gap:10px;justify-content:center}
.ob-coach{
  position:relative;z-index:6;max-width:min(620px,94vw);margin:0 auto 6px;
  padding:9px 16px;border-radius:999px;text-align:center;
  font-family:var(--fb);font-size:.8rem;font-weight:600;letter-spacing:.03em;color:#04040e;
  background:linear-gradient(90deg,var(--cyan),var(--lime));
  box-shadow:0 6px 24px rgba(0,245,255,.35);
  opacity:0;transform:translateY(-6px);pointer-events:none;
  transition:opacity .28s ease,transform .28s ease
}
.ob-coach.show{opacity:1;transform:translateY(0)}

/* ── 📸 PHOTO MODE ── */
#photo-bar{
  position:fixed;left:50%;bottom:max(16px,env(safe-area-inset-bottom));transform:translateX(-50%);
  z-index:120;display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center;
  max-width:94vw;padding:9px 14px;border-radius:999px;
  background:rgba(6,8,18,.92);border:1px solid var(--cyan);
  box-shadow:0 10px 40px rgba(0,0,0,.6),0 0 26px rgba(0,245,255,.22)
}
.pb-tag{font-family:var(--fd);font-size:.6rem;font-weight:700;letter-spacing:.14em;color:var(--cyan)}
/* The HUD is hidden by class rather than by inline style, so RESUME puts it
   back by removing one class and cannot leave a stat pill stranded. */
body.photo-nohud .g-hdr,
body.photo-nohud .prog-bar,
body.photo-nohud #pu-dock,
body.photo-nohud #chaos-banner,
body.photo-nohud #mp-hud,
body.photo-nohud .ghost-pace,
body.photo-nohud #arcade-controls,
body.photo-nohud #gl-fx{opacity:0;pointer-events:none;transition:opacity .2s}
body.photo-mode #board-frame{cursor:grab}
body.photo-mode #board-frame:active{cursor:grabbing}
/* The round is frozen, so every control that feeds it is too. The board stays
   live because the orbit drags on it (bindCanvasDrag() is what keeps those
   drags out of the game); the pad, the power-up dock and every panel a mission
   mounts under the board — Battle Bots' deploy deck, the Frequency sliders,
   Math Blitz's answer box — stop taking clicks until RESUME. Before this a
   SLAM on the pad dropped a Tetris piece, and Math Blitz's Submit scored, on
   a round whose clock was held. (Keys are photoBindOrbit()'s job.) By class,
   like the HUD, so RESUME cannot leave a control switched off. */
body.photo-mode #arcade-controls,
body.photo-mode #pu-dock,
body.photo-mode .g-area > :not(#g-canvas-holder){pointer-events:none}
.photo-modal{max-width:820px}
.photo-body{display:flex;flex-direction:column;gap:12px;align-items:center}
.ph-img{max-width:100%;border-radius:var(--r);border:1px solid var(--border)}
.ph-code{
  font-family:monospace;font-size:.66rem;word-break:break-all;text-align:center;
  color:var(--gold);background:rgba(255,215,0,.06);border:1px solid rgba(255,215,0,.24);
  border-radius:var(--r);padding:8px 12px;max-width:100%
}
.ph-btns{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}

/* ── 🎮 THE VIRTUAL POINTER ── */
#pad-cursor{
  position:fixed;left:0;top:0;width:22px;height:22px;border-radius:50%;
  z-index:115;pointer-events:none;display:none;
  border:2px solid var(--cyan);background:rgba(0,245,255,.16);
  box-shadow:0 0 14px rgba(0,245,255,.6);
  transition:background .08s,transform .02s linear
}
#pad-cursor.down{background:rgba(0,245,255,.55);box-shadow:0 0 22px rgba(0,245,255,.9)}
.ctrl-btn.pad-lit{filter:brightness(1.45)}
.inp-toggle.on{border-color:var(--lime);color:var(--lime)}
/* ✨ GRAPHICS PROFILE — Normal / Ultra Realistic. Gold when Ultra is on: it is
   the premium, heavier setting, and gold is the arcade's colour for that. The
   in-game ✨ is an emoji, which ignores `color`, so its state is carried by the
   border, the glow and the opacity instead. */
.gfx-toggle.on{border-color:var(--gold);color:var(--gold);box-shadow:0 0 14px rgba(255,215,0,.18)}
.g-actions .btn-gfx-game{opacity:.78}
.g-actions .btn-gfx-game.on{opacity:1;border-color:rgba(255,215,0,.55);box-shadow:0 0 14px rgba(255,215,0,.22)}
body.mode-3d .gfx-toggle.on,body.mode-3d .g-actions .btn-gfx-game.on{--wall:#8a6b00;--wallb:#453600}

/* ── 📁 THE MISSION DOSSIER PAGE ── */
.dos-mission[data-dos]{cursor:pointer;transition:border-color .16s,transform .16s}
.dos-mission[data-dos]:hover,
.dos-mission[data-dos]:focus-visible{border-color:var(--cyan);transform:translateY(-2px);outline:none}
.dos-back{margin-bottom:12px}
.dos-hero{display:flex;align-items:center;gap:14px;margin-bottom:16px}
.dos-hero-ico{font-size:2.6rem;line-height:1}
.dos-hero-name{font-family:var(--fd);font-size:1.05rem;font-weight:900;letter-spacing:.08em;color:var(--cyan)}
.dos-hero-sub{font-size:.76rem;line-height:1.5;color:var(--dim);margin-top:4px}
.dos-pace{width:100%;height:180px;display:block;background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:var(--r)}
.dos-axes{display:flex;justify-content:space-between;font-size:.6rem;color:var(--dim);padding:4px 2px 0}
.dos-legend{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px}
.dl-chip{
  font-family:var(--fd);font-size:.58rem;font-weight:700;letter-spacing:.07em;
  padding:3px 9px;border-radius:999px;color:var(--c);border:1px solid var(--c);background:rgba(255,255,255,.03)
}
.dos-ends{display:flex;align-items:flex-end;gap:4px;height:120px;padding:8px 4px 0;
  background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:var(--r)}
.de-bar{flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;height:100%;gap:4px}
.de-bar>span{
  width:100%;border-radius:3px 3px 0 0;
  background:linear-gradient(180deg,var(--cyan),rgba(0,245,255,.18))
}
.de-bar>em{font-style:normal;font-size:.52rem;color:var(--dim)}
.dos-endnote{font-size:.72rem;color:var(--dim);margin-top:8px;line-height:1.5}
.dos-endnote b{color:var(--cyan);font-weight:700}
.dos-last{display:flex;align-items:flex-end;gap:5px;height:96px;padding:8px 4px 0;
  background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:var(--r)}
.dl-cell{flex:1;height:var(--h);min-height:6px;border-radius:3px 3px 0 0;position:relative;
  background:linear-gradient(180deg,var(--lime),rgba(57,255,20,.16))}
.dl-cell>em{position:absolute;inset:auto 0 -16px;font-style:normal;font-size:.5rem;color:var(--dim);text-align:center}
.dos-mbtns{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}

/* Overlay tints — one variable each, the same shape the existing tints use. */
.endless-tint .fb-modal{border-color:rgba(0,245,255,.34)}
.loadout-tint .fb-modal{border-color:rgba(57,255,20,.3)}
.onboard-tint .fb-modal{border-color:rgba(0,245,255,.4)}
.photo-tint .fb-modal{border-color:rgba(255,215,0,.32)}

@media (max-width:760px){
  .loadout-rail{margin-left:10px;margin-right:10px}
  .loadout-rail .btn{margin-left:0}
  .en-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}
  .dos-ends{height:96px}
  #photo-bar{gap:6px;padding:7px 10px}
  .pb-tag{display:none}
  .ob-coach{font-size:.72rem;padding:7px 12px}
}

/* ══════════════════════════════════════════════════════════════════════
   🧊 3D CHROME — the whole interface as physical objects
   ══════════════════════════════════════════════════════════════════════
   ⚠️ EVERY SELECTOR BELOW IS SCOPED TO `body.mode-3d`, and that is not a
   decoration — it is the point. The renderer the player picks on the login
   screen is a choice about the WHOLE arcade, not just the board: pick 2D
   CLASSIC and the interface is the flat neon one the rules above draw, pick
   3D CYBERPUNK and every control grows a body. A solid button over a flat
   board was the tell that the setting only reached half the app. PI3D.setMode()
   owns the class, so the switch is live the moment the card is clicked.

   Nothing here may be written unscoped. An unprefixed rule in this section is
   a control that stays extruded in 2D mode, which is exactly the bug this
   scoping exists to prevent.

   Everything below turns the flat neon UI into a set of solids. The rule
   throughout: depth is GEOMETRY, not a drop shadow. Each control is built as

     · a TOP FACE      — the element itself, lit from above by a gradient
     · a SIDE WALL     — a stack of 1px solid offsets in the object's own edge
                         colour. That is the extruded body of the solid, not a
                         blur: it is opaque, it is the same silhouette as the
                         face, and it gets SHORTER when the control is pressed,
                         which is what selling "this moved 6px toward the desk"
                         actually requires
     · a BEVEL         — a lit inner top lip and a dark inner bottom lip, so the
                         edge between face and wall catches light
     · a SPECULAR      — a fixed highlight across the upper third; it is what
                         makes a surface read as glossy plastic or brushed metal
                         rather than as coloured paper
     · a CONTACT SHADE — the only soft shadow here, and it is doing the one job
                         a shadow legitimately does: darkening the surface the
                         object is resting ON

   On top of that geometry, every control lives in a real perspective and moves
   in three axes: hover lifts it toward the viewer and tilts it, press drives it
   down into its own wall. Cards tilt TOWARD THE POINTER (wired in app.js), so
   moving the mouse across the hub visibly rotates the objects — the parallax
   between a lifted glyph and the plate behind it is the depth cue.

   perspective() is applied per-element inside transform rather than as a
   `perspective` property on the containers. A perspective container is also a
   containing block for fixed positioning and a new stacking context, and
   putting one on .screen — which scrolls — costs a repaint of the whole surface
   per frame. Per-element, each object gets its own eye point directly in front
   of it, which is also the projection that keeps a grid of cards from fanning
   outward at the edges of a wide screen. */

:root{
  /* Body colours for the extruded walls. Two tones: the upper wall catches a
     little of the top light, the lower one is in the object's own shade. */
  --w1:#161c30;
  --w2:#0a0e1a;
  --w3:#05070e;
  --lip-hi:rgba(255,255,255,.26);
  --lip-lo:rgba(0,0,0,.55);
  --spec:linear-gradient(180deg,rgba(255,255,255,.16) 0,rgba(255,255,255,.05) 42%,rgba(255,255,255,0) 62%);
  --eye:820px;                       /* viewing distance for interface solids */
  --pop-ease:cubic-bezier(.2,.9,.28,1);
}

/* Every solid gets a 3D context so its raised children — a card's icon, a
   row's rank — are laid out in the same space as its own face rather than
   flattened onto it.

   `position` is deliberately NOT set here. Several of these already position
   themselves and a blanket `position:relative` silently breaks them: #toast is
   fixed to the bottom of the viewport, #tetris-next-wrap is absolute inside
   the board frame, .fb-close is absolute in the modal's corner. Only .btn
   actually needs a positioned box — for its specular pseudo-element — and it
   is given one on its own rule below. */
body.mode-3d .btn,
body.mode-3d .auth-tab,
body.mode-3d .market-tab,
body.mode-3d .lb-tab,
body.mode-3d .mp-filter,
body.mode-3d .diff-btn,
body.mode-3d .ctrl-btn,
body.mode-3d .bb-btn,
body.mode-3d .fb-node,
body.mode-3d .mode-card,
body.mode-3d .game-card,
body.mode-3d .shop-card,
body.mode-3d .mp-mode,
body.mode-3d .card,
body.mode-3d .lb-row,
body.mode-3d .stat-pill,
body.mode-3d .hub-pts,
body.mode-3d .hub-credits,
body.mode-3d .gc-pts,
body.mode-3d .rarity-badge,
body.mode-3d .mem-card,
body.mode-3d .hack-node,
body.mode-3d .path-node,
body.mode-3d .hub-bar,
body.mode-3d .market-bar,
body.mode-3d .mp-bar,
body.mode-3d .g-hdr,
body.mode-3d .freq-slider,
body.mode-3d #g-sort-rule,
body.mode-3d .hack-readout,
body.mode-3d .path-readout,
body.mode-3d .res-bd,
body.mode-3d .mp-banner,
body.mode-3d .market-bal span,
body.mode-3d .hub-guest-tag,
body.mode-3d .up-carry{
  transform-style:preserve-3d;
}

/* ══ BUTTONS — moulded keycaps ══
   The one control the hand expects to depress, so it gets a deep body and real
   travel. translateZ on hover is a move toward the viewer: the wall
   foreshortens and the face grows, which is the difference between a button
   rising and a button merely getting brighter. */
body.mode-3d .btn{
  --wall:var(--w1);
  --wallb:var(--w3);
  position:relative;
  border-radius:10px;
  transform:perspective(var(--eye)) translateZ(0) rotateX(0deg);
  transform-origin:50% 100%;
  transition:transform .16s var(--pop-ease),box-shadow .16s var(--pop-ease),
             filter .16s ease,background-color .16s ease,border-color .16s ease;
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),
    0 4px 0 var(--wallb),0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),
    0 12px 18px -8px rgba(0,0,0,.8),
    inset 0 1px 0 var(--lip-hi),inset 0 -1px 0 var(--lip-lo);
}
body.mode-3d .btn:hover:not(:disabled){
  transform:perspective(var(--eye)) translateZ(9px) rotateX(6deg);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
    0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),
    0 8px 0 var(--wallb),0 9px 0 var(--wallb),0 10px 0 var(--wallb),
    0 20px 26px -10px rgba(0,0,0,.85),
    inset 0 1px 0 var(--lip-hi),inset 0 -1px 0 var(--lip-lo);
}
/* Pressed: the face travels DOWN by almost the whole body height and the wall
   shrinks to match. Both halves are needed — moving the face alone slides the
   object, shortening the wall alone squashes it. Together it depresses. */
body.mode-3d .btn:active:not(:disabled){
  transform:perspective(var(--eye)) translateZ(0) translateY(5px) rotateX(0deg);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wallb),
    0 4px 6px -3px rgba(0,0,0,.8),
    inset 0 2px 5px rgba(0,0,0,.55),inset 0 -1px 0 rgba(255,255,255,.08);
}
body.mode-3d .btn:disabled{transform:none;filter:saturate(.35) brightness(.8)}
/* The specular rides on a pseudo-element rather than background-image, because
   .btn-primary paints its face with a gradient of its own and a background-image
   here would replace it — which is a button with no face at all, not a button
   with a highlight. Absolutely positioned so it is out of flow: a couple of
   these buttons are flex containers, and an in-flow pseudo becomes a flex item
   and pushes their label sideways. */
body.mode-3d .btn::before{
  content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(180deg,rgba(255,255,255,.2),rgba(255,255,255,.06) 42%,rgba(255,255,255,0) 62%);
}
body.mode-3d .btn:active:not(:disabled)::before{
  background:linear-gradient(180deg,rgba(0,0,0,.28),rgba(255,255,255,0) 55%);
}

body.mode-3d .btn-primary{--wall:#0a8f9a;--wallb:#05545c;color:#01131a}
body.mode-3d .btn-secondary{--wall:#1b2237;--wallb:#0a0e1a}
body.mode-3d .btn-guest{--wall:#1c7a10;--wallb:#0c3d08}
body.mode-3d .btn-feedback{--wall:#1c7a10;--wallb:#0c3d08}
body.mode-3d .btn-sound{--wall:#0a6f7a;--wallb:#053b42}
body.mode-3d .btn-sound.snd-partial{--wall:#8a6b00;--wallb:#453600}
body.mode-3d .btn-sound.snd-off{--wall:#1b2237;--wallb:#0a0e1a}
body.mode-3d .btn-save-acct{--wall:#1c7a10;--wallb:#0c3d08}
body.mode-3d .btn-lg{border-radius:12px}
body.mode-3d .btn-sm{border-radius:8px}

/* ══ ARCADE KEYCAPS — the play-surface pad ══
   The tallest bodies in the interface, because these are hit with a thumb
   rather than clicked, and because a game pad that visibly bottoms out is the
   clearest feedback a touch control can give. */
body.mode-3d .ctrl-btn{
  --wall:#1b2338;--wallb:#070a14;
  border-radius:12px;
  background-image:linear-gradient(180deg,rgba(255,255,255,.13),rgba(255,255,255,.02) 46%,rgba(0,0,0,.22));
  transform:perspective(600px) translateZ(0);
  transform-origin:50% 100%;
  transition:transform .1s var(--pop-ease),box-shadow .1s var(--pop-ease),background-color .12s;
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
    0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),
    0 8px 0 var(--wallb),0 9px 0 var(--wallb),0 10px 0 var(--wallb),
    0 16px 20px -8px rgba(0,0,0,.9),
    inset 0 1px 0 rgba(255,255,255,.3),inset 0 -2px 0 rgba(0,0,0,.55);
}
body.mode-3d .ctrl-btn:active{
  transform:perspective(600px) translateY(8px);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wallb),
    0 3px 5px -2px rgba(0,0,0,.85),
    inset 0 3px 7px rgba(0,0,0,.6);
}
body.mode-3d .ctrl-btn.accent{--wall:#5b21a8;--wallb:#2a0e52}
body.mode-3d .ctrl-btn.accent:active{transform:perspective(600px) translateY(8px)}

/* ══ DEPLOY DECK ══ Cards standing in a rack, tinted to their unit colour. */
body.mode-3d .bb-btn{
  --wall:#141a2c;--wallb:#070a14;
  border-radius:11px;
  background-image:linear-gradient(180deg,rgba(255,255,255,.1),rgba(255,255,255,0) 55%);
  transform:perspective(560px) translateZ(0);
  transform-origin:50% 100%;
  transition:transform .14s var(--pop-ease),box-shadow .14s var(--pop-ease),opacity .2s;
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),
    0 4px 0 var(--wallb),0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),
    0 12px 16px -8px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.2),inset 0 -1px 0 rgba(0,0,0,.5);
}
body.mode-3d .bb-btn:hover:not(.broke):not(.maxed){
  transform:perspective(560px) translateZ(10px) rotateX(7deg);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
    0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),
    0 8px 0 var(--wallb),0 9px 0 var(--wallb),0 10px 0 var(--wallb),
    0 20px 26px -10px rgba(0,0,0,.9),0 0 24px -4px var(--bc),
    inset 0 1px 0 rgba(255,255,255,.24),inset 0 -1px 0 rgba(0,0,0,.5);
}
body.mode-3d .bb-btn:active{
  transform:perspective(560px) translateY(6px);
  box-shadow:0 1px 0 var(--wall),0 2px 0 var(--wallb),inset 0 3px 6px rgba(0,0,0,.6);
}
body.mode-3d .bb-btn.broke,
body.mode-3d .bb-btn.maxed{
  transform:perspective(560px) translateY(5px);
  box-shadow:0 1px 0 var(--wallb),0 2px 0 var(--wallb),inset 0 2px 6px rgba(0,0,0,.55);
}

/* ══ TABS & PILL SWITCHES ══
   Shallower bodies than a button — these read as keys on a strip, and the
   SELECTED one is the one pressed IN, which is how a mechanical tab bank shows
   its state without needing colour to carry the whole message. */
body.mode-3d .auth-tab,
body.mode-3d .market-tab,
body.mode-3d .lb-tab,
body.mode-3d .mp-filter,
body.mode-3d .diff-btn,
body.mode-3d .fb-node{
  --wall:#151b2e;--wallb:#080b16;
  border-radius:9px;
  background-image:var(--spec);
  transform:perspective(700px) translateZ(0);
  transform-origin:50% 100%;
  transition:transform .15s var(--pop-ease),box-shadow .15s var(--pop-ease),
             color .15s,border-color .15s,background-color .15s;
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wallb),0 4px 0 var(--wallb),
    0 9px 12px -6px rgba(0,0,0,.8),
    inset 0 1px 0 rgba(255,255,255,.16),inset 0 -1px 0 rgba(0,0,0,.45);
}
body.mode-3d .auth-tab:hover,
body.mode-3d .market-tab:hover,
body.mode-3d .lb-tab:hover,
body.mode-3d .mp-filter:hover,
body.mode-3d .diff-btn:hover:not(.active),
body.mode-3d .fb-node:hover{
  transform:perspective(700px) translateZ(7px) rotateX(5deg);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),
    0 4px 0 var(--wallb),0 5px 0 var(--wallb),0 6px 0 var(--wallb),
    0 16px 20px -9px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.18),inset 0 -1px 0 rgba(0,0,0,.45);
}
body.mode-3d .auth-tab.active,
body.mode-3d .market-tab.active,
body.mode-3d .lb-tab.on,
body.mode-3d .mp-filter.on,
body.mode-3d .diff-btn.active,
body.mode-3d .fb-node.lit{
  transform:perspective(700px) translateY(3px);
  box-shadow:
    0 1px 0 var(--wallb),
    inset 0 3px 7px rgba(0,0,0,.55),inset 0 -1px 0 rgba(255,255,255,.1);
}
body.mode-3d .diff-btn.stable-active,
body.mode-3d .diff-btn.diff-stable.active{--wallb:#0a4a12}
body.mode-3d .diff-btn.overclock-active,
body.mode-3d .diff-btn.diff-overclocked.active{--wallb:#4a3a05}
body.mode-3d .diff-btn.meltdown-active,
body.mode-3d .diff-btn.diff-meltdown.active{--wallb:#4a0512}
body.mode-3d .diff-btn.stable-active,
body.mode-3d .diff-btn.overclock-active,
body.mode-3d .diff-btn.meltdown-active{
  transform:perspective(700px) translateY(3px);
  box-shadow:0 1px 0 var(--wallb),inset 0 3px 7px rgba(0,0,0,.55),
             inset 0 -1px 0 rgba(255,255,255,.1);
}
body.mode-3d .diff-selector.locked .diff-btn:not(.active):hover{transform:perspective(700px) translateZ(0)}
body.mode-3d .diff-selector.locked .diff-btn:active{transform:perspective(700px) translateZ(0)}

/* ══ CARDS — plates raised off the desk ══
   The biggest surfaces in the interface, so they get the strongest modelling: a
   lit top edge, a real body, and a pointer-driven tilt wired up in app.js.
   --tx/--ty are written per-element by that code; with no pointer near them
   they are 0 and the card sits square. */
body.mode-3d .game-card,
body.mode-3d .shop-card,
body.mode-3d .card,
body.mode-3d .mp-mode,
body.mode-3d .mode-card,
body.mode-3d .convert-card{
  --wall:#141a2c;--wallb:#070a14;--tx:0deg;--ty:0deg;--tz:0px;
  /* overflow MUST stay visible. Any overflow other than visible forces
     transform-style back to flat, and a flattened card composites its raised
     icon straight onto its own face — no parallax, no standing off, nothing.
     Nothing on these cards was ever being clipped: the sparkline strip is in
     flow, and the one decoration that does need clipping (the legendary
     shimmer sweep) re-declares overflow on its own rarity class. */
  overflow:visible;
  background-image:linear-gradient(170deg,rgba(255,255,255,.07),rgba(255,255,255,0) 46%);
  transform:perspective(var(--eye)) translateZ(var(--tz)) rotateX(var(--tx)) rotateY(var(--ty));
  transform-origin:50% 60%;
  transition:transform .2s var(--pop-ease),box-shadow .2s var(--pop-ease),border-color .2s;
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
    0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),0 8px 0 var(--wallb),
    0 18px 26px -12px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.14),inset 0 -1px 0 rgba(0,0,0,.5);
}
/* Hover only supplies the LIFT. The rotation arrives as --tx/--ty from the
   pointer, and a rotate declared here would overwrite it every frame. */
/* The [data-c] selectors that carry each mission's neon are specificity 0-3-1
   and were overriding the body on hover — a card that lit up but lost its
   walls. The accent is folded into the object instead: one variable per colour,
   and the lift keeps the full wall stack with the glow added on the end. */
body.mode-3d .game-card[data-c="cyan"],
body.mode-3d .gc-pts[data-c="cyan"]{--acc:#00f5ff}
body.mode-3d .game-card[data-c="pink"],
body.mode-3d .gc-pts[data-c="pink"]{--acc:#ff0090}
body.mode-3d .game-card[data-c="lime"],
body.mode-3d .gc-pts[data-c="lime"]{--acc:#39ff14}
body.mode-3d .game-card[data-c="gold"],
body.mode-3d .gc-pts[data-c="gold"]{--acc:#ffd700}
body.mode-3d .game-card[data-c="purple"],
body.mode-3d .gc-pts[data-c="purple"]{--acc:#a855f7}
body.mode-3d .game-card[data-c="orange"],
body.mode-3d .gc-pts[data-c="orange"]{--acc:#ff6600}
body.mode-3d .game-card[data-c="red"],
body.mode-3d .gc-pts[data-c="red"]{--acc:#ff2442}
body.mode-3d .game-card:hover,
body.mode-3d .game-card[data-c]:hover,
body.mode-3d .shop-card:hover,
body.mode-3d .mp-mode:hover,
body.mode-3d .mode-card:hover{
  transform:perspective(var(--eye)) translateZ(34px) rotateX(var(--tx)) rotateY(var(--ty));
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
    0 5px 0 var(--wall),0 6px 0 var(--wallb),0 7px 0 var(--wallb),0 8px 0 var(--wallb),
    0 9px 0 var(--wallb),0 10px 0 var(--wallb),0 11px 0 var(--wallb),0 12px 0 var(--wallb),
    0 13px 0 var(--wallb),0 14px 0 var(--wallb),
    0 40px 50px -18px rgba(0,0,0,.92),
    0 0 40px -10px var(--acc,rgba(255,255,255,.35)),
    inset 0 1px 0 rgba(255,255,255,.2),inset 0 -1px 0 rgba(0,0,0,.5);
}
body.mode-3d .game-card[data-c]:hover{border-color:var(--acc)}
body.mode-3d .game-card:active,
body.mode-3d .game-card[data-c]:active,
body.mode-3d .shop-card:active,
body.mode-3d .mp-mode:active,
body.mode-3d .mode-card:active{
  transform:perspective(var(--eye)) translateZ(2px) translateY(4px);
  box-shadow:0 1px 0 var(--wall),0 2px 0 var(--wallb),0 6px 10px -5px rgba(0,0,0,.8),
             inset 0 3px 8px rgba(0,0,0,.5);
}
body.mode-3d .mode-card.on{
  transform:perspective(var(--eye)) translateZ(10px);
  box-shadow:
    0 1px 0 #0a5a63,0 2px 0 #0a5a63,0 3px 0 #063a41,0 4px 0 #063a41,0 5px 0 #063a41,
    0 20px 30px -12px rgba(0,0,0,.9),0 0 34px -6px rgba(0,245,255,.5),
    inset 0 1px 0 rgba(255,255,255,.24);
}
/* Icons stand OFF their card rather than being printed on it — the parallax
   between a lifted glyph and the plate behind it is the cheapest and most
   convincing depth cue a flat panel has. */
body.mode-3d .gc-icon,
body.mode-3d .shop-emoji,
body.mode-3d .res-emoji,
body.mode-3d .auth-icon,
body.mode-3d .mp-banner-icon,
body.mode-3d .bb-ico{
  transform:translateZ(26px);
  transform-style:preserve-3d;
}
body.mode-3d .gc-name,
body.mode-3d .gc-pts,
body.mode-3d .gc-desc,
body.mode-3d .shop-name{transform:translateZ(10px)}

/* ══ LEADERBOARD ROWS — a stack of physical plates ══ */
body.mode-3d .lb-row{
  --wall:#121829;--wallb:#080b16;
  background-image:linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,0) 60%);
  transform:perspective(760px) translateZ(0);
  transition:transform .16s var(--pop-ease),box-shadow .16s var(--pop-ease);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wallb),
    0 7px 10px -6px rgba(0,0,0,.8),
    inset 0 1px 0 rgba(255,255,255,.1);
}
body.mode-3d .lb-row:hover{
  transform:perspective(760px) translateZ(8px) translateX(3px);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wallb),
    0 5px 0 var(--wallb),0 14px 18px -8px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.14);
}
body.mode-3d .lb-row.r1{--wall:#6b5600;--wallb:#3a2f00}
body.mode-3d .lb-row.r2{--wall:#4a525c;--wallb:#252a30}
body.mode-3d .lb-row.r3{--wall:#553318;--wallb:#2b190c}
body.mode-3d .lb-row.me{--wall:#153a72;--wallb:#0a1e3d}
body.mode-3d .lb-rank{transform:translateZ(12px)}

/* ══ BARS & PANELS — the desk everything else sits on ══
   Lower and wider than the controls, with the body colour running underneath.
   These do not move; they are the ground plane, and their job is to give the
   things standing on them something to be raised ABOVE. */
body.mode-3d .hub-bar,
body.mode-3d .market-bar,
body.mode-3d .mp-bar,
body.mode-3d .g-hdr,
body.mode-3d .freq-slider,
body.mode-3d #g-sort-rule,
body.mode-3d .hack-readout,
body.mode-3d .path-readout,
body.mode-3d .res-bd,
body.mode-3d .mp-banner,
body.mode-3d #tetris-next-wrap,
body.mode-3d .up-carry{
  background-image:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,0) 70%);
  box-shadow:
    0 1px 0 #10162a,0 2px 0 #10162a,0 3px 0 #070a14,0 4px 0 #070a14,
    0 14px 22px -12px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.11),inset 0 -1px 0 rgba(0,0,0,.5);
}
body.mode-3d #bb-deck{
  border-radius:14px;padding:10px;
  background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,0));
  box-shadow:inset 0 2px 6px rgba(0,0,0,.6),inset 0 -1px 0 rgba(255,255,255,.08),
             0 1px 0 rgba(255,255,255,.05);
}

/* ══ STATUS PILLS & BADGES — small solids, shallow bodies ══ */
body.mode-3d .stat-pill,
body.mode-3d .hub-pts,
body.mode-3d .hub-credits,
body.mode-3d .gc-pts,
body.mode-3d .rarity-badge,
body.mode-3d .market-bal span,
body.mode-3d .hub-guest-tag,
body.mode-3d .shop-tag,
body.mode-3d .mp-link,
body.mode-3d .res-bonus{
  background-image:var(--spec);
  box-shadow:
    0 1px 0 rgba(255,255,255,.05),0 2px 0 #0b0f1c,0 3px 0 #070a14,
    0 6px 8px -4px rgba(0,0,0,.75),
    inset 0 1px 0 rgba(255,255,255,.18),inset 0 -1px 0 rgba(0,0,0,.45);
}

/* The specular sheen above is a background-image, and on the legendary badge
   the background-image is the word itself — painting --spec there clips a
   16%-white gradient to the glyphs and the text all but disappears. This badge
   keeps its shimmer and forgoes the sheen; it still gets the solid body and
   lit lip from the shared rule, only the face gradient differs. */
body.mode-3d .rarity-badge.legendary{background-image:var(--legend-shimmer)}

/* ══ RECESSED WELLS — the inverse solid ══
   A field you type into is a hole in the panel, not a block on top of it. Same
   modelling logic run backwards: the lit lip goes on the BOTTOM edge (light
   from above reaches the far wall of a pit), the dark lip on the top, and the
   body is an inset gradient rather than an outset stack. */
body.mode-3d .input,
body.mode-3d #math-answer,
body.mode-3d .fb-textarea,
body.mode-3d .fb-select,
body.mode-3d input[type=text],
body.mode-3d input[type=email],
body.mode-3d input[type=password],
body.mode-3d input[type=number],
body.mode-3d textarea,
body.mode-3d select{
  border-radius:10px;
  background-image:linear-gradient(180deg,rgba(0,0,0,.42),rgba(0,0,0,.1) 55%,rgba(255,255,255,.04));
  box-shadow:
    inset 0 3px 7px rgba(0,0,0,.65),inset 0 1px 0 rgba(0,0,0,.7),
    inset 0 -1px 0 rgba(255,255,255,.14),
    0 1px 0 rgba(255,255,255,.07);
}
body.mode-3d .input:focus,
body.mode-3d #math-answer:focus,
body.mode-3d .fb-textarea:focus,
body.mode-3d .fb-select:focus{
  box-shadow:
    inset 0 3px 9px rgba(0,0,0,.7),inset 0 1px 0 rgba(0,0,0,.7),
    inset 0 -1px 0 rgba(0,245,255,.5),
    0 0 0 3px rgba(0,245,255,.13),0 1px 0 rgba(255,255,255,.07);
}

/* ══ PROGRESS & METER CHANNELS ══
   A milled channel with a bar sitting IN it — the fill has its own top light
   and its own bottom shade, so it reads as a lit rod in a groove. */
body.mode-3d .prog-bar,
body.mode-3d .path-meter{
  height:8px;border-radius:5px;
  background:linear-gradient(180deg,#03040a,#0b0e1a);
  box-shadow:
    inset 0 2px 4px rgba(0,0,0,.85),inset 0 -1px 0 rgba(255,255,255,.1),
    0 1px 0 rgba(255,255,255,.06);
}
body.mode-3d .prog-fill,
body.mode-3d .path-meter-fill{
  border-radius:5px;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.55),inset 0 -2px 3px rgba(0,0,0,.4);
}

/* ══ SLIDERS — a knurled knob on a rail ══ */
body.mode-3d .freq-range{
  background:linear-gradient(180deg,#03040a,#0d1120);
  box-shadow:inset 0 2px 5px rgba(0,0,0,.85),inset 0 -1px 0 rgba(255,255,255,.12);
}
body.mode-3d .freq-range::-webkit-slider-thumb{
  background-image:linear-gradient(180deg,#e8faff 0,var(--cyan) 38%,#036c78 100%);
  box-shadow:
    0 1px 0 #0a5a63,0 2px 0 #0a5a63,0 3px 0 #063a41,0 4px 0 #063a41,
    0 8px 12px -5px rgba(0,0,0,.9),
    inset 0 1px 0 rgba(255,255,255,.85),inset 0 -2px 3px rgba(0,0,0,.4);
}
body.mode-3d .freq-range::-moz-range-thumb{
  background-image:linear-gradient(180deg,#e8faff 0,var(--cyan) 38%,#036c78 100%);
  box-shadow:0 2px 0 #0a5a63,0 4px 0 #063a41,0 8px 12px -5px rgba(0,0,0,.9),
             inset 0 1px 0 rgba(255,255,255,.85);
}
body.mode-3d .freq-range:active::-webkit-slider-thumb{
  box-shadow:0 1px 0 #063a41,0 3px 5px -3px rgba(0,0,0,.9),
             inset 0 2px 4px rgba(0,0,0,.4);
}

/* ══ 2D-MODE PLAY SURFACES ══
   Memory tiles, keypad nodes and circuit contacts are objects the player
   physically presses, so they get the same treatment as the pad. (The 3D
   missions build their own geometry; these are the classic renderer's.) */
body.mode-3d .mem-card,
body.mode-3d .hack-node,
body.mode-3d .path-node{
  transform:perspective(620px) translateZ(0);
  transform-origin:50% 100%;
  transition:transform .16s var(--pop-ease),box-shadow .16s var(--pop-ease),
             background-color .16s,border-color .16s;
  background-image:linear-gradient(180deg,rgba(255,255,255,.1),rgba(255,255,255,0) 55%);
  box-shadow:
    0 1px 0 #161d31,0 2px 0 #161d31,0 3px 0 #0a0e1a,0 4px 0 #0a0e1a,0 5px 0 #070a14,
    0 12px 16px -8px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.2),inset 0 -1px 0 rgba(0,0,0,.5);
}
body.mode-3d .mem-card:hover,
body.mode-3d .hack-node:hover,
body.mode-3d .path-node:not(.dead):hover{
  transform:perspective(620px) translateZ(10px) rotateX(6deg);
}
body.mode-3d .mem-card.flipped{
  transform:perspective(620px) translateZ(14px);
  box-shadow:
    0 1px 0 #1c6b12,0 2px 0 #1c6b12,0 3px 0 #0d3a08,0 4px 0 #0d3a08,
    0 18px 22px -10px rgba(0,0,0,.9),0 0 26px -6px rgba(57,255,20,.6),
    inset 0 1px 0 rgba(255,255,255,.3);
}
body.mode-3d .hack-node.lit,
body.mode-3d .path-node.on{
  transform:perspective(620px) translateZ(12px);
  box-shadow:
    0 1px 0 rgba(0,0,0,.6),0 2px 0 rgba(0,0,0,.6),0 3px 0 rgba(0,0,0,.7),
    0 18px 22px -10px rgba(0,0,0,.9),
    0 0 24px var(--nc,var(--pc,#39ff14)),
    inset 0 1px 0 rgba(255,255,255,.35);
}
body.mode-3d .path-node.dead{
  transform:perspective(620px) translateY(4px);
  box-shadow:0 1px 0 #2a0a12,inset 0 3px 8px rgba(0,0,0,.7);
}
body.mode-3d #click-btn{
  background-image:radial-gradient(circle at 38% 26%,rgba(255,255,255,.85),rgba(255,255,255,0) 46%),
                   linear-gradient(135deg,var(--cyan),var(--purple));
  transform:perspective(700px) translateZ(0);
  transition:transform .09s var(--pop-ease),box-shadow .09s var(--pop-ease);
  box-shadow:
    0 2px 0 #0a5a63,0 4px 0 #0a5a63,0 6px 0 #06414a,0 8px 0 #06414a,
    0 10px 0 #3a1d6b,0 12px 0 #3a1d6b,0 14px 0 #24114a,
    0 26px 34px -12px rgba(0,0,0,.9),
    inset 0 2px 0 rgba(255,255,255,.5),inset 0 -6px 12px rgba(0,0,0,.4);
}
body.mode-3d #click-btn:active{
  transform:perspective(700px) translateY(11px) scale(.985);
  box-shadow:0 2px 0 #06414a,0 3px 0 #24114a,0 6px 10px -5px rgba(0,0,0,.9),
             inset 0 5px 12px rgba(0,0,0,.5);
}

/* ══ MODALS & RESULT CARDS — brought forward off the page ══ */
body.mode-3d .fb-modal,
body.mode-3d #results-screen .res-card{
  transform:perspective(1200px) translateZ(0) rotateX(1.2deg);
  box-shadow:
    0 2px 0 #131a2e,0 4px 0 #131a2e,0 6px 0 #080c18,0 8px 0 #080c18,0 10px 0 #05070f,
    0 44px 70px -22px rgba(0,0,0,.95),
    inset 0 1px 0 rgba(255,255,255,.16),inset 0 -1px 0 rgba(0,0,0,.6);
}
body.mode-3d .fb-overlay.show .fb-modal{transform:perspective(1200px) translateZ(30px) rotateX(1.2deg)}
body.mode-3d .fb-close{
  box-shadow:0 1px 0 #171e33,0 2px 0 #0a0e1a,0 4px 6px -3px rgba(0,0,0,.8),
             inset 0 1px 0 rgba(255,255,255,.18);
  transition:transform .14s var(--pop-ease),box-shadow .14s var(--pop-ease),color .14s;
}
body.mode-3d .fb-close:hover{transform:perspective(500px) translateZ(6px) scale(1.06)}
body.mode-3d .fb-close:active{transform:translateY(2px)}
body.mode-3d #toast{
  box-shadow:
    0 2px 0 rgba(255,255,255,.06),0 3px 0 #0a0e1a,0 5px 0 #05070f,
    0 22px 34px -14px rgba(0,0,0,.9),
    inset 0 1px 0 rgba(255,255,255,.16);
}

/* ══ TITLES THAT STAND OFF THE PAGE ══
   Extruded type: a stack of solid offsets under the glyphs is the same body the
   controls have, applied to letterforms. It is the one place a text-shadow is
   doing geometry rather than decoration. */
/* ⚠️ The gradient titles take their body from filter:drop-shadow, NOT
   text-shadow. Their face is a background clipped to the glyphs under a
   transparent text fill, and Chrome paints text-shadow ABOVE a
   background-clip:text background — so the extrusion was drawn over the face
   and buried it: the logo came out a flat dull teal, and every modal title a
   near-black word with a thin lit edge. drop-shadow works on the element's
   finished rendering and puts the body behind it. Chained 1px shadows
   accumulate, the first nearest the face, so the stack reads exactly like the
   text-shadow list it replaced. Plain white type (.g-title) keeps text-shadow,
   which is correct there. */
body.mode-3d .logo{
  filter:
    drop-shadow(0 1px 0 rgba(0,90,110,.9)) drop-shadow(0 1px 0 rgba(0,70,90,.85))
    drop-shadow(0 1px 0 rgba(0,52,70,.8)) drop-shadow(0 1px 0 rgba(0,36,52,.75))
    drop-shadow(0 1px 0 rgba(0,22,36,.7)) drop-shadow(0 9px 11px rgba(0,0,0,.8));
}
body.mode-3d .g-title{
  text-shadow:0 1px 0 rgba(0,0,0,.9),0 2px 0 rgba(0,0,0,.7),0 3px 0 rgba(0,0,0,.5),
              0 8px 14px rgba(0,0,0,.85);
}
body.mode-3d .market-title,
body.mode-3d .mp-title,
body.mode-3d .fb-title{
  filter:drop-shadow(0 1px 0 rgba(0,0,0,.9)) drop-shadow(0 1px 0 rgba(0,0,0,.7))
         drop-shadow(0 1px 0 rgba(0,0,0,.5)) drop-shadow(0 5px 7px rgba(0,0,0,.85));
}
body.mode-3d #cd-num{
  text-shadow:
    0 2px 0 rgba(120,120,140,.9),0 4px 0 rgba(80,80,100,.85),0 6px 0 rgba(50,50,70,.8),
    0 8px 0 rgba(28,28,44,.75),0 10px 0 rgba(14,14,26,.7),
    0 26px 40px rgba(0,0,0,.9);
}
body.mode-3d #click-count{transform:perspective(700px) translateZ(20px)}

/* ══ SCROLLBARS — a chrome rod in a milled channel ══
   Scrollbar pseudo-elements cannot take 3D transforms, so the solidity has to
   come entirely from shading. A cylinder is a specific and recognisable thing:
   dark at both edges, a bright specular band a third of the way in, and a
   softer bounce light near the far edge. Painted across the bar's SHORT axis
   that reads as a rod you could roll under a finger; the track around it is a
   channel cut into the panel, so the rod is sitting in something. */
body.mode-3d::-webkit-scrollbar,body.mode-3d ::-webkit-scrollbar{width:14px;height:14px}
body.mode-3d::-webkit-scrollbar-track,body.mode-3d ::-webkit-scrollbar-track{
  background:linear-gradient(90deg,#05070f,#0b0f1c 45%,#05070f);
  border-radius:10px;
  box-shadow:
    inset 2px 0 4px rgba(0,0,0,.9),inset -2px 0 4px rgba(0,0,0,.9),
    inset 0 1px 0 rgba(0,0,0,.8),inset 0 -1px 0 rgba(255,255,255,.07);
}
body.mode-3d::-webkit-scrollbar-thumb,body.mode-3d ::-webkit-scrollbar-thumb{
  border:3px solid transparent;background-clip:padding-box;border-radius:10px;
  background-image:linear-gradient(90deg,
    #024a52 0%,#0798a6 22%,#7ff2ff 38%,#00f5ff 52%,#0a8390 76%,#01323a 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.55),inset 0 -1px 0 rgba(0,0,0,.6),
    0 0 10px var(--sb-glow);
}
body.mode-3d::-webkit-scrollbar-thumb:hover,body.mode-3d ::-webkit-scrollbar-thumb:hover{
  background-image:linear-gradient(90deg,
    #4a0230 0%,#c00775 22%,#ff9fd8 38%,#ff0090 52%,#a8085e 76%,#3a0125 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.6),inset 0 -1px 0 rgba(0,0,0,.6),
    0 0 16px rgba(255,0,144,.55);
}
body.mode-3d::-webkit-scrollbar-thumb:active,body.mode-3d ::-webkit-scrollbar-thumb:active{
  background-image:linear-gradient(90deg,
    #2a0640 0%,#6b21a8 22%,#d8b4fe 38%,#a855f7 52%,#5b21a8 76%,#1e0733 100%);
}
/* A horizontal bar is the same rod lying the other way, so the cylinder shading
   has to rotate with it or it reads as a flat painted strip. */
body.mode-3d::-webkit-scrollbar-thumb:horizontal,body.mode-3d ::-webkit-scrollbar-thumb:horizontal{
  background-image:linear-gradient(180deg,
    #024a52 0%,#0798a6 22%,#7ff2ff 38%,#00f5ff 52%,#0a8390 76%,#01323a 100%);
  box-shadow:inset 1px 0 0 rgba(255,255,255,.5),inset -1px 0 0 rgba(0,0,0,.6),
             0 0 10px var(--sb-glow);
}
body.mode-3d::-webkit-scrollbar-track:horizontal,body.mode-3d ::-webkit-scrollbar-track:horizontal{
  background:linear-gradient(180deg,#05070f,#0b0f1c 45%,#05070f);
  box-shadow:inset 0 2px 4px rgba(0,0,0,.9),inset 0 -2px 4px rgba(0,0,0,.9);
}
body.mode-3d .fb-modal::-webkit-scrollbar-thumb{
  background-image:linear-gradient(90deg,
    #0a3a04 0%,#22a80c 22%,#b6ffa1 38%,#39ff14 52%,#1c7a10 76%,#082806 100%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.55),inset 0 -1px 0 rgba(0,0,0,.6),
             0 0 10px rgba(57,255,20,.4);
}
body.mode-3d .fb-modal::-webkit-scrollbar-thumb:hover{
  background-image:linear-gradient(90deg,
    #0a3a04 0%,#39ff14 22%,#e6ffdd 38%,#7cffa1 52%,#1c7a10 76%,#082806 100%);
}
body.mode-3d textarea::-webkit-scrollbar,
body.mode-3d select::-webkit-scrollbar,
body.mode-3d .input::-webkit-scrollbar{width:10px;height:10px}
body.mode-3d textarea::-webkit-scrollbar-thumb,
body.mode-3d select::-webkit-scrollbar-thumb,
body.mode-3d .input::-webkit-scrollbar-thumb{border-width:2px}

/* ══ THE BOARD ITSELF ══
   The play surface is a screen set into a bezel: a raised frame with a lit
   inner lip and the panel dropping away behind it. */
body.mode-3d #arcade-canvas,
body.mode-3d #gl-canvas{
  border-radius:10px;
  box-shadow:
    0 2px 0 #10162a,0 4px 0 #10162a,0 6px 0 #070a14,0 8px 0 #070a14,
    0 26px 40px -16px rgba(0,0,0,.95),
    inset 0 1px 0 rgba(255,255,255,.14);
}
/* ⚠️ DO NOT PUT transform-style:preserve-3d BACK ON #board-frame.
   It was here, it is the canvas's direct parent, and it existed ONLY in 3D mode
   — which is the exact shape of "the 3D board is soft and the 2D one is not".
   A preserve-3d ancestor makes the element a 3D rendering context, and Blink
   stops pixel-snapping descendants of one (snapping would break coplanarity),
   so the canvas quad goes through the general-transform path with bilinear
   filtering instead of the axis-aligned fast path. #board-frame is
   width:fit-content centred in .g-area, so a fractional left edge is the normal
   case, not the exception — and half-pixel bilinear resampling of a 1:1 texture
   is exactly the uniform softness that gets reported.
   It bought nothing: every child (#tetris-next-wrap, #arcade-canvas, #gl-canvas,
   #gl-fx, #touch-hint, #mp-overlay, .gl-pop/.gl-label/.gl-stick) was checked at
   runtime and in the stylesheet, and not one uses a 3D transform. The bezel look
   is the box-shadow stack above, which is untouched by this. */

/* ══ REDUCED MOTION ══
   The geometry stays — it is what the interface IS now, and a static solid is
   not motion. Only the travel is removed. */
@media (prefers-reduced-motion:reduce){
  body.mode-3d .btn,
  body.mode-3d .ctrl-btn,
  body.mode-3d .bb-btn,
  body.mode-3d .game-card,
  body.mode-3d .shop-card,
  body.mode-3d .card,
  body.mode-3d .mp-mode,
  body.mode-3d .mode-card,
  body.mode-3d .lb-row,
  body.mode-3d .auth-tab,
  body.mode-3d .market-tab,
  body.mode-3d .lb-tab,
  body.mode-3d .mp-filter,
  body.mode-3d .diff-btn,
  body.mode-3d .fb-node,
  body.mode-3d .mem-card,
  body.mode-3d .hack-node,
  body.mode-3d .path-node,
  body.mode-3d #click-btn{transition:box-shadow .12s linear}
  body.mode-3d .game-card:hover,
  body.mode-3d .shop-card:hover,
  body.mode-3d .mp-mode:hover,
  body.mode-3d .mode-card:hover{
    transform:perspective(var(--eye)) translateZ(10px)
  }
}

/* ══ SMALL SCREENS ══
   Shallower bodies on a phone: the same object, scaled to a surface held 30cm
   from the eye rather than 60. Deep walls at this size eat the tap target. */
@media (max-width:560px){
  body.mode-3d .ctrl-btn{
    box-shadow:
      0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),
      0 4px 0 var(--wallb),0 5px 0 var(--wallb),0 6px 0 var(--wallb),
      0 12px 16px -8px rgba(0,0,0,.9),
      inset 0 1px 0 rgba(255,255,255,.3),inset 0 -2px 0 rgba(0,0,0,.55)
  }
  body.mode-3d .ctrl-btn:active{transform:perspective(600px) translateY(5px)}
  body.mode-3d .gc-icon,
  body.mode-3d .shop-emoji{transform:translateZ(16px)}
  body.mode-3d::-webkit-scrollbar,body.mode-3d ::-webkit-scrollbar{width:10px;height:10px}
}

/* ══════════════════════════════════════════════════════════════
   🌀 CHAOS PROTOCOL — the opt-in dial
   Sits directly under the stability selector and borrows its frame, because
   the two are read together: one sets how hard the round is, the other sets
   whether the round is a gamble. Deliberately NOT the same colour — stability
   is a dial you turn, this is a switch you throw.
   ══════════════════════════════════════════════════════════════ */
.chaos-selector{
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  /* #hub-screen is a flex COLUMN, so this is a flex item and shrinks by default
     — which squashed a 26px toggle into a 19px strip on any hub tall enough to
     overflow. .diff-selector above it never hit this because it is taller than
     its content either way. */
  flex:none;width:100%;
  max-width:1060px;margin:8px auto 20px;padding:9px 16px;
  border:1px solid var(--border);border-radius:var(--r);
  background:var(--s1);position:relative;overflow:hidden;
  transition:border-color .25s ease,box-shadow .25s ease,background .25s ease;
}
.chaos-selector.armed{
  border-color:rgba(168,85,247,.5);
  background:linear-gradient(100deg,rgba(168,85,247,.12),rgba(255,36,66,.07));
  box-shadow:0 0 24px rgba(168,85,247,.18);
}
/* A slow sweep across the strip while it is armed — the one piece of motion
   in the hub that says "something is running". Held to a 6s cycle so it reads
   as a system light rather than as an animation demanding attention. */
.chaos-selector.armed::after{
  content:'';position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(100deg,transparent 30%,rgba(168,85,247,.18) 50%,transparent 70%);
  background-size:220% 100%;animation:chaosSweep 6s linear infinite;
}
@keyframes chaosSweep{from{background-position:120% 0}to{background-position:-120% 0}}
html.reduced-motion .chaos-selector.armed::after{animation:none;opacity:.35}

.chaos-toggle{
  display:inline-flex;align-items:center;gap:9px;
  padding:5px 13px 5px 6px;border-radius:20px;cursor:pointer;
  border:1px solid var(--border2);background:var(--s2);
  font-family:var(--fd);font-size:.5rem;font-weight:900;letter-spacing:.16em;
  color:var(--dim);transition:all .2s ease;position:relative;z-index:1;
}
.chaos-toggle:hover{border-color:var(--purple);color:var(--text)}
.chaos-toggle .chaos-knob{
  width:15px;height:15px;border-radius:50%;
  background:var(--dimmer);transition:all .22s ease;flex:none;
}
.chaos-toggle.on{border-color:var(--purple);color:var(--purple);background:rgba(168,85,247,.15)}
.chaos-toggle.on .chaos-knob{background:var(--purple);box-shadow:0 0 12px var(--purple)}
.chaos-note{font-size:.72rem;color:var(--dim);position:relative;z-index:1;flex:1;min-width:170px}
.chaos-selector.armed .chaos-note{color:rgba(255,255,255,.72)}

/* A boon has to be readable as GOOD from the first glance, or the banner reads
   as bad news and the round gets played defensively for nothing. */
#chaos-banner.chaos-boon{
  border-color:rgba(57,255,20,.55);
  background:linear-gradient(100deg,rgba(57,255,20,.14),rgba(0,245,255,.1));
  box-shadow:0 0 26px rgba(57,255,20,.22);
}
#chaos-banner.chaos-boon .chaos-name{color:var(--lime)}
#chaos-banner.chaos-boon .chaos-icon{filter:drop-shadow(0 0 10px var(--lime))}
/* 🎲 Signal Filter's reroll. Lives on the banner and disappears with it. */
.chaos-reroll{
  flex:none;margin-left:auto;cursor:pointer;
  padding:6px 12px;border-radius:18px;
  border:1px solid var(--border2);background:var(--s2);color:var(--text);
  font-family:var(--fd);font-size:.5rem;font-weight:900;letter-spacing:.14em;
  transition:all .18s ease;
}
.chaos-reroll:hover{border-color:var(--gold);color:var(--gold);transform:translateY(-1px)}
#chaos-banner.chip .chaos-reroll{display:none}

/* 📶 SIGNAL DEGRADATION. A corrupted FEED, not a corrupted round — the board
   underneath is pristine and the mission never learns this happened. Two
   overlaid effects: a chromatic smear on the pixels, and a tear that walks
   down the picture. */
.g-area.sig-degraded{
  /* Scoped to the modifier, never blanket: .g-area is an ancestor of every
     mission's play surface, and giving it a permanent containing block would
     silently repoint any absolutely positioned overlay that currently resolves
     further up the tree. It only needs to be positioned while the scanline
     overlay below exists. */
  position:relative;
  filter:blur(.7px) contrast(1.22) saturate(.72);
  animation:sigJitter 2.4s steps(1,end) infinite;
}
.g-area.sig-degraded::after{
  content:'';position:absolute;inset:0;pointer-events:none;z-index:5;
  background:repeating-linear-gradient(0deg,rgba(0,0,0,.32) 0 2px,transparent 2px 4px);
  mix-blend-mode:multiply;
}
@keyframes sigJitter{
  0%,74%{transform:none;filter:blur(.7px) contrast(1.22) saturate(.72)}
  76%{transform:translateX(-3px);filter:blur(1.4px) contrast(1.5) saturate(.4) hue-rotate(8deg)}
  78%{transform:translateX(4px)}
  80%,100%{transform:none}
}
html.reduced-motion .g-area.sig-degraded{animation:none}

/* 📉 PACKET LOSS. A whisper, never an alert: the modifier's whole character is
   that a dropped input is indistinguishable from your own mistake until the
   pattern adds up, and a big red "DROPPED" would make it a fair, readable
   handicap instead. */
.g-area.pkt-drop{animation:pktDrop .16s ease-out}
@keyframes pktDrop{0%{filter:brightness(1.16)}100%{filter:none}}
html.reduced-motion .g-area.pkt-drop{animation:none}

/* 🌀 AXIS DRIFT rotates #board-frame from script (boardPos() untilts the
   finger to match). The transition only has to smooth the 60ms steps the
   driver writes — anything longer and the aim correction, which reads the
   angle instantly, leads the picture. */
#board-frame{transition:transform .07s linear}

/* ⚙️ OVERCLOCK CHIP — a standing tint for the rest of the round, so the board
   itself says the points are running hot. */
/* ⚠️ NOT `filter:` ON .g-area — that is an ancestor of the GL canvas, and any
   non-none filter pulls the whole subtree into an offscreen filter surface and
   resamples it on the way back. Spending a full render surface over a WebGL
   board to buy a 12% saturation bump is a bad trade. The tint is painted by an
   overlay that sits on top of the board instead, so the canvas is never inside
   a filtered subtree. */
.g-area.overclocked-run{position:relative}
.g-area.overclocked-run::after{
  content:'';position:absolute;inset:0;pointer-events:none;z-index:5;
  background:radial-gradient(ellipse at 50% 55%,rgba(255,145,0,.07),rgba(255,60,0,.10) 70%);
  mix-blend-mode:screen;
}

/* ══════════════════════════════════════════════════════════════
   👻 PACE GHOST
   How far ahead of your own best run you are right now. Lives in the stat
   strip beside the clock and the score, because it is the third number that
   describes the round in progress.
   ══════════════════════════════════════════════════════════════ */
.ghost-pace{
  display:flex;align-items:center;gap:7px;white-space:nowrap;
  background:var(--s2);border:1px solid var(--border);border-radius:20px;
  padding:5px 11px;font-family:var(--fd);font-size:.6rem;font-weight:700;
  /* Only the BORDER and the shadow may animate. Width must not: see below. */
  transition:border-color .25s ease,box-shadow .25s ease;
  /* Equal-width digits, same reason as .stat-pill — and here it matters far
     more, because this readout is repainted on EVERY frame. */
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  flex-shrink:0;
}
.ghost-pace .gp-ico{font-size:.8rem;line-height:1;flex-shrink:0}
/* ⚠️ THE FIXED DELTA SLOT — do not remove the min-width.
   .g-stats is a RIGHT-ALIGNED flex row, so anything that changes width in it
   drags everything to its left along with it. paintPace() rewrites this span
   from a requestAnimationFrame tick, and the delta is the one number in the
   header that moves continuously — so an auto-width span here re-flowed the
   whole row up to sixty times a second. Measured on the shipped build: the
   pill swung 124.6px → 162px between "+0" and "-99,999", sliding the clock and
   the score pills 37px left and back while the player was trying to read them.
   That is the "the pace ghost bar keeps moving" report, and on a phone it was
   also what tipped .g-stats into wrapping mid-round.
   The slot is sized for the widest thing the span can ever hold: an arrow, a
   sign and Cyber Arena's five-digit score with its separator — "▼ − 99,999".
   The score is right-aligned inside it so the digits stay put as they grow. */
.ghost-pace .gp-delta{
  letter-spacing:.06em;
  min-width:9ch;text-align:right;flex-shrink:0;
}
/* No reserved slot needed: the target is the personal best being raced, which
   is fixed for the whole round — it is written once by beginPace() and never
   changes again until the next round. */
.ghost-pace .gp-target{font-size:.5rem;color:var(--dimmer);letter-spacing:.1em;flex-shrink:0}
.ghost-pace.ahead{border-color:rgba(57,255,20,.5);box-shadow:0 0 14px rgba(57,255,20,.16)}
.ghost-pace.ahead .gp-delta{color:var(--lime)}
.ghost-pace.behind{border-color:rgba(255,36,66,.45)}
.ghost-pace.behind .gp-delta{color:var(--red)}

/* ══════════════════════════════════════════════════════════════
   🌳 PERK TREE — the tiers
   The tree is drawn as rows because the one thing that matters about twelve
   perks is that half of them cannot be bought yet, and a flat grid hides
   exactly that.
   ══════════════════════════════════════════════════════════════ */
.perk-tint .fb-modal{max-width:980px}
.perk-tree{display:block}
.perk-row{margin-bottom:20px}
.perk-row:last-child{margin-bottom:0}
.perk-row-nodes{display:grid;grid-template-columns:repeat(auto-fit,minmax(215px,1fr));gap:12px}
.perk-tier-hdr{
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  margin-bottom:9px;padding-bottom:6px;border-bottom:1px solid var(--border);
}
.perk-tier-name{font-family:var(--fd);font-size:.62rem;font-weight:900;letter-spacing:.18em;color:var(--lime)}
.perk-tier-cost{
  font-family:var(--fd);font-size:.44rem;letter-spacing:.14em;color:var(--gold);
  border:1px solid rgba(255,215,0,.35);border-radius:12px;padding:2px 8px;
}
.perk-tier-note{font-size:.7rem;color:var(--dim);flex:1;min-width:180px}
/* BARRED is not LOCKED. "You have the points but not the parent" and "you have
   the parent but not the points" are different problems, and a player has to
   be able to tell at a glance which one they are looking at. */
.perk-node.barred{opacity:.42;border-style:dashed}
.perk-node.barred .perk-ico{filter:grayscale(1)}
.perk-needs{
  font-family:var(--fd);font-size:.46rem;letter-spacing:.12em;
  color:var(--red);border-top:1px solid var(--border);padding-top:6px;margin-top:2px;
}
.perk-needs.met{color:var(--lime)}
.perk-kids{font-size:.66rem;color:var(--dimmer);line-height:1.45}
.perk-node .perk-desc{margin-bottom:2px}

/* 💱 Data Broker repoints the headline conversion rate, so the number says so. */
.convert-rate.perked{color:var(--lime);text-shadow:0 0 12px rgba(57,255,20,.4)}

/* ══════════════════════════════════════════════════════════════
   💀 EXIT STATES II — datamosh, core purge, tape rewind
   Same stage as the first three: fixed, above every screen, below the toast.
   All three are pure CSS over a body the script fills with a handful of
   elements, so nothing here needs a canvas or an image.
   ══════════════════════════════════════════════════════════════ */

/* 🟪 DATAMOSH. A picture arriving in the wrong ORDER — each band carries its
   own offset and its own delay, so the eye keeps trying to reassemble a stream
   that never settles. The RGB layer underneath supplies the channel shred. */
#exit-fx.fx-glitch{background:rgba(4,4,14,.82);animation:glFade 1.2s ease forwards}
@keyframes glFade{0%{opacity:0}8%{opacity:1}82%{opacity:1}100%{opacity:0}}
.gl-band{
  position:absolute;left:0;width:100%;
  background:linear-gradient(90deg,rgba(0,245,255,.22),rgba(255,0,144,.3),rgba(168,85,247,.2));
  transform:translateX(var(--gx));
  animation:glShift .34s steps(3,end) infinite;
  mix-blend-mode:screen;
}
@keyframes glShift{
  0%{transform:translateX(var(--gx))}
  40%{transform:translateX(calc(var(--gx) * -0.6)) scaleY(1.06)}
  70%{transform:translateX(calc(var(--gx) * 1.4))}
  100%{transform:translateX(var(--gx))}
}
.gl-rgb{
  position:absolute;inset:0;
  background:
    repeating-linear-gradient(90deg,rgba(255,0,0,.12) 0 2px,transparent 2px 4px),
    repeating-linear-gradient(90deg,transparent 0 2px,rgba(0,255,255,.12) 2px 4px);
  animation:glRgb .16s steps(2,end) infinite;
}
@keyframes glRgb{0%{transform:translateX(-3px)}50%{transform:translateX(4px)}100%{transform:translateX(-2px)}}

/* ⬜ CORE PURGE. A CRT switching off, run backwards and then forwards: the
   picture collapses to a single line, holds just long enough to register as a
   deliberate pause, then blows out. The hold is what makes it land. */
#exit-fx.fx-purge{background:#000}
.pg-line{
  position:absolute;left:0;right:0;top:50%;height:3px;
  background:#fff;box-shadow:0 0 40px 10px rgba(255,255,255,.85);
  transform:scaleX(0);transform-origin:50% 50%;
  animation:pgLine 1.3s cubic-bezier(.7,0,.3,1) forwards;
}
@keyframes pgLine{
  0%{transform:scaleX(0) scaleY(60);opacity:0}
  14%{transform:scaleX(1) scaleY(60);opacity:1}
  34%{transform:scaleX(1) scaleY(1)}
  62%{transform:scaleX(1) scaleY(1)}
  78%{transform:scaleX(1.02) scaleY(1);box-shadow:0 0 90px 26px rgba(255,255,255,1)}
  100%{transform:scaleX(0) scaleY(1);opacity:0}
}
.pg-flash{position:absolute;inset:0;background:#fff;opacity:0;animation:pgFlash 1.3s ease forwards}
@keyframes pgFlash{0%,72%{opacity:0}80%{opacity:.9}100%{opacity:0}}

/* 📼 TAPE REWIND. The tracking bar IS the effect: a band of noise walking UP
   the picture is what a tape looks like running backwards, and it reads as
   that instantly with no frame of video anywhere. */
#exit-fx.fx-rewind{background:rgba(2,3,10,.9)}
.rw-noise{
  position:absolute;inset:0;opacity:.5;
  background:repeating-linear-gradient(0deg,rgba(255,255,255,.07) 0 1px,transparent 1px 3px);
  animation:rwNoise .1s steps(2,end) infinite;
}
@keyframes rwNoise{0%{transform:translateY(0)}100%{transform:translateY(-3px)}}
.rw-track{
  position:absolute;left:0;right:0;height:16%;
  background:linear-gradient(180deg,transparent,rgba(255,255,255,.32),rgba(0,245,255,.22),transparent);
  filter:blur(1px);
  animation:rwTrack .62s linear infinite;
}
@keyframes rwTrack{from{top:104%}to{top:-20%}}
.rw-lbl{
  position:absolute;top:8%;left:50%;transform:translateX(-50%);
  font-family:var(--fd);font-size:.8rem;font-weight:900;letter-spacing:.3em;
  color:#fff;text-shadow:0 0 18px rgba(0,245,255,.9);
  animation:rwBlink .5s steps(1,end) infinite;
}
@keyframes rwBlink{0%,60%{opacity:1}61%,100%{opacity:.25}}

/* Every exit state is a flourish and nothing else, so reduced motion simply
   holds the first frame rather than trying to find a gentler version of a
   deliberate visual assault. playExitFx() already skips them entirely there —
   this is the belt to that braces. */
html.reduced-motion #exit-fx *{animation:none!important}

/* ══════════════════════════════════════════════════════════════
   🎖️ SEASON TRACK
   Ten tiers, drawn as a rail with a fill behind it so the whole week reads as
   one distance rather than as ten separate goals.
   ══════════════════════════════════════════════════════════════ */
.season-tint .fb-modal{max-width:920px;border-color:rgba(255,215,0,.45)}
.season-bar{
  height:8px;border-radius:6px;background:var(--s2);
  border:1px solid var(--border);overflow:hidden;margin-bottom:16px;
}
.season-bar-fill{
  height:100%;border-radius:6px;
  background:linear-gradient(90deg,var(--cyan),var(--gold));
  box-shadow:0 0 18px rgba(255,215,0,.5);transition:width .5s ease;
}
.season-tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,1fr));gap:10px}
.season-tier{
  display:flex;flex-direction:column;gap:5px;align-items:center;text-align:center;
  padding:12px 8px;border:1px solid var(--border);border-radius:var(--r);
  background:var(--s1);transition:border-color .2s ease,box-shadow .2s ease;
}
/* Three states, and they have to be distinguishable at a glance: a tier you
   have been paid for, one being paid right now, and one you are working
   toward. Anything less and the track is ten identical boxes. */
.season-tier.paid{border-color:var(--gold);background:rgba(255,215,0,.09);box-shadow:0 0 18px rgba(255,215,0,.14)}
.season-tier.ready{border-color:var(--lime);background:rgba(57,255,20,.08)}
.season-tier.locked{opacity:.6}
.st-num{
  font-family:var(--fd);font-size:.52rem;font-weight:900;letter-spacing:.14em;
  color:var(--dimmer);
}
.season-tier.paid .st-num{color:var(--gold)}
.st-at{font-family:var(--fd);font-size:.68rem;font-weight:900}
.st-reward{font-size:.72rem;color:var(--lime)}
.st-meter{width:100%;height:4px;border-radius:3px;background:var(--s2);overflow:hidden}
.st-meter span{display:block;height:100%;background:var(--cyan);border-radius:3px}
.season-tier.paid .st-meter span{background:var(--gold)}
.st-state{font-family:var(--fd);font-size:.44rem;letter-spacing:.12em;color:var(--dim)}
.season-tier.paid .st-state{color:var(--gold)}

/* ══════════════════════════════════════════════════════════════
   📁 OPERATIVE DOSSIER
   Read-only, so it is allowed to be dense — nothing here is a control the
   player has to find among the numbers.
   ══════════════════════════════════════════════════════════════ */
.dossier-tint .fb-modal{max-width:1000px;border-color:rgba(0,245,255,.4)}
.dos-sec{
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  font-family:var(--fd);font-size:.56rem;font-weight:900;letter-spacing:.18em;
  color:var(--cyan);margin:22px 0 10px;padding-bottom:6px;
  border-bottom:1px solid var(--border);
}
.dos-sec:first-child{margin-top:4px}
.dos-sec em{font-family:var(--fb);font-size:.74rem;font-style:normal;letter-spacing:0;color:var(--dim)}
.dos-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(128px,1fr));gap:10px}
.dos-tile{
  display:flex;flex-direction:column;gap:2px;
  padding:12px;border:1px solid var(--border);border-radius:var(--r);background:var(--s1);
}
.dos-ico{font-size:1.1rem;line-height:1}
.dos-val{font-family:var(--fd);font-size:1rem;font-weight:900;line-height:1.2}
.dos-lbl{font-size:.72rem;color:var(--dim)}
.dos-note{font-size:.66rem;color:var(--dimmer)}

/* The heatmap. Columns are weeks, rows are weekdays — the shape everybody
   already reads as a calendar, so it needs no legend beyond quiet→busy. */
.dos-heat{display:flex;gap:3px;overflow-x:auto;padding-bottom:4px}
.dos-heat-col{display:flex;flex-direction:column;gap:3px;flex:none}
.dos-cell{
  width:12px;height:12px;border-radius:3px;display:block;flex:none;
  background:var(--s2);border:1px solid transparent;
}
.dos-cell.l1{background:rgba(0,245,255,.28)}
.dos-cell.l2{background:rgba(0,245,255,.5)}
.dos-cell.l3{background:rgba(0,245,255,.72)}
.dos-cell.l4{background:var(--cyan);box-shadow:0 0 8px rgba(0,245,255,.6)}
/* A day that has not happened yet must not read as a quiet day. */
.dos-cell.fut{opacity:.25}
.dos-cell.today{border-color:var(--gold)}
.dos-heat-key{
  display:flex;align-items:center;gap:4px;margin-top:8px;
  font-size:.66rem;color:var(--dimmer);
}

.dos-missions{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px}
.dos-mission{
  display:flex;flex-direction:column;gap:5px;
  padding:12px;border:1px solid var(--border);border-radius:var(--r);background:var(--s1);
}
.dm-ico{font-size:1.2rem;line-height:1}
.dm-name{font-family:var(--fd);font-size:.58rem;font-weight:900;letter-spacing:.08em}
.dm-stats{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
.dm-best{font-family:var(--fd);font-size:.95rem;font-weight:900;color:var(--cyan)}
.dm-cap{font-size:.66rem;color:var(--dim)}
.dm-bar{height:5px;border-radius:4px;background:var(--s2);overflow:hidden}
.dm-bar span{display:block;height:100%;border-radius:4px;background:linear-gradient(90deg,var(--cyan),var(--lime))}
.dm-bar-none{background:repeating-linear-gradient(90deg,var(--s2) 0 4px,transparent 4px 8px)}
.dm-foot{display:flex;justify-content:space-between;gap:8px;font-size:.66rem;color:var(--dimmer)}
/* Bars, not a polyline: at twelve points a line is mostly interpolation and a
   bar is a fact. */
.dm-spark{display:flex;align-items:flex-end;gap:2px;height:24px}
.dm-spark span{flex:1;min-width:2px;background:rgba(0,245,255,.45);border-radius:2px 2px 0 0}
.dm-spark span:last-child{background:var(--cyan)}
.dos-missing{display:flex;flex-wrap:wrap;gap:8px}
.dos-chip{
  cursor:pointer;padding:6px 11px;border-radius:16px;
  border:1px dashed var(--border2);background:var(--s1);
  font-size:.72rem;color:var(--dim);transition:all .18s ease;
}
.dos-chip:hover{border-style:solid;border-color:var(--cyan);color:var(--text);transform:translateY(-1px)}
.dos-empty{font-size:.76rem;color:var(--dim);padding:14px;text-align:center}

/* ══════════════════════════════════════════════════════════════
   🔒 CLEARANCE LEVELS
   A locked card stays legible — you are meant to want it. It is dimmed and
   un-clickable, not hidden or blanked.
   ══════════════════════════════════════════════════════════════ */
.mission-clearance{
  font-family:var(--fb);font-size:.72rem;font-style:normal;letter-spacing:0;
  color:var(--dim);margin-left:8px;
}
.mission-clearance.all-clear{color:var(--lime)}
.game-card.locked{
  opacity:.42;cursor:not-allowed;filter:grayscale(.7);
  border-style:dashed;
}
.game-card.locked:hover{transform:none;box-shadow:none}
.gc-lock{
  position:absolute;inset:auto 10px 10px 10px;
  display:flex;align-items:center;gap:7px;justify-content:center;
  padding:6px 8px;border-radius:10px;
  background:rgba(4,4,14,.82);border:1px solid var(--border2);
  backdrop-filter:blur(2px);
}
.gcl-ico{font-size:.85rem;line-height:1}
.gcl-txt{
  display:flex;flex-direction:column;line-height:1.25;
  font-family:var(--fd);font-size:.46rem;font-weight:900;letter-spacing:.14em;
  color:var(--text);
}
.gcl-txt em{font-family:var(--fb);font-size:.62rem;font-style:normal;letter-spacing:0;color:var(--dim)}
/* The flash a dossier chip leaves on the card it scrolled you to. */
.game-card.gc-flash{animation:gcFlash 1.6s ease}
@keyframes gcFlash{
  0%,100%{box-shadow:none}
  15%,55%{box-shadow:0 0 0 2px var(--cyan),0 0 28px rgba(0,245,255,.45)}
}
html.reduced-motion .game-card.gc-flash{animation:none;box-shadow:0 0 0 2px var(--cyan)}

/* ══════════════════════════════════════════════════════════════
   ⚔️ RIVAL CHALLENGES
   Lives in the hub's right column with the two leaderboards, because it is the
   same kind of thing: other people's numbers.
   ══════════════════════════════════════════════════════════════ */
.chal-panel{display:flex;flex-direction:column;gap:8px}
.chal-mine{margin-bottom:4px}
.chal-mine-lbl{
  font-family:var(--fd);font-size:.44rem;font-weight:900;letter-spacing:.16em;
  color:var(--gold);margin-bottom:6px;
}
.chal-row{
  display:flex;align-items:center;gap:10px;
  padding:9px 11px;border:1px solid var(--border);border-radius:var(--r);
  background:var(--s1);transition:border-color .2s ease;
}
.chal-row:hover{border-color:var(--border2)}
.chal-row.own{border-color:rgba(255,215,0,.45);background:rgba(255,215,0,.07)}
.chal-row.locked{opacity:.5}
.chal-ico{font-size:1.15rem;line-height:1;flex:none}
.chal-txt{min-width:0;flex:1}
.chal-name{font-family:var(--fd);font-size:.52rem;font-weight:900;letter-spacing:.1em}
.chal-meta{font-size:.7rem;color:var(--dim);margin-top:2px}
.chal-meta strong{color:var(--cyan)}
.chal-meta em{color:var(--lime);font-style:normal}
.chal-locked{
  flex:none;font-family:var(--fd);font-size:.44rem;letter-spacing:.12em;color:var(--dim);
}
.chal-take{flex:none}
/* The pace HUD says whose curve it is racing, and a rival is not your ghost. */
.ghost-pace.vs-rival{border-color:rgba(255,215,0,.55);box-shadow:0 0 14px rgba(255,215,0,.18)}
.ghost-pace.vs-rival .gp-target{color:var(--gold)}

/* ══════════════════════════════════════════════════════════════
   🩸 BOSS RUSH CHAIN INTEGRITY
   A pill in the stat strip, because it is a number that describes the round in
   progress — the same category as the clock and the score.
   ══════════════════════════════════════════════════════════════ */
.br-int-pill{display:flex;align-items:center;gap:6px}
.br-int-pill em{font-style:normal;color:var(--lime)}
.br-int-pill.mid em{color:var(--gold)}
.br-int-pill.low em{color:var(--red)}
.br-int-bar{
  display:block;width:38px;height:4px;border-radius:3px;
  background:var(--s2);overflow:hidden;
}
.br-int-bar span{display:block;height:100%;border-radius:3px;background:var(--lime);transition:width .4s ease}
.br-int-pill.mid .br-int-bar span{background:var(--gold)}
.br-int-pill.low .br-int-bar span{background:var(--red)}
.br-int-pill.low{border-color:rgba(255,36,66,.6);box-shadow:0 0 14px rgba(255,36,66,.25)}


/* ══════════════════════════════════════════════════════════════
   🧊 § 10 IN THE SOLID INTERFACE
   ══════════════════════════════════════════════════════════════
   ⚠️ EVERY SELECTOR HERE IS SCOPED TO `body.mode-3d`, like everything else in
   this section. An unscoped rule added down here is a control that stays
   extruded in 2D CLASSIC — the exact bug the scoping exists to prevent.

   The new controls reuse the shells that were already modelled (.btn, .lb-row,
   .mp-banner, .fb-modal), so most of them are already solid. These are the
   three that are genuinely new geometry: the loadout rail (a ground plane, like
   the other bars), its chips (small keycaps), and the photo-mode bar (a raised
   console floating over the board). The HOW face has to keep preserve-3d or the
   flip collapses to a cross-fade. */
body.mode-3d .loadout-rail,
body.mode-3d #photo-bar{
  background-image:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,0) 70%);
  box-shadow:
    0 1px 0 #10162a,0 2px 0 #10162a,0 3px 0 #070a14,0 4px 0 #070a14,
    0 14px 22px -12px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.11),inset 0 -1px 0 rgba(0,0,0,.5);
}
body.mode-3d .loadout-rail,
body.mode-3d .en-card,
body.mode-3d .lo-card{ transform-style:preserve-3d; }

/* Small keycaps: a short wall and a lit top lip, the same vocabulary .btn uses,
   at chip scale. */
body.mode-3d .lo-chip,
body.mode-3d .gc-howbtn{
  box-shadow:
    0 1px 0 #131a2e,0 2px 0 #0a0e1c,
    0 6px 10px -6px rgba(0,0,0,.8),
    inset 0 1px 0 rgba(255,255,255,.14),inset 0 -1px 0 rgba(0,0,0,.45);
  transition:transform .12s ease,box-shadow .12s ease,border-color .15s,color .15s;
}
body.mode-3d .lo-chip:hover{ transform:translateY(-1px); }
body.mode-3d .lo-chip:active{
  transform:translateY(2px);
  box-shadow:0 0 0 #131a2e,inset 0 2px 5px rgba(0,0,0,.6);
}

/* The badge needs its OWN transforms in this mode, split out from .lo-chip
   above, because every one of them has to keep the translateZ. The card is
   preserve-3d and its contents are lifted off the face — the icon by 26px, the
   text by 10px — so a badge at Z 0 is behind all of them, both visually and for
   hit-testing. 34px puts it in front of the tallest of them. */
body.mode-3d .gc-howbtn{ transform:translateZ(34px); }
body.mode-3d .gc-howbtn:hover{ transform:translateZ(34px) translateY(-1px); }
body.mode-3d .gc-howbtn:active{
  transform:translateZ(34px) translateY(2px);
  box-shadow:0 0 0 #131a2e,inset 0 2px 5px rgba(0,0,0,.6);
}

/* The briefing modal is a slab standing off the backdrop, like every other
   overlay in this mode — it replaced a flip FACE on the card, which is why the
   preserve-3d above no longer names it. */
body.mode-3d .brief-modal{
  background-image:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,0) 60%);
  box-shadow:0 0 70px rgba(0,245,255,.13),
             inset 0 1px 0 rgba(255,255,255,.10),inset 0 -2px 6px rgba(0,0,0,.6),
             0 22px 44px -18px rgba(0,0,0,.92);
}
body.mode-3d .brf-ctrl{
  box-shadow:inset 0 1px 0 rgba(255,255,255,.07),inset 0 -1px 0 rgba(0,0,0,.4);
}

/* The endless cards and loadout cards are objects on a shelf, like .shop-card. */
body.mode-3d .en-card,
body.mode-3d .lo-card{
  background-image:linear-gradient(180deg,rgba(255,255,255,.045),rgba(255,255,255,0) 62%);
  box-shadow:
    0 1px 0 #10162a,0 2px 0 #070a14,0 3px 0 #070a14,
    0 16px 26px -14px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.09);
}
body.mode-3d .en-card:hover:not(.locked){ transform:translateY(-3px); }

/* The coach strip is a lit pill standing off the board, not a label on it. */
body.mode-3d .ob-coach{
  box-shadow:0 6px 24px rgba(0,245,255,.35),
             0 2px 0 rgba(0,0,0,.5),
             inset 0 1px 0 rgba(255,255,255,.4);
}

/* ══════════════════════════════════════════════════════════════
   ⚙️ SETTINGS — the gear on the hub bar, and the panel it opens
   ══════════════════════════════════════════════════════════════
   Purple, because the bar's other doors already own cyan (audio) and gold
   (points), and purple is the other half of the arcade's primary gradient —
   so the gear reads as part of the cabinet, not as a browser widget. */
.btn-settings{
  position:relative;display:inline-flex;align-items:center;justify-content:center;
  padding:7px 11px;line-height:1;
  color:var(--purple);border-color:rgba(168,85,247,.42)
}
.btn-settings:hover{background:rgba(168,85,247,.14);border-color:var(--purple);box-shadow:0 0 18px rgba(168,85,247,.28)}
.btn-settings:focus-visible{outline:2px solid var(--purple);outline-offset:2px}
.gear-ico{
  display:block;width:14px;height:14px;fill:currentColor;
  filter:drop-shadow(0 0 3px rgba(168,85,247,.8));
  transition:transform .6s cubic-bezier(.2,.9,.3,1)
}
/* Eight teeth look identical every 45°, so a quarter turn lands on the same
   silhouette: the gear spins and settles, with no pose to snap back from when
   the pointer leaves. */
.btn-settings:hover .gear-ico,
.btn-settings:focus-visible .gear-ico{transform:rotate(90deg)}
/* The nudge the pulsing Save Account button used to give from the bar itself:
   a guest's whole profile is one Exit away from deletion. */
.gear-dot{
  display:none;position:absolute;top:-4px;right:-4px;width:10px;height:10px;border-radius:50%;
  background:var(--lime);box-shadow:0 0 0 2px var(--bg),0 0 10px var(--lime);
  pointer-events:none;animation:gearPing 2.4s ease-in-out infinite
}
.btn-settings.needs-save .gear-dot{display:block}
@keyframes gearPing{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(.7);opacity:.6}}
html.reduced-motion .gear-dot{animation:none}
html.reduced-motion .gear-ico{transition:none}
@media (prefers-reduced-motion:reduce){
  .gear-dot{animation:none}
  .gear-ico{transition:none}
}
@media (pointer:coarse){ .btn-settings{min-width:44px} }

/* Under the login screen's picker: the choice is not final. */
.mode-hint{margin:-6px 0 16px;font-size:.72rem;line-height:1.4;color:var(--dimmer);text-align:center}

/* ── The panel: the .fb-overlay shell, tinted purple like the gear ── */
.set-tint .fb-modal{max-width:560px;border-color:rgba(168,85,247,.45);box-shadow:0 0 70px rgba(168,85,247,.14),inset 0 0 0 1px rgba(255,255,255,.03)}
.set-tint .fb-modal::before{background:linear-gradient(90deg,transparent,var(--purple),transparent);box-shadow:0 0 12px var(--purple)}
.set-tint .fb-title{background:linear-gradient(90deg,var(--purple),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.set-tint .fb-head{margin-bottom:20px}

.set-sec{padding:14px 16px 16px;border:1px solid var(--border);border-radius:var(--r);background:var(--s1)}
.set-sec + .set-sec{margin-top:12px}
.set-sec-h{
  display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px;
  font-family:var(--fd);font-size:.58rem;font-weight:700;letter-spacing:.22em;color:var(--cyan)
}
.set-state{flex:none;font-style:normal;font-size:.46rem;letter-spacing:.14em;padding:3px 9px;border-radius:20px;border:1px solid currentColor;color:var(--dim)}
.set-state:empty{display:none}
.set-state.ok{color:var(--lime);background:rgba(57,255,20,.08)}
.set-state.warn{color:var(--gold);background:rgba(255,215,0,.08)}
.set-state.local{color:var(--orange);background:rgba(255,102,0,.08)}
.set-note{font-size:.84rem;line-height:1.45;color:var(--dim);margin-bottom:12px}
.set-note b{color:var(--text);font-weight:700}
.set-picker{margin:0}
/* The INSTRUCTIONS half of each door: what the button will ask of the player,
   read before pressing it rather than discovered after. */
.set-steps{list-style:none;counter-reset:set-step;display:grid;gap:7px;margin:0 0 14px}
.set-steps:empty{display:none}
.set-steps li{counter-increment:set-step;display:flex;align-items:flex-start;gap:9px;font-size:.8rem;line-height:1.35;color:var(--text)}
.set-steps li::before{
  content:counter(set-step);flex:none;display:flex;align-items:center;justify-content:center;
  width:17px;height:17px;border-radius:50%;
  font-family:var(--fd);font-size:.48rem;font-weight:700;
  color:var(--cyan);border:1px solid rgba(0,245,255,.45);background:rgba(0,245,255,.08)
}
.set-sec .btn-full{padding-left:16px;padding-right:16px}
/* Two switches that belong together read as a pair rather than a stack. The
   .btn default is padding:11px 26px + nowrap, which overflows a half-width
   column once paintInputToggles() appends ON/OFF — so the row trims the side
   padding and the tracking instead of letting the label clip. */
.set-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.set-row .btn{width:100%;padding:11px 10px;font-size:.62rem;letter-spacing:.05em}
/* A LOCAL profile always runs in offline mode — that is what local means — so
   the offline rule greys its Save button permanently, including the moment it
   reads "Save Progress Online" and would work. The steps above tell the player
   to press it, so when it can actually claim, it must look like it can.
   refreshClaimUi() sets the class off the same check that picks the label. */
html.is-offline #btn-save-acct.claim-ready{opacity:1;filter:none;cursor:pointer}
@media (max-width:520px){
  .set-sec{padding:12px 12px 14px}
  .set-note{font-size:.8rem}
}

/* ⚠️ The solids below are scoped to body.mode-3d, like everything above. */
body.mode-3d .btn-settings{--wall:#5b21a8;--wallb:#2a0e52}
/* The walls every modal slab gets in this mode. The tint's glow above is more
   specific than that shared rule, so without restating them here this one
   panel would be the only flat modal in the 3D interface. */
body.mode-3d .set-tint .fb-modal{
  box-shadow:
    0 2px 0 #131a2e,0 4px 0 #131a2e,0 6px 0 #080c18,0 8px 0 #080c18,0 10px 0 #05070f,
    0 44px 70px -22px rgba(0,0,0,.95),0 0 60px -10px rgba(168,85,247,.32),
    inset 0 1px 0 rgba(255,255,255,.16),inset 0 -1px 0 rgba(0,0,0,.6);
}
body.mode-3d .set-sec{
  background-image:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,0) 70%);
  box-shadow:
    0 1px 0 #10162a,0 2px 0 #10162a,0 3px 0 #070a14,0 4px 0 #070a14,
    0 14px 22px -12px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.11),inset 0 -1px 0 rgba(0,0,0,.5);
}
body.mode-3d .set-sec + .set-sec{margin-top:16px}
body.mode-3d .set-state{
  background-image:var(--spec);
  box-shadow:
    0 1px 0 rgba(255,255,255,.05),0 2px 0 #0b0f1c,0 3px 0 #070a14,
    0 6px 8px -4px rgba(0,0,0,.75),
    inset 0 1px 0 rgba(255,255,255,.18),inset 0 -1px 0 rgba(0,0,0,.45);
}
body.mode-3d .set-steps li::before{
  box-shadow:0 1px 0 #0a4a52,0 2px 0 #05272c,inset 0 1px 0 rgba(255,255,255,.22);
}
/* The Save button's attention pulse animates box-shadow, and an animation beats
   every ordinary rule — so in this mode it replaced the button's whole wall
   stack, and the one control the panel most wants pressed was the one flat
   button in it. The pulse is re-keyed WITH the walls, and handed back to the
   keycap rules on hover and press so the lift and the travel still happen. */
body.mode-3d .btn-save-acct{animation:saveGlow3d 2.4s ease-in-out infinite}
body.mode-3d .btn-save-acct:hover,
body.mode-3d .btn-save-acct:active,
body.mode-3d .btn-save-acct:disabled{animation:none}
@keyframes saveGlow3d{
  0%,100%{box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),
    0 4px 0 var(--wallb),0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),
    0 12px 18px -8px rgba(0,0,0,.8),0 0 0 rgba(57,255,20,0),
    inset 0 1px 0 var(--lip-hi),inset 0 -1px 0 var(--lip-lo)}
  50%{box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),
    0 4px 0 var(--wallb),0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),
    0 12px 18px -8px rgba(0,0,0,.8),0 0 18px rgba(57,255,20,.32),
    inset 0 1px 0 var(--lip-hi),inset 0 -1px 0 var(--lip-lo)}
}

/* ══════════════════════════════════════════════════════════════
   § v40 — 🔵 SAFE MODE · 🏅 MEDALS & GRID RECORDS · ⭐ FAVOURITES & FILTERS
   ══════════════════════════════════════════════════════════════ */

/* ── 🔵 SAFE MODE — the one tier below Stable, in a cool blue ── */
.diff-btns{flex-wrap:wrap;justify-content:center}
.diff-btn.diff-safe.active{background:rgba(77,163,255,.12);color:#4da3ff;box-shadow:0 0 10px #4da3ff}
.diff-btn.diff-safe.active:hover{box-shadow:0 0 15px #4da3ff}
.diff-selector.tier-safe{border-color:rgba(77,163,255,.45)}
.diff-selector.tier-safe::before{background:#4da3ff}
.res-bonus.res-bonus-safe{border-color:#4da3ff;color:#4da3ff;background:rgba(77,163,255,.08);box-shadow:0 0 10px rgba(77,163,255,.35)}
#toast.toast-safe{border-color:#4da3ff;color:#4da3ff;box-shadow:0 10px 30px rgba(77,163,255,.25)}
#toast.toast-medal{border-color:var(--gold);color:var(--gold);box-shadow:0 10px 30px rgba(255,215,0,.25)}
body.mode-3d .diff-btn.diff-safe.active{--wallb:#0a2a4a}
/* Four tiers on a phone: a 2×2 block, not a row that runs off the edge. */
@media (max-width:520px){
  .diff-btns{display:grid;grid-template-columns:1fr 1fr;width:100%;gap:6px}
  .diff-btn{padding:10px 8px}
}

/* ── 🏅 THE CARD STRIP — medal, bar to the next one, the record holder ──
   Decorative: the card handles every click, so it never takes the pointer. */
.gc-medal{
  --mcol:#56607a;
  margin-top:10px;display:flex;flex-direction:column;gap:5px;text-align:left
}
.game-card > .gc-medal{pointer-events:none}
.gc-medal.m-bronze{--mcol:#d08a45}
.gc-medal.m-silver{--mcol:#c9d2e3}
.gc-medal.m-gold{--mcol:#ffd700}
.gc-medal.m-diamond{--mcol:#7df9ff}
.gm-row{display:flex;align-items:center;gap:7px}
.gm-ico{font-size:1rem;line-height:1;width:1.35em;text-align:center;flex-shrink:0}
.gc-medal.m-none .gm-ico{color:var(--dimmer);font-size:.95rem}
.gm-bar{flex:1;height:5px;min-width:0;border-radius:3px;background:rgba(255,255,255,.07);overflow:hidden}
.gm-bar i{display:block;height:100%;border-radius:3px;background:var(--mcol);box-shadow:0 0 8px var(--mcol)}
.gc-medal.m-none .gm-bar i{box-shadow:none}
.gm-next{font-family:var(--fd);font-size:.46rem;font-weight:700;letter-spacing:.06em;color:var(--dim);white-space:nowrap}
.gm-rec{
  font-family:var(--fb);font-size:.66rem;font-weight:600;color:var(--dim);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis
}
/* The lock tag sits across the foot of a locked card; the strip would only
   peek out from under it. */
.game-card.locked .gc-medal{display:none}
body.mode-3d .gc-medal{transform:translateZ(10px)}
@media (max-width:700px){
  .gm-next{font-size:.42rem}
  .gm-rec{font-size:.6rem}
}

/* ── 🏅 THE RESULTS PANEL ── */
#res-medal{
  --mcol:#56607a;
  max-width:440px;margin:-8px auto 20px;padding:12px 14px;border-radius:12px;
  background:var(--s2);border:1px solid var(--mcol);text-align:left
}
#res-medal.m-bronze{--mcol:#d08a45}
#res-medal.m-silver{--mcol:#c9d2e3}
#res-medal.m-gold{--mcol:#ffd700}
#res-medal.m-diamond{--mcol:#7df9ff}
#res-medal.m-safe{--mcol:#4da3ff}
.rm-top{display:flex;align-items:center;gap:12px}
.rm-ico{display:inline-block;font-size:1.9rem;line-height:1}
.rm-txt{
  display:flex;flex-direction:column;
  font-family:var(--fd);font-size:.7rem;font-weight:900;letter-spacing:.12em;color:var(--mcol)
}
.rm-txt b{color:#fff;margin-right:6px}
.rm-txt em{font-family:var(--fb);font-style:normal;font-weight:600;font-size:.76rem;letter-spacing:0;color:var(--dim);margin-top:3px}
.rm-bar{height:6px;border-radius:3px;background:rgba(255,255,255,.07);overflow:hidden;margin:11px 0 6px}
.rm-bar i{display:block;height:100%;border-radius:3px;background:var(--mcol);box-shadow:0 0 10px var(--mcol)}
.rm-next{font-size:.78rem;color:var(--dim)}
.rm-rec{
  margin-top:9px;padding-top:9px;border-top:1px solid var(--border);
  font-family:var(--fd);font-size:.6rem;font-weight:900;letter-spacing:.12em;color:var(--gold)
}
#res-medal.is-new{animation:medalIn .8s cubic-bezier(.2,1.4,.4,1) both;box-shadow:0 0 30px -6px var(--mcol)}
#res-medal.is-new .rm-ico{animation:medalSpin 1.3s ease .15s both}
@keyframes medalIn{0%{transform:scale(.86);opacity:0}100%{transform:none;opacity:1}}
@keyframes medalSpin{
  0%{transform:perspective(300px) rotateY(0) scale(.5)}
  65%{transform:perspective(300px) rotateY(540deg) scale(1.25)}
  100%{transform:perspective(300px) rotateY(720deg) scale(1)}
}
html.reduced-motion #res-medal.is-new,
html.reduced-motion #res-medal.is-new .rm-ico{animation:none}

/* ── ⭐ THE FAVOURITE STAR — the ⓘ badge's twin, in the other top corner ── */
.gc-fav{
  position:absolute;top:8px;left:8px;z-index:3;
  width:30px;height:30px;line-height:28px;padding:0;text-align:center;
  border-radius:50%;cursor:pointer;font-size:1rem;
  background:rgba(255,255,255,.06);border:1px solid var(--border);color:var(--dim);
  transition:color .15s,border-color .15s,background .15s
}
.gc-fav::after{content:'';position:absolute;inset:-9px;border-radius:50%}
.gc-fav:hover{color:var(--gold);border-color:var(--gold);background:rgba(255,215,0,.1)}
.gc-fav.on{color:var(--gold);border-color:rgba(255,215,0,.6);background:rgba(255,215,0,.12);text-shadow:0 0 10px rgba(255,215,0,.6)}
@media (pointer: coarse){
  .gc-fav{width:40px;height:40px;line-height:38px;font-size:1.15rem;top:6px;left:6px}
  .gc-fav::after{inset:-6px}
}
.game-card.is-fav{border-color:rgba(255,215,0,.32)}
/* Same reason as the ⓘ: the card is preserve-3d and its contents are lifted
   off the face, so a control at Z 0 is behind them for hit-testing too. */
body.mode-3d .gc-fav{
  transform:translateZ(34px);
  box-shadow:
    0 1px 0 #131a2e,0 2px 0 #0a0e1c,
    0 6px 10px -6px rgba(0,0,0,.8),
    inset 0 1px 0 rgba(255,255,255,.14),inset 0 -1px 0 rgba(0,0,0,.45);
  transition:transform .12s ease,box-shadow .12s ease,border-color .15s,color .15s;
}
body.mode-3d .gc-fav:hover{transform:translateZ(34px) translateY(-1px)}
body.mode-3d .gc-fav:active{
  transform:translateZ(34px) translateY(2px);
  box-shadow:0 0 0 #131a2e,inset 0 2px 5px rgba(0,0,0,.6);
}

/* ── ⭐ GRID TOOLS — the filter chips reuse the Network Arena's .mp-filter ── */
.grid-tools{margin:-2px 0 14px}
.grid-tools .gt-chips{margin-bottom:0;align-items:center}
.grid-tools .gt-sort{margin-left:auto}
.gt-empty{
  margin-top:12px;padding:18px;border:1px dashed var(--border2);border-radius:12px;
  text-align:center;font-size:.86rem;color:var(--dim)
}
.gt-empty:empty{display:none}
.game-card.gf-hide{display:none}
@media (max-width:700px){
  .grid-tools .gt-sort{margin-left:0}
}

/* ══════════════════════════════════════════════════════════════
   § v41 — ⏸️ PAUSE MENU
   The backdrop is near-opaque on purpose: a paused Signal Trace or Defrag
   board that stayed readable would be free thinking time against a clock.
   ══════════════════════════════════════════════════════════════ */
.pause-tint{background:rgba(3,3,12,.965);backdrop-filter:blur(18px);z-index:2100}
.pause-tint .fb-modal{max-width:420px;border-color:rgba(0,245,255,.45);
  box-shadow:0 0 70px rgba(0,245,255,.14),inset 0 0 0 1px rgba(255,255,255,.03)}
.pause-tint .fb-modal::before{background:linear-gradient(90deg,transparent,var(--cyan),transparent);box-shadow:0 0 12px var(--cyan)}
.pause-tint .fb-title{background:linear-gradient(90deg,var(--cyan),var(--pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.pause-stats{
  background:var(--s2);border:1px solid var(--border);border-radius:12px;
  padding:12px 14px;margin:4px 0 18px;text-align:center
}
.ps-name{font-family:var(--fd);font-size:.66rem;font-weight:900;letter-spacing:.12em;color:#fff;margin-bottom:8px}
.ps-row{display:flex;justify-content:center;flex-wrap:wrap;gap:8px 16px;font-size:.84rem;color:var(--dim)}
.ps-row b{color:var(--cyan);font-family:var(--fd);font-size:.8rem}
.pause-btns{display:flex;flex-direction:column;gap:10px}
.pause-hint{margin-top:14px;text-align:center;font-size:.74rem;color:var(--dimmer)}
/* The header button: a touch wider than the other glyph buttons, because it
   is the one a player reaches for in a hurry. */
.btn-pause{min-width:40px}
@media (pointer:coarse){ .btn-pause{min-width:48px} }

/* ══════════════════════════════════════════════════════════════
   § v42 — 🎃 SEASONAL EVENTS · NEON HAUNT
   Everything here is scoped to body.ev-haunt (set by applyEventTheme() from
   the date), so outside October none of it exists. The player's own colour
   cosmetic owns --cyan, so the event accents with its own two colours rather
   than repainting that variable.
   ══════════════════════════════════════════════════════════════ */
body.ev-haunt{--ev-a:#ff7a1a;--ev-b:#a24dff}

/* A pumpkin-and-violet night behind every screen: two fixed glows, painted
   once, no animation, never in front of anything. */
body.ev-haunt::before{
  content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(120% 55% at 50% 108%,rgba(255,106,0,.16),transparent 60%),
    radial-gradient(90% 50% at 50% -8%,rgba(150,60,255,.14),transparent 62%);
}
body.ev-haunt .hub-bar{box-shadow:0 0 34px -8px rgba(255,122,26,.45),inset 0 -1px 0 rgba(162,77,255,.35)}
body.ev-haunt .hub-logo-sm::after{content:' 🎃';-webkit-text-fill-color:initial}
body.ev-haunt .sec-label{text-shadow:0 0 12px rgba(255,122,26,.25)}

/* ── The hub banner ── */
.ev-banner{
  border-color:rgba(255,122,26,.55)!important;
  background:linear-gradient(120deg,rgba(255,106,0,.13),rgba(120,40,200,.12) 60%,rgba(8,6,20,.6))!important;
  box-shadow:0 0 40px -10px rgba(255,122,26,.5)
}
/* Gradient text like every banner title — and a drop-shadow FILTER, never a
   text-shadow, which Chrome paints over a background-clip:text face. */
.ev-banner .mp-banner-title{background:linear-gradient(90deg,#ffc08a,#ff7a1a 45%,#c78bff);-webkit-background-clip:text;background-clip:text;filter:drop-shadow(0 0 8px rgba(255,122,26,.45))}
.ev-banner.ev-tease{opacity:.86;border-style:dashed!important}
.ev-prog{height:5px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden;margin-top:8px;max-width:320px}
.ev-prog i{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,#ff7a1a,#a24dff);box-shadow:0 0 10px rgba(255,122,26,.6)}
.ev-lb-label{color:#ffb070}

/* ── Floating decorations (hub only, never over a round) ── */
#ev-deco{position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden;display:none}
body.ev-haunt:has(#hub-screen.active) #ev-deco{display:block}
html.reduced-motion #ev-deco{display:none!important}
.evd-bat{position:absolute;left:-60px;opacity:.55;filter:drop-shadow(0 0 6px rgba(162,77,255,.8));
  animation-name:evdFly;animation-timing-function:linear;animation-iteration-count:infinite}
@keyframes evdFly{
  0%{transform:translate(0,0) scaleX(-1)}
  25%{transform:translate(28vw,-3vh) scaleX(-1)}
  50%{transform:translate(55vw,2vh) scaleX(-1)}
  75%{transform:translate(82vw,-2vh) scaleX(-1)}
  100%{transform:translate(112vw,1vh) scaleX(-1)}
}
.evd-float{position:absolute;font-size:2.1rem;opacity:.42;animation:evdBob 6s ease-in-out infinite;
  filter:drop-shadow(0 0 12px rgba(255,122,26,.7))}
.evd-l{left:2.5%;bottom:9%}
.evd-r{right:3%;bottom:14%;animation-delay:-2.2s;font-size:1.7rem}
.evd-g{right:9%;top:22%;animation-delay:-4s;font-size:1.5rem;opacity:.3;filter:drop-shadow(0 0 10px rgba(255,255,255,.5))}
@keyframes evdBob{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-14px) rotate(4deg)}}
@media (max-width:700px){ .evd-float{display:none} }

/* ── The shelf ── */
.market-tab-event{color:#ffb070!important;border-color:rgba(255,122,26,.5)!important}
.market-tab-event.active{background:rgba(255,122,26,.14)!important;box-shadow:0 0 18px rgba(255,122,26,.3)}
/* Nine tabs on one line. The eight stock tabs leave 27px of a 1060px row, and
   the event tab is ~106px, so while it shows (renderEventShelf sets has-event)
   every tab gives up a few pixels of side padding and the gap closes a little.
   Outside an event the row keeps its roomier look.
   ⚠️ The padding must SNAP, not animate: .market-tab's `transition:all` would
   ease it over .2s, and for those frames the nine wide tabs wrap — the very
   two-line row this rule exists to prevent, flashed on every first visit. So
   the 2D transition list here names only what hover and .active change. (3D
   tabs carry their own list, which never included padding — and which this
   must not clobber, or their hover press would stop animating.) */
.market-tabs.has-event{gap:6px}
.market-tabs.has-event .market-tab{padding-left:12px;padding-right:12px}
body:not(.mode-3d) .market-tabs.has-event .market-tab{transition-property:color,background-color,border-color,box-shadow}
.ev-shelf-note{
  margin:0 0 16px;padding:12px 14px;border-radius:12px;
  border:1px dashed rgba(255,122,26,.55);background:rgba(255,106,0,.07);
  font-family:var(--fd);font-size:.56rem;font-weight:700;letter-spacing:.1em;line-height:1.7;color:#ffb070
}
.shop-card.shop-event{border-color:rgba(255,122,26,.45)}
.shop-evtag{
  display:inline-block;margin:0 auto 6px;padding:3px 10px;border-radius:20px;
  font-family:var(--fd);font-size:.44rem;font-weight:900;letter-spacing:.12em;
  color:#1a0800;background:linear-gradient(90deg,#ff9a3c,#ff6a00);box-shadow:0 0 12px rgba(255,122,26,.5)
}

/* ── 🪦 GRAVEYARD SHIFT — the event's exit state ── */
/* ⚠️ The layer itself carries a background. With a TRANSPARENT root, Chrome
   drew the 3D chrome's lifted buttons (preserve-3d, translateZ) OVER this
   z-index 2500 layer in 3D mode — the hub's buttons showed through the
   headstones. Every other exit paints its root, which is why only this one
   met it. */
#exit-fx.fx-grave{background:rgba(3,1,8,.35);animation:gvOut 1.8s ease forwards}
@keyframes gvOut{0%,82%{opacity:1}100%{opacity:0}}
.gv-sky{position:absolute;inset:0;
  background:linear-gradient(180deg,#07020f 0%,#1a0630 55%,#3a1236 80%,#5a1d10 100%);
  animation:gvSky .35s ease both}
@keyframes gvSky{from{opacity:0}to{opacity:.97}}
.gv-moon{position:absolute;left:66%;top:14%;width:15vmin;height:15vmin;border-radius:50%;
  background:radial-gradient(circle at 38% 38%,#fff8e0,#ffd98a 55%,#e8a64a);
  box-shadow:0 0 60px 12px rgba(255,210,130,.35);animation:gvMoon 1.1s cubic-bezier(.2,.8,.2,1) .1s both}
@keyframes gvMoon{from{transform:translateY(40vh);opacity:0}to{transform:none;opacity:1}}
.gv-bat{position:absolute;left:-8%;animation-name:gvBat;animation-timing-function:ease-in;animation-fill-mode:both;
  filter:drop-shadow(0 0 4px #000)}
@keyframes gvBat{from{transform:translate(0,0) scaleX(-1)}to{transform:translate(118vw,-18vh) scaleX(-1)}}
.gv-hill{position:absolute;left:-10%;right:-10%;bottom:-30vh;height:52vh;border-radius:50%;
  background:#050208;box-shadow:0 -6px 30px rgba(255,106,0,.25)}
.gv-stones{position:absolute;left:0;right:0;bottom:6vh;display:flex;justify-content:center;align-items:flex-end;gap:4vw}
.gv-stone{width:calc(9vmin * var(--s));height:calc(13vmin * var(--s));border-radius:48% 48% 6% 6%/38% 38% 6% 6%;
  background:linear-gradient(180deg,#5b5a6e,#2a2936);box-shadow:inset 0 3px 0 rgba(255,255,255,.12),0 0 18px rgba(0,0,0,.6);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.4vmin;
  animation:gvRise .6s cubic-bezier(.2,1.3,.4,1) both}
@keyframes gvRise{from{transform:translateY(22vh)}to{transform:none}}
.gv-main{width:22vmin;height:28vmin}
.gv-main b{font-family:var(--fd);font-size:2.6vmin;letter-spacing:.2em;color:#e9e6ff}
.gv-main em{font-family:var(--fd);font-style:normal;font-weight:900;font-size:4.6vmin;color:#ffb070;text-shadow:0 0 12px rgba(255,122,26,.7)}
.gv-main i{font-family:var(--fd);font-style:normal;font-size:1.8vmin;letter-spacing:.3em;color:#bdb8d8}

/* ══════════════════════════════════════════════════════════════
   § v43 — 🏷️ TITLES · 🪪 PLAYER CARDS · 📋 DAILY CONTRACTS
   ══════════════════════════════════════════════════════════════ */

/* ── 🏷️ A title under a name on the boards ── */
.lb-ttl{
  display:block;font-style:normal;margin-top:2px;
  font-family:var(--fd);font-size:.42rem;font-weight:900;letter-spacing:.14em;
  color:var(--gold);opacity:.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis
}
.lb-row[data-uid]{cursor:pointer}
.lb-row[data-uid]:hover .lb-name{text-decoration:underline;text-decoration-color:rgba(255,255,255,.3);text-underline-offset:3px}
.ach-wear{
  margin-top:8px;width:100%;padding:6px 8px;border-radius:8px;cursor:pointer;
  font-family:var(--fd);font-size:.44rem;font-weight:900;letter-spacing:.12em;
  background:rgba(255,215,0,.06);border:1px solid rgba(255,215,0,.3);color:var(--gold)
}
.ach-wear:hover{background:rgba(255,215,0,.14)}
.ach-wear.on{background:rgba(255,215,0,.2);border-color:var(--gold);box-shadow:0 0 12px rgba(255,215,0,.3)}

/* ── 🪪 The player card ── */
.pcard-tint .fb-modal{max-width:500px;border-color:rgba(255,215,0,.4);box-shadow:0 0 70px rgba(255,215,0,.12),inset 0 0 0 1px rgba(255,255,255,.03)}
.pcard-tint .fb-modal::before{background:linear-gradient(90deg,transparent,var(--gold),transparent);box-shadow:0 0 12px var(--gold)}
.pcard-body{display:flex;flex-direction:column;gap:14px;text-align:left}
.pc-hero{display:flex;align-items:center;gap:16px;padding-right:34px}
.pc-skin{font-size:3.2rem;line-height:1;filter:drop-shadow(0 0 14px rgba(0,245,255,.35))}
.pc-id{min-width:0}
.pc-name{font-family:var(--fd);font-size:1rem;font-weight:900;letter-spacing:.08em;color:#fff;word-break:break-word}
.pc-you{font-size:.5rem;letter-spacing:.14em;padding:2px 7px;border-radius:10px;background:rgba(0,245,255,.15);color:var(--cyan);vertical-align:middle}
.pc-title{margin-top:5px;font-family:var(--fd);font-size:.56rem;font-weight:900;letter-spacing:.14em;color:var(--gold)}
.pc-lvl{margin-top:5px;font-family:var(--fd);font-size:.52rem;letter-spacing:.14em;color:var(--dim)}
.pc-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.pc-tile{background:var(--s2);border:1px solid var(--border);border-radius:10px;padding:10px 8px;text-align:center;display:flex;flex-direction:column;gap:3px}
.pc-tile span{font-size:1.1rem}
.pc-tile b{font-family:var(--fd);font-size:.78rem;color:#fff}
.pc-tile em{font-style:normal;font-size:.62rem;color:var(--dim);line-height:1.3}
.pc-sec{font-family:var(--fd);font-size:.56rem;font-weight:700;letter-spacing:.16em;color:var(--dimmer)}
.pc-best{display:flex;flex-direction:column;gap:6px}
.pc-bm{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:10px;
  background:var(--s2);border:1px solid var(--border);border-radius:10px;padding:8px 12px}
.pc-bm-ico{font-size:1.2rem}
.pc-bm-name{font-family:var(--fd);font-size:.56rem;font-weight:700;letter-spacing:.08em;color:var(--txt)}
.pc-bm-pts{font-family:var(--fd);font-size:.62rem;color:var(--cyan)}
.pc-badges{display:flex;flex-wrap:wrap;gap:6px}
.pc-badge{font-size:.72rem;padding:4px 9px;border-radius:14px;background:var(--s2);border:1px solid var(--border);color:var(--txt)}
.pc-empty{font-size:.82rem;color:var(--dim)}
.pc-btns{display:flex;flex-direction:column;gap:8px}
@media (max-width:420px){ .pc-tiles{grid-template-columns:repeat(2,1fr)} }

/* ── 📋 Today's contracts ── */
.contracts-panel{
  margin:0 0 18px;padding:14px 16px;border-radius:var(--r);
  background:linear-gradient(135deg,rgba(57,255,20,.05),rgba(0,245,255,.04));
  border:1px solid rgba(57,255,20,.3)
}
.contracts-panel:empty{display:none}
.ct-head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:6px;margin-bottom:10px}
.ct-title{font-family:var(--fd);font-size:.66rem;font-weight:900;letter-spacing:.14em;color:var(--lime)}
.ct-meta{font-size:.74rem;color:var(--dim)}
.ct-row{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:10px;
  padding:8px 10px;border-radius:10px;background:var(--s2);border:1px solid var(--border);margin-bottom:6px}
.ct-row.done{opacity:.62}
.ct-row.done .ct-txt{text-decoration:line-through;text-decoration-color:rgba(57,255,20,.6)}
.ct-ico{font-size:1.05rem}
.ct-txt{font-size:.84rem;color:var(--txt);line-height:1.35}
.ct-cr{font-family:var(--fd);font-size:.52rem;font-weight:900;letter-spacing:.08em;color:var(--cyan);white-space:nowrap}
.ct-go{padding:6px 10px!important;min-height:0!important;font-size:.5rem!important}
.ct-bonus{margin-top:4px;font-size:.76rem;color:var(--dim);text-align:right}
.ct-bonus.done{color:var(--lime)}
@media (max-width:520px){
  .ct-row{grid-template-columns:auto 1fr auto;row-gap:6px}
  .ct-go{grid-column:1 / -1;justify-self:end}
}

/* ══════════════════════════════════════════════════════════════
   § v47 — 🎉 PARTY MODE
   ══════════════════════════════════════════════════════════════ */
.pt-banner .mp-banner-title{background:linear-gradient(90deg,#ff0090,#ffd700 55%,#39ff14);-webkit-background-clip:text;background-clip:text}
.party-tint .fb-modal{max-width:560px;border-color:rgba(255,0,144,.45);box-shadow:0 0 70px rgba(255,0,144,.14),inset 0 0 0 1px rgba(255,255,255,.03)}
.party-tint .fb-modal::before{background:linear-gradient(90deg,transparent,#ff0090,#ffd700,transparent);box-shadow:0 0 12px #ff0090}
.party-tint .fb-title{background:linear-gradient(90deg,#ff0090,#ffd700);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.party-body{display:flex;flex-direction:column;gap:12px;text-align:left}
.pt-sec{font-family:var(--fd);font-size:.58rem;font-weight:700;letter-spacing:.16em;color:var(--dimmer);margin-top:4px}
.pt-sec em{font-family:var(--fb);font-style:normal;letter-spacing:0;font-size:.74rem;margin-left:6px;color:var(--dim)}
.pt-players{display:flex;flex-direction:column;gap:7px}
.pt-player{display:flex;align-items:center;gap:9px}
.pt-dot{width:12px;height:12px;border-radius:50%;background:var(--pc);box-shadow:0 0 10px var(--pc);flex-shrink:0}
.pt-name{flex:1;min-width:0;padding:9px 12px!important}
/* The same control as the modal's own ✕ (.fb-close) — 30px, glyph centred,
   and flex:none so a long name cannot squash it into an oval. */
.pt-del{flex:none;width:30px;height:30px;display:flex;align-items:center;justify-content:center;
  border-radius:50%;border:1px solid var(--border);background:var(--s2);color:var(--dim);
  font-size:.8rem;cursor:pointer;transition:all .2s}
.pt-del:hover{color:var(--red);border-color:var(--red);background:rgba(255,36,66,.1)}
/* Both section actions hug their text; START THE PARTY is the only full-width
   bar in the sheet. Left to stretch, SHUFFLE came out 507px against ADD
   PLAYER's 110px — two sibling secondary buttons at odds. */
.pt-add,#pt-shuffle{align-self:flex-start}
/* ── the three mission slots ──
   Each round is a plate you press to change it, made of the parts a hub card
   is made of: the mission's art, its name in its own accent, its one-line
   hook. They replaced three native <select>s, whose open list is an OS window
   no stylesheet can paint — a flat grey list in 2D, and the one flat object
   left in the 3D party flow. */
.pt-missions{display:flex;flex-direction:column;gap:8px}
/* --acc is the mission's accent, from the palette NAME the hub card carries
   in data-c (partyCardInfo). var(--cyan) and friends rather than hex, so the
   colour-blind palette re-points these exactly as it does the hub grid. */
.pt-slot,.ptp-card{--acc:var(--cyan)}
.pt-slot[data-c="pink"],.ptp-card[data-c="pink"]{--acc:var(--pink)}
.pt-slot[data-c="lime"],.ptp-card[data-c="lime"]{--acc:var(--lime)}
.pt-slot[data-c="gold"],.ptp-card[data-c="gold"]{--acc:var(--gold)}
.pt-slot[data-c="purple"],.ptp-card[data-c="purple"]{--acc:var(--purple)}
.pt-slot[data-c="orange"],.ptp-card[data-c="orange"]{--acc:var(--orange)}
.pt-slot[data-c="red"],.ptp-card[data-c="red"]{--acc:var(--red)}
.pt-slot{
  position:relative;display:grid;grid-template-columns:24px 40px minmax(0,1fr) auto;align-items:center;gap:12px;
  width:100%;padding:11px 14px 11px 10px;text-align:left;cursor:pointer;
  font:inherit;color:var(--text);
  background:linear-gradient(90deg,color-mix(in srgb,var(--acc) 11%,transparent),transparent 58%),var(--s1);
  border:1px solid var(--border);border-radius:var(--r);
  box-shadow:inset 3px 0 0 var(--acc);
  transition:transform .22s ease,border-color .22s ease,box-shadow .22s ease
}
/* The card is the only hit target. Its parts are lifted off the face in the 3D
   chrome, and a lifted child in a preserve-3d parent hit-tests in front of
   anything at Z 0 — the ⓘ badge's bug on the hub cards. None has a handler. */
.pt-slot > *,.ptp-card > *{pointer-events:none}
.pt-slot-n{width:22px;height:22px;display:flex;align-items:center;justify-content:center;border-radius:50%;
  border:1px solid color-mix(in srgb,var(--gold) 50%,transparent);background:color-mix(in srgb,var(--gold) 7%,transparent);
  font-family:var(--fd);font-size:.6rem;font-weight:700;color:var(--gold)}
.pt-slot-ico{font-size:1.75rem;line-height:1;text-align:center;
  filter:drop-shadow(0 0 10px color-mix(in srgb,var(--acc) 38%,transparent))}
.pt-slot-txt{min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:5px 9px}
/* A fixed line box for the name row, taller than the SAME BOARD chip: left to
   size itself, the one slot carrying the chip came out 4px taller. */
.pt-slot-name{font-family:var(--fd);font-size:.64rem;font-weight:700;letter-spacing:.08em;line-height:18px;color:var(--acc)}
.pt-slot-desc{flex-basis:100%;min-width:0;font-size:.8rem;color:var(--dim);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pt-same{font-style:normal;white-space:nowrap;line-height:1.2;
  font-family:var(--fd);font-size:.44rem;font-weight:700;letter-spacing:.14em;
  color:var(--lime);background:color-mix(in srgb,var(--lime) 8%,transparent);border:1px solid currentColor;
  border-radius:20px;padding:2px 8px}
.pt-slot-go{font-family:var(--fd);font-size:.48rem;font-weight:700;letter-spacing:.14em;color:var(--dim);
  padding:6px 10px;border:1px solid var(--border);border-radius:20px;
  transition:color .2s ease,border-color .2s ease,background-color .2s ease}
/* Hover is gated on a real pointer: a tap latches :hover on most phones, and
   the plate stayed lifted and lit long after the finger had moved on. */
@media (hover:hover){
  .pt-slot:hover{transform:translateY(-2px);border-color:color-mix(in srgb,var(--acc) 70%,transparent);
    box-shadow:inset 3px 0 0 var(--acc),0 8px 26px -8px color-mix(in srgb,var(--acc) 45%,transparent)}
  .pt-slot:hover .pt-slot-go{color:var(--acc);border-color:var(--acc);background:color-mix(in srgb,var(--acc) 10%,transparent)}
}
.pt-slot:active{transform:scale(.985)}
.pt-slot:focus-visible{outline:2px solid var(--acc);outline-offset:3px}
/* The ring a pick leaves on the slot it changed (both of them, on a swap).
   An overlay that fades, NOT an animated box-shadow: an animation on
   box-shadow replaces the whole stack, 3D walls included, while it runs. */
.pt-slot::after{content:'';position:absolute;inset:-1px;border-radius:inherit;pointer-events:none;opacity:0;
  border:1px solid var(--acc);box-shadow:0 0 24px -2px var(--acc),inset 0 0 18px -8px var(--acc)}
.pt-slot.pt-flash::after{animation:ptFlash 1.1s ease-out}
@keyframes ptFlash{0%,30%{opacity:1}100%{opacity:0}}
html.reduced-motion .pt-slot.pt-flash::after{animation:none}

/* ── the mission picker (#ptpick-overlay) ──
   A second overlay standing ON the setup sheet, so it goes above the shell's
   z-index. Its backdrop is lighter and less blurred than the shell's: the
   sheet is already sitting on a dimmed, blurred page, and the full treatment
   again is the doubled dim the Settings hand-off steps aside to avoid — this
   keeps the party visibly underneath the thing being chosen for it. */
#ptpick-overlay{z-index:2050;background:rgba(2,2,10,.58);backdrop-filter:blur(3px)}
.ptpick-tint .fb-modal{max-width:780px;border-color:rgba(255,215,0,.42);box-shadow:0 0 70px rgba(255,215,0,.12),inset 0 0 0 1px rgba(255,255,255,.03)}
.ptpick-tint .fb-modal::before{background:linear-gradient(90deg,transparent,#ffd700,#ff0090,transparent);box-shadow:0 0 12px #ffd700}
.ptpick-tint .fb-title{background:linear-gradient(90deg,#ffd700,#ff0090);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.ptpick-tint .fb-head{margin-bottom:22px}
/* The rows are further apart than the columns: a round tab stands 9px above
   its card, and in the 3D chrome the card above has an 8px wall under it. */
.ptp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(158px,1fr));gap:20px 12px}
/* The hub card, at picker scale: art, name in its accent, the hook, the cap. */
.ptp-card{
  position:relative;display:flex;flex-direction:column;align-items:center;text-align:center;
  padding:22px 12px 14px;cursor:pointer;font:inherit;color:var(--text);
  background:var(--s1);border:1px solid var(--border);border-radius:var(--rl);
  transition:transform .25s ease,border-color .25s ease,box-shadow .25s ease
}
.ptp-ico{font-size:2.2rem;line-height:1;margin-bottom:11px}
.ptp-name{font-family:var(--fd);font-size:.56rem;font-weight:700;letter-spacing:.08em;line-height:1.5;color:var(--acc);margin-bottom:7px}
.ptp-desc{font-size:.74rem;line-height:1.4;color:var(--dim);margin-bottom:12px;
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.ptp-pills{margin-top:auto;display:flex;flex-wrap:wrap;justify-content:center;gap:5px}
.ptp-pts,.ptp-same{padding:3px 9px;border-radius:20px;border:1px solid;white-space:nowrap;
  font-family:var(--fd);font-size:.44rem;font-weight:700;letter-spacing:.06em}
.ptp-pts{color:var(--acc);border-color:var(--acc);background:color-mix(in srgb,var(--acc) 6%,transparent)}
/* Borders and washes are mixed from the same variable as the text, so the
   colour-blind palette moves all three together. */
.ptp-same{color:var(--lime);border-color:color-mix(in srgb,var(--lime) 60%,transparent);background:color-mix(in srgb,var(--lime) 7%,transparent)}
/* Which round a card is in: a tab on the top edge. In a corner it ran into
   the centred art. Opaque, because it sits across the card's border line. */
.ptp-badge{position:absolute;top:-9px;left:50%;transform:translateX(-50%);padding:3px 9px;border-radius:20px;white-space:nowrap;
  font-family:var(--fd);font-size:.42rem;font-weight:900;letter-spacing:.1em;line-height:1.2}
/* This round's mission now: lit in its own accent, the way the pass card is. */
.ptp-card.on{border-color:var(--acc);
  background:radial-gradient(120% 90% at 50% 0%,color-mix(in srgb,var(--acc) 18%,transparent),transparent 70%),var(--s2);
  box-shadow:0 0 0 1px var(--acc),0 0 30px -8px var(--acc)}
.ptp-card.on .ptp-badge{color:#05060f;background:var(--acc);box-shadow:0 0 12px -2px var(--acc)}
/* Another round's mission: badged with that round, its print set back, still
   pickable (it swaps). The PRINT dims, not the card — opacity on the card
   would dim the tab too, and in the 3D chrome opacity is a grouping property
   that forces the card flat and drops its raised art onto the face. */
.ptp-card.taken > :not(.ptp-badge){opacity:.5;transition:opacity .2s ease}
.ptp-card.taken .ptp-badge{color:var(--gold);border:1px solid color-mix(in srgb,var(--gold) 60%,transparent);background:#0b0e1e}
@media (hover:hover){
  .ptp-card:hover{transform:translateY(-4px) scale(1.02);border-color:var(--acc);
    box-shadow:0 8px 30px color-mix(in srgb,var(--acc) 22%,transparent)}
  .ptp-card.on:hover{box-shadow:0 0 0 1px var(--acc),0 8px 30px color-mix(in srgb,var(--acc) 32%,transparent)}
  .ptp-card.taken:hover > *{opacity:1}
}
.ptp-card:active{transform:scale(.97)}
.ptp-card:focus-visible{outline:2px solid var(--acc);outline-offset:3px}
.ptp-card.taken:focus-visible > *{opacity:1}
.ptp-foot{margin-top:18px;display:flex;flex-direction:column;gap:4px;
  font-size:.78rem;line-height:1.5;color:var(--dimmer);text-align:center}
/* the legend's ⇄, drawn as the taken tab is */
.ptp-key{display:inline-block;padding:0 7px;border-radius:20px;font-size:.7rem;line-height:1.5;
  color:var(--gold);border:1px solid color-mix(in srgb,var(--gold) 55%,transparent);background:color-mix(in srgb,var(--gold) 8%,transparent)}
.pt-note{font-size:.8rem;line-height:1.5;color:var(--dim);background:var(--s2);border:1px solid var(--border);border-radius:10px;padding:10px 12px}
.pt-note b{color:var(--text)}
/* the pass-the-device card */
.pt-turn{text-align:center;padding:18px 14px;border-radius:14px;border:1px solid var(--pc);
  background:radial-gradient(120% 90% at 50% 0%,color-mix(in srgb,var(--pc) 16%,transparent),transparent 70%),var(--s2);
  box-shadow:0 0 30px -12px var(--pc)}
.pt-pass{font-family:var(--fd);font-size:.54rem;letter-spacing:.2em;color:var(--dim)}
.pt-who{font-family:var(--fd);font-size:1.6rem;font-weight:900;letter-spacing:.06em;color:var(--pc);margin:6px 0 10px;
  filter:drop-shadow(0 0 10px var(--pc));word-break:break-word}
.pt-what{font-family:var(--fd);font-size:.72rem;font-weight:700;letter-spacing:.1em;color:#fff}
.pt-what em{font-style:normal;color:var(--lime);font-size:.6rem}
.pt-how{font-size:.82rem;line-height:1.5;color:var(--dim);margin:8px 0 14px}
/* the scoreboard */
.pt-board{display:flex;flex-direction:column;gap:5px}
.pt-row,.pt-heads{display:grid;grid-template-columns:34px 12px 1fr repeat(3,56px) 64px;align-items:center;gap:8px;padding:7px 10px;border-radius:9px}
.pt-row{background:var(--s2);border:1px solid var(--border)}
.pt-row.on{border-color:var(--pc);box-shadow:0 0 14px -6px var(--pc)}
.pt-heads{padding-top:0;padding-bottom:0;border:1px solid transparent;
  font-family:var(--fd);font-size:.5rem;letter-spacing:.1em;color:var(--dimmer);text-align:right}
/* These emoji ARE the column labels, so they are sized to be read rather than
   to match the TOTAL caption beside them. */
.pt-heads span[title]{font-size:.92rem;line-height:1}
.pt-rank{font-family:var(--fd);font-size:.62rem;color:var(--dim)}
.pt-rname{font-family:var(--fd);font-size:.58rem;font-weight:700;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pt-cell{font-family:var(--fd);font-size:.56rem;color:var(--dim);text-align:right}
.pt-cell.cur{color:var(--gold)}
.pt-total{font-family:var(--fd);font-size:.66rem;font-weight:900;color:var(--cyan);text-align:right}
.pt-end{align-self:center;margin-top:4px}
/* the winner */
.pt-win{text-align:center;padding:10px 0 6px}
.pt-crown{font-size:2.8rem;animation:ptCrown 1.4s cubic-bezier(.2,1.4,.4,1) both}
@keyframes ptCrown{from{transform:translateY(-30px) scale(.4);opacity:0}to{transform:none;opacity:1}}
html.reduced-motion .pt-crown{animation:none}
.pt-wname{font-family:var(--fd);font-size:1.2rem;font-weight:900;letter-spacing:.08em;color:var(--pc);filter:drop-shadow(0 0 12px var(--pc))}
.pt-wpts{font-family:var(--fd);font-size:.8rem;color:var(--gold);margin-top:4px}
.pt-final-btns{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:6px}
@media (max-width:520px){
  .pt-row,.pt-heads{grid-template-columns:26px 10px 1fr repeat(3,40px) 50px;gap:5px;padding:6px 7px}
  .pt-row .pt-dot{width:10px;height:10px}   /* 12px overran its 10px track */
  .pt-heads span[title]{font-size:.8rem}
  .pt-cell{font-size:.48rem}
  .pt-who{font-size:1.2rem}
  /* REMATCH + NEW MISSIONS do not fit one row here, and wrapping left the
     primary stranded alone above the other two. */
  .pt-final-btns{flex-direction:column;align-items:stretch}
  .pt-final-btns .btn{width:100%}
  .pt-slot{grid-template-columns:22px 32px minmax(0,1fr) auto;gap:9px;padding:10px 10px 10px 8px}
  .pt-slot-ico{font-size:1.45rem}
  .pt-slot-name{font-size:.56rem}
  .pt-slot-desc{font-size:.74rem}
  .pt-slot-go{padding:5px 8px;font-size:.42rem;letter-spacing:.1em}
  /* auto-fill's 158px floor leaves ONE column on a 375px phone; two it is. */
  .ptp-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:18px 10px}
  .ptp-card{padding:22px 8px 12px}
  .ptp-ico{font-size:1.8rem;margin-bottom:8px}
  .ptp-name{font-size:.5rem}
  .ptp-desc{font-size:.7rem}
}

/* ── 🧊 in the 3D chrome: the board is a stack of plates like the
   leaderboard's, and the pass card is the panel the GO button stands on. The
   party sheet was the one screen left flat inside an extruded modal. ── */
body.mode-3d .pt-row,
body.mode-3d .pt-turn,
body.mode-3d .pt-note{transform-style:preserve-3d}
body.mode-3d .pt-row{
  --wall:#121829;--wallb:#080b16;
  background-image:linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,0) 60%);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wallb),
    0 7px 10px -6px rgba(0,0,0,.8),
    inset 0 1px 0 rgba(255,255,255,.1)
}
/* The leader's plate takes the gold body the leaderboard's #1 has. It is
   the row after the heading — not :first-of-type, which .pt-heads owns. */
body.mode-3d .pt-heads + .pt-row{--wall:#6b5600;--wallb:#3a2f00}
/* Whose turn it is stands one wall taller and keeps its accent glow; same
   specificity as the leader rule, so it comes last and wins when they are
   the same player. */
body.mode-3d .pt-row.on{
  --wall:#1b2340;--wallb:#0a0f1e;
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wallb),
    0 5px 0 var(--wallb),0 12px 16px -8px rgba(0,0,0,.85),
    0 0 20px -7px var(--pc),
    inset 0 1px 0 rgba(255,255,255,.14)
}
/* .pt-turn paints its face with a radial tint through the background
   shorthand, so it gets the body only — a background-image here would wipe it. */
body.mode-3d .pt-turn{
  box-shadow:
    0 1px 0 #10162a,0 2px 0 #10162a,0 3px 0 #070a14,0 4px 0 #070a14,
    0 16px 24px -12px rgba(0,0,0,.85),
    0 0 30px -12px var(--pc),
    inset 0 1px 0 rgba(255,255,255,.11),inset 0 -1px 0 rgba(0,0,0,.5)
}
body.mode-3d .pt-note{
  background-image:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,0) 70%);
  box-shadow:
    0 1px 0 #10162a,0 2px 0 #10162a,0 3px 0 #070a14,0 4px 0 #070a14,
    0 14px 22px -12px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.11),inset 0 -1px 0 rgba(0,0,0,.5)
}
body.mode-3d .pt-del{
  box-shadow:0 1px 0 #171e33,0 2px 0 #0a0e1a,0 4px 6px -3px rgba(0,0,0,.8),
             inset 0 1px 0 rgba(255,255,255,.18);
  transition:transform .14s var(--pop-ease),box-shadow .14s var(--pop-ease),color .14s,border-color .14s
}
body.mode-3d .pt-del:hover{transform:perspective(500px) translateZ(6px) scale(1.06)}
body.mode-3d .pt-del:active{transform:translateY(2px)}

/* The mission slots and the picker's cards are plates like the hub's cards:
   the same wall stack, lip and contact shade, lifted on hover, driven down on
   press, and tilted toward the pointer — .pt-slot and .ptp-card are in the
   tilt registry in app.js (SEL); a class missing there never gets --tx/--ty. */
body.mode-3d .pt-slot,
body.mode-3d .ptp-card{
  --wall:#141a2c;--wallb:#070a14;--tx:0deg;--ty:0deg;
  transform-style:preserve-3d;
  transform-origin:50% 60%;
  transition:transform .2s var(--pop-ease),box-shadow .2s var(--pop-ease),border-color .2s
}
/* A slot is a wide, short plate, so it takes a fraction of the tilt a card
   does: at the full nine degrees its far end swung ~40px out of the sheet.
   Its face keeps the 2D accent wash, restated WITH the specular in one
   background shorthand — a background-image alone would wipe the wash, the
   .pt-turn trap above. */
body.mode-3d .pt-slot{
  background:linear-gradient(180deg,rgba(255,255,255,.07),rgba(255,255,255,0) 55%),
             linear-gradient(90deg,color-mix(in srgb,var(--acc) 12%,transparent),transparent 58%),var(--s1);
  transform:perspective(var(--eye)) translateZ(0) rotateX(calc(var(--tx) * .5)) rotateY(calc(var(--ty) * .15));
  box-shadow:
    inset 3px 0 0 var(--acc),
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),
    0 4px 0 var(--wallb),0 5px 0 var(--wallb),0 6px 0 var(--wallb),
    0 14px 20px -10px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.13),inset 0 -1px 0 rgba(0,0,0,.5)
}
body.mode-3d .ptp-card{
  overflow:visible;           /* anything else flattens the card — see .game-card */
  background-image:linear-gradient(170deg,rgba(255,255,255,.07),rgba(255,255,255,0) 46%);
  transform:perspective(var(--eye)) translateZ(0) rotateX(var(--tx)) rotateY(var(--ty));
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
    0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),0 8px 0 var(--wallb),
    0 18px 26px -12px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.14),inset 0 -1px 0 rgba(0,0,0,.5)
}
/* This round's mission stands in its own colour — walls mixed from its
   accent, the way .mode-card.on takes cyan ones — a step higher, and lit. Its
   2D face is a radial wash through the shorthand, restated with the specular. */
body.mode-3d .ptp-card.on{
  --wall:color-mix(in srgb,var(--acc) 42%,#070a14);--wallb:color-mix(in srgb,var(--acc) 22%,#05070e);
  background:linear-gradient(170deg,rgba(255,255,255,.09),rgba(255,255,255,0) 46%),
             radial-gradient(120% 90% at 50% 0%,color-mix(in srgb,var(--acc) 18%,transparent),transparent 70%),var(--s2);
  transform:perspective(var(--eye)) translateZ(10px) rotateX(var(--tx)) rotateY(var(--ty));
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
    0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),0 8px 0 var(--wallb),
    0 22px 30px -12px rgba(0,0,0,.9),
    0 0 34px -8px var(--acc),
    inset 0 1px 0 rgba(255,255,255,.22),inset 0 -1px 0 rgba(0,0,0,.5)
}
@media (hover:hover){
  body.mode-3d .pt-slot:hover{
    transform:perspective(var(--eye)) translateZ(12px) rotateX(calc(var(--tx) * .5)) rotateY(calc(var(--ty) * .15));
    box-shadow:
      inset 3px 0 0 var(--acc),
      0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
      0 5px 0 var(--wallb),0 6px 0 var(--wallb),0 7px 0 var(--wallb),0 8px 0 var(--wallb),0 9px 0 var(--wallb),
      0 26px 32px -16px rgba(0,0,0,.9),
      0 0 30px -10px var(--acc),
      inset 0 1px 0 rgba(255,255,255,.18),inset 0 -1px 0 rgba(0,0,0,.5)
  }
  body.mode-3d .ptp-card:hover,
  body.mode-3d .ptp-card.on:hover{
    transform:perspective(var(--eye)) translateZ(28px) rotateX(var(--tx)) rotateY(var(--ty));
    box-shadow:
      0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wall),
      0 5px 0 var(--wall),0 6px 0 var(--wallb),0 7px 0 var(--wallb),0 8px 0 var(--wallb),
      0 9px 0 var(--wallb),0 10px 0 var(--wallb),0 11px 0 var(--wallb),0 12px 0 var(--wallb),
      0 13px 0 var(--wallb),0 14px 0 var(--wallb),
      0 40px 50px -18px rgba(0,0,0,.92),
      0 0 40px -10px var(--acc),
      inset 0 1px 0 rgba(255,255,255,.2),inset 0 -1px 0 rgba(0,0,0,.5)
  }
}
body.mode-3d .pt-slot:active{
  transform:perspective(var(--eye)) translateZ(1px) translateY(4px);
  box-shadow:inset 3px 0 0 var(--acc),0 1px 0 var(--wall),0 2px 0 var(--wallb),
             0 6px 10px -5px rgba(0,0,0,.8),inset 0 3px 8px rgba(0,0,0,.5)
}
body.mode-3d .ptp-card:active,
body.mode-3d .ptp-card.on:active{
  transform:perspective(var(--eye)) translateZ(2px) translateY(4px);
  box-shadow:0 1px 0 var(--wall),0 2px 0 var(--wallb),0 6px 10px -5px rgba(0,0,0,.8),
             inset 0 3px 8px rgba(0,0,0,.5)
}
/* Parts stand off the face, the art furthest — the parallax is the depth cue. */
body.mode-3d .pt-slot-ico{transform:translateZ(18px)}
body.mode-3d .pt-slot-n,
body.mode-3d .pt-slot-txt,
body.mode-3d .pt-slot-go{transform:translateZ(8px)}
body.mode-3d .ptp-ico{transform:translateZ(24px)}
body.mode-3d .ptp-badge{transform:translateX(-50%) translateZ(16px)}
body.mode-3d .ptp-name,
body.mode-3d .ptp-desc,
body.mode-3d .ptp-pills{transform:translateZ(9px)}
/* The modal slab's walls, restated with the picker's gold glow: the tint rule
   above would otherwise leave this the one flat modal in the 3D interface. */
body.mode-3d .ptpick-tint .fb-modal{
  box-shadow:
    0 2px 0 #131a2e,0 4px 0 #131a2e,0 6px 0 #080c18,0 8px 0 #080c18,0 10px 0 #05070f,
    0 44px 70px -22px rgba(0,0,0,.95),0 0 60px -10px rgba(255,215,0,.26),
    inset 0 1px 0 rgba(255,255,255,.16),inset 0 -1px 0 rgba(0,0,0,.6);
}
@media (max-width:560px){
  body.mode-3d .pt-slot-ico{transform:translateZ(12px)}
  body.mode-3d .ptp-ico{transform:translateZ(16px)}
}
/* Reduced motion (html.reduced-motion follows the OS until the player sets
   it): the solids stay, the travel goes. Hover changes the light, not the
   position — and the pointer tilt stops too, which the parallax code cannot
   do itself: it reads the OS preference once, at load. */
html.reduced-motion body:not(.mode-3d) .pt-slot:hover,
html.reduced-motion body:not(.mode-3d) .ptp-card:hover{transform:none}
html.reduced-motion body.mode-3d .pt-slot:hover{transform:perspective(var(--eye)) translateZ(0)}
html.reduced-motion body.mode-3d .ptp-card:hover{transform:perspective(var(--eye)) translateZ(0)}
html.reduced-motion body.mode-3d .ptp-card.on:hover{transform:perspective(var(--eye)) translateZ(10px)}
html.reduced-motion .pt-slot,
html.reduced-motion .ptp-card{--tx:0deg !important;--ty:0deg !important}

/* ══════════════════════════════════════════════════════════════
   § v48 — 🌍 WORLD THEMES
   ══════════════════════════════════════════════════════════════ */
.shop-sky{
  width:100%;height:74px;border-radius:10px;margin-bottom:10px;position:relative;overflow:hidden;
  border:1px solid rgba(255,255,255,.08);box-shadow:inset 0 -18px 30px rgba(0,0,0,.45)
}
.shop-sky span{position:absolute;right:10px;bottom:6px;font-size:1.7rem;filter:drop-shadow(0 0 8px rgba(0,0,0,.6))}
/* A horizon glow line, so the sunset reads as a sunset rather than a gradient. */
.shop-sky::after{content:'';position:absolute;left:0;right:0;top:62%;height:2px;background:rgba(255,255,255,.35);filter:blur(1px)}
.wld-note{border-color:rgba(0,245,255,.4)!important;background:rgba(0,245,255,.05)!important;color:var(--cyan)!important}

/* ══════════════════════════════════════════════════════════════
   § v49 — 📖 CAMPAIGN · ROOT ACCESS
   Terminal green and gold: ARC's console. Same furniture as the party and
   anomaly screens (.mp-banner, .fb-overlay/.fb-modal); only the accents and
   the chapter map are new. Colours are palette vars, so cb-safe repoints them.
   ══════════════════════════════════════════════════════════════ */
.cp-banner{border-color:rgba(57,255,20,.42);background:linear-gradient(120deg,rgba(57,255,20,.11),rgba(0,245,255,.07))}
.cp-banner .mp-banner-title{background:linear-gradient(90deg,var(--lime),var(--cyan) 60%,var(--gold));-webkit-background-clip:text;background-clip:text}
.campaign-tint .fb-modal,.campaign-modal{max-width:620px}
.campaign-tint .fb-modal{border-color:rgba(57,255,20,.45);box-shadow:0 0 70px rgba(57,255,20,.14),inset 0 0 0 1px rgba(255,255,255,.03)}
.campaign-tint .fb-head{margin-bottom:18px}
#campaign-body{display:flex;flex-direction:column;text-align:left}

/* ── the selected chapter: ARC's briefing ── */
.cp-detail{
  position:relative;overflow:hidden;padding:16px 16px 14px;margin-bottom:4px;border-radius:14px;
  border:1px solid rgba(57,255,20,.34);
  background:linear-gradient(160deg,rgba(57,255,20,.08),rgba(0,245,255,.03) 60%),var(--s1);
  box-shadow:0 0 28px -14px var(--lime)
}
/* CRT scanlines, faint enough to read as texture rather than noise. */
.cp-detail::before{
  content:'';position:absolute;inset:0;pointer-events:none;
  background:repeating-linear-gradient(0deg,rgba(57,255,20,.035) 0 1px,transparent 1px 3px)
}
.cp-detail > *{position:relative}
.cp-detail.locked{border-color:var(--border);background:var(--s1);box-shadow:none}
.cp-detail.locked .cp-title,.cp-detail.locked .cp-story{opacity:.72}
.cp-kicker{font-family:var(--fd);font-size:.5rem;font-weight:700;letter-spacing:.2em;line-height:1.6;color:var(--lime)}
.cp-detail.locked .cp-kicker{color:var(--dim)}
.cp-title{font-family:var(--fd);font-size:1.2rem;font-weight:900;letter-spacing:.06em;color:#fff;margin:7px 0 11px;overflow-wrap:anywhere}
.cp-story{
  font-size:.95rem;line-height:1.55;color:rgba(255,255,255,.82);margin-bottom:12px;
  padding-left:12px;border-left:2px solid rgba(57,255,20,.5)
}
.cp-story em{
  font-style:normal;font-family:var(--fd);font-size:.52rem;font-weight:700;letter-spacing:.14em;
  color:var(--lime);margin-left:4px;white-space:nowrap
}
.cp-facts{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
.cp-facts span{
  font-family:var(--fd);font-size:.5rem;font-weight:700;letter-spacing:.08em;line-height:1.3;
  padding:5px 10px;border-radius:999px;border:1px solid var(--border);
  background:rgba(255,255,255,.05);color:var(--text)
}
.cp-targets{font-family:var(--fd);font-size:.56rem;letter-spacing:.05em;line-height:1.7;color:var(--gold);text-align:center;text-wrap:balance;margin:4px 0 2px}
.cp-have{font-size:2.1rem;line-height:1.1;letter-spacing:.18em;color:var(--gold);text-align:center;text-shadow:0 0 16px rgba(255,215,0,.45);margin:2px 0 12px}
.cp-lock{
  font-family:var(--fd);font-size:.58rem;font-weight:700;letter-spacing:.1em;line-height:1.6;text-align:center;
  color:var(--orange);padding:11px 12px;border-radius:10px;
  border:1px dashed rgba(255,102,0,.45);background:rgba(255,102,0,.06)
}

/* ── the map: four acts of five ── */
.cp-act{
  display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  margin:18px 2px 8px;padding-bottom:6px;border-bottom:1px solid rgba(57,255,20,.22);
  font-family:var(--fd);font-size:.58rem;font-weight:700;letter-spacing:.16em;color:var(--lime)
}
.cp-act em{font-style:normal;letter-spacing:.06em;color:var(--gold);white-space:nowrap}
.cp-act.shut{color:var(--dimmer);border-bottom-color:var(--border)}
.cp-act.shut em{color:var(--dim)}
.cp-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px}
.cp-node{
  position:relative;display:flex;flex-direction:column;align-items:center;gap:3px;min-width:0;
  padding:12px 4px 8px;border-radius:12px;border:1px solid var(--border);background:var(--s1);
  color:var(--text);font-family:var(--fb);cursor:pointer;
  transition:border-color .16s,background .16s,transform .16s
}
.cp-node:hover{border-color:var(--lime);transform:translateY(-2px)}
.cp-node:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
.cp-node.done{border-color:rgba(57,255,20,.42)}
.cp-node.locked{border-style:dashed}
.cp-node.locked:hover{border-color:var(--border2)}
.cp-node.sel{border-style:solid;border-color:var(--gold);background:rgba(255,215,0,.08);box-shadow:0 0 16px -6px var(--gold)}
.cp-n{position:absolute;top:5px;left:8px;font-family:var(--fd);font-size:.5rem;font-weight:700;color:var(--dim)}
.cp-node.done .cp-n{color:var(--lime)}
.cp-ico{font-size:1.5rem;line-height:1;margin-top:6px}
.cp-nt{
  font-family:var(--fd);font-size:.5rem;font-weight:700;letter-spacing:.04em;line-height:1.3;
  text-align:center;min-height:2.6em;overflow-wrap:anywhere;padding:0 2px
}
.cp-st{font-size:.74rem;letter-spacing:.06em;color:var(--gold)}
.cp-node:not(.done) .cp-st{color:var(--dimmer)}
.cp-node.locked .cp-ico,.cp-node.locked .cp-nt,.cp-node.locked .cp-st{opacity:.5}
@media (max-width:520px){
  .cp-grid{grid-template-columns:repeat(3,minmax(0,1fr))}
  .campaign-tint .fb-modal{padding:24px 16px 20px}
  .cp-title{font-size:1.02rem}
  .cp-have{font-size:1.8rem}
}

/* ── 🧊 in the 3D chrome: each chapter is a plate, like a leaderboard row ── */
body.mode-3d .cp-node{
  --wall:#121829;--wallb:#080b16;
  background-image:linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,0) 60%);
  transform:perspective(760px) translateZ(0);
  transition:transform .16s var(--pop-ease),box-shadow .16s var(--pop-ease),border-color .16s;
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wallb),
    0 7px 10px -6px rgba(0,0,0,.8),
    inset 0 1px 0 rgba(255,255,255,.1)
}
body.mode-3d .cp-node:hover{
  transform:perspective(760px) translateZ(8px) translateY(-2px);
  box-shadow:
    0 1px 0 var(--wall),0 2px 0 var(--wall),0 3px 0 var(--wall),0 4px 0 var(--wallb),
    0 5px 0 var(--wallb),0 14px 18px -8px rgba(0,0,0,.85),
    inset 0 1px 0 rgba(255,255,255,.14)
}
body.mode-3d .cp-node.done{--wall:#0f4a12;--wallb:#062308}
body.mode-3d .cp-node.sel{--wall:#6b5600;--wallb:#3a2f00}
body.mode-3d .cp-node.locked{--wall:#0d1120;--wallb:#05070f}
