/* ═══════════════════════════════════════════════════════════════════════════
   NEXAVERSE — platform stylesheet
   Deep black foundation · controlled glow · light cutting through darkness
   ═══════════════════════════════════════════════════════════════════════════ */

@font-face {
  font-family: 'Space Grotesk';
  src: url('../assets/fonts/space-grotesk-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-display: swap;
}

:root {
  /* surfaces */
  --void: #050507;
  --black: #0B0B0E;
  --surface: #101014;
  --surface-2: #16161C;
  --raised: #1C1C24;
  --line: #22222C;
  --line-soft: #1A1A22;

  /* text */
  --text: #EDEAE4;
  --text-2: #A6A6B4;
  --text-3: #6E6E7C;

  /* accents */
  --lavender: #C8B4E4;
  --gold: #EFDCA3;
  --ice: #9FE8F5;
  --danger: #F07A7A;
  --good: #8FE0A8;
  --warn: #E9B96B;

  /* geometry */
  --r-sm: 6px;
  --r: 10px;
  --r-lg: 16px;
  --sidebar: 232px;
  --gap: 20px;

  --shadow: 0 12px 40px rgba(0, 0, 0, .55);
  --shadow-sm: 0 4px 16px rgba(0, 0, 0, .4);
  --glow: 0 0 0 1px rgba(200, 180, 228, .18), 0 8px 32px rgba(200, 180, 228, .08);

  --ease: cubic-bezier(.22, .61, .36, 1);
  --fast: 140ms var(--ease);
  --slow: 320ms var(--ease);

  --text-scale: 1;

  /* ── the device, as four numbers ─────────────────────────────────────────
     `viewport-fit=cover` (index.html) puts the page under the notch, the
     rounded corners and the home indicator, which is what a full-bleed 3D
     world wants. It also means every control anchored to an edge has to know
     how much of that edge the hardware has taken. These are that measurement,
     named once: a rule inserts `var(--safe-b)` rather than guessing 34px, and
     is correct on a device that has never existed.

     A fallback of 0px is not decoration — `env()` with no fallback resolves to
     nothing inside a `calc()` on a browser that does not support it, which
     invalidates the whole declaration. */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);

  /* The smallest a control may be and still be hittable with a thumb. Every
     platform's guidance lands within a pixel or two of this; gameplay buttons
     are deliberately bigger, and nothing is allowed to be smaller. */
  --tap: 48px;

  /* ── the touch control band ──────────────────────────────────────────────
     The size of the on-screen gameplay controls, and the height they reserve
     at the bottom of the screen. Tokens rather than values inside the control
     layer, because things OUTSIDE that layer have to clear the band — the
     chat panel, the emote sheet, the Creator chip — and a reserved size only
     works as a contract if everyone can read it.

     `clamp()` against `vmin` so the controls grow with the screen between a
     floor that is still hittable on the smallest phone and a ceiling that
     stops a tablet drawing dinner plates. The player's own Touch control size
     setting is applied last, so their preference still wins.

     `--touch-scale` is written to this element by TouchControls.applyScale(). */
  --touch-scale: 1;
  --tc-btn: calc(clamp(54px, 13.5vmin, 76px) * var(--touch-scale, 1));
  --tc-gap: clamp(10px, 2.4vmin, 18px);
  /* Must equal the stick travel radius in world3d/touch-controls.js, or the
     ring the thumb sees is not the ring the avatar obeys. Pinned by
     test/unit/touch-controls.test.js. */
  --tc-stick: calc(46px * var(--touch-scale, 1));
  --tc-band: max(calc(var(--tc-btn) * 2.2 + var(--tc-gap)), 156px);
  /* The gap between the controls and the edge of the safe area. It is
     INSIDE the control layer and therefore below the band, so a panel
     clearing only the band lands 12px into the layer — measured at
     390x844 as the chat panel resting 2px on the movement zone. Every
     clearance is band + edge. */
  --tc-edge: 12px;

  /* Written by src/platform/viewport.js: how much of the bottom of the screen
     the software keyboard is covering, and the height that is actually
     visible. Zero on every device without one, so a rule can use them
     unconditionally. */
  --kb-inset: 0px;

  /* ── v1 token aliases ───────────────────────────────────────────────────
     styles/avatar.css and styles/nexus.css were lifted from the v1 stylesheet
     and still reference its names. Those names were never carried across, so
     ~90 declarations across the Avatar Studio and the NEXUS HUD were resolving
     to nothing: panels drew with no background, headings fell back to the system
     font, and selected states lost their fill.

     They are defined as aliases OF the tokens above rather than as fresh
     literals, so there is still one palette — and because a `var()` alias is
     resolved where it is used, the high-contrast overrides below flow through
     these too. */
  --bg1: var(--surface);
  --bg2: var(--surface-2);
  --bg3: var(--raised);
  --line-strong: #2E2E3A;
  --text-dim: var(--text-2);
  --text-faint: var(--text-3);
  --r-md: var(--r);
  --lav: var(--lavender);
  --lav-deep: #6A55C4;
  --ok: var(--good);
  --grad: linear-gradient(135deg, var(--lavender), #F1EBE2, var(--gold));
  --font-display: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;

  color-scheme: dark;
}

/* ── light theme ──────────────────────────────────────────────────────────
   Settings → Interface → Appearance. Written as an override of the token block
   above rather than as a second stylesheet: every panel, button, chip and
   overlay in the app already resolves its colour through these names, so this
   is the whole theme.

   It is not the dark palette inverted. A straight inversion turns the
   lavender/gold accents to mud and leaves text at contrasts that fail WCAG on
   white, so the surfaces are warm off-whites (pure #FFF glares next to this
   much type) and the accents are DARKENED versions of the same hues — the brand
   stays recognisable and body text lands at ~12:1 on the base surface.

   `color-scheme: light` is what re-themes scrollbars, form controls and the
   caret, which no amount of custom-property work will reach. */
:root[data-theme="light"] {
  /* surfaces — warm neutrals, lightest at the base */
  --void: #EFEDE8;
  --black: #F6F4EF;
  --surface: #FFFFFF;
  --surface-2: #F7F5F1;
  --raised: #EDEAE3;
  --line: #D8D4CB;
  --line-soft: #E6E2DA;

  /* text — near-black rather than black, to match the warm surfaces */
  --text: #1A1820;
  --text-2: #55525F;
  --text-3: #7C7887;

  /* accents — same hues, taken down until they carry on white */
  --lavender: #6B4FA8;
  --gold: #8A6D17;
  --ice: #10707F;
  --danger: #B4322F;
  --good: #1F7A45;
  --warn: #8A5A10;

  /* Shadows have to be far softer: the dark theme's 55% black is a smear here. */
  --shadow: 0 10px 32px rgba(26, 24, 32, .14);
  --shadow-sm: 0 3px 12px rgba(26, 24, 32, .10);
  --glow: 0 0 0 1px rgba(107, 79, 168, .22), 0 8px 28px rgba(107, 79, 168, .10);

  --line-strong: #C4BFB4;
  --lav-deep: #4B3585;
  /* The brand gradient is a light wash here; at full strength it is unreadable
     behind the dark text that sits on it. */
  --grad: linear-gradient(135deg, #D9CCF0, #FBF7F0, #F0E2B4);

  color-scheme: light;
}

/* A few places paint their own translucent black over the page — a scrim that
   is correct on a dark app and a bruise on a light one. */
:root[data-theme="light"] .modal-scrim,
:root[data-theme="light"] .scrim { background: rgba(28, 26, 34, .38); }

/* High contrast, applied to the light palette instead of the dark one. */
:root[data-theme="light"][data-high-contrast="true"] {
  --text: #000000;
  --text-2: #2A2833;
  --text-3: #4A4756;
  --line: #8C8879;
  --surface: #FFFFFF;
}

:root[data-high-contrast="true"] {
  --text: #FFFFFF;
  --text-2: #D6D6E0;
  --text-3: #A0A0B0;
  --line: #4A4A5C;
  --surface: #08080B;
}
:root[data-reduce-motion="true"] * {
  animation-duration: .001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
  scroll-behavior: auto !important;
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--void);
  color: var(--text);
  font-family: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: calc(15px * var(--text-scale));
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
  overflow-x: clip;
}
body.in-game { overflow: hidden; }

/* ═══ selection and touch policy ══════════════════════════════════════════
   Why this section exists
   ───────────────────────
   NEXAVERSE had no selection policy at all: `user-select` computed to `auto`
   on every element from `<body>` down, `-webkit-touch-callout` was never set,
   and `touch-action` was declared only on the three canvases that knew to ask
   for it. On a phone that is not a neutral default — it is a document. A held
   thumb over the NEXUS world put the iOS text magnifier on screen; a long
   press on a HUD chip or a game card raised Copy / Paste / Share; and a
   double- or triple-tap on any button label — which is what rapid gameplay
   tapping IS to the browser — selected the label and left it highlighted.

   The policy, in four layers, in this order
   ────────────────────────────────────────
     A. the application is chrome, not prose      → nothing selects
     B. a pointing device has no long press       → prose selects again
     C. interaction surfaces are never text       → back to nothing
     D. text-editing and opt-in regions           → always select

   Every selector below is wrapped in `:where()`, so all four layers have a
   specificity of ZERO and the cascade between them is decided purely by the
   order above — read top to bottom, last match wins. That matters more than it
   sounds: a `<p>` inside a `.btn`, an `<input>` inside `.hud`, and a chat
   composer inside `.touch-controls` each match two of these layers, and the
   right answer is always the later one. Zero specificity also means any
   component that has already made its own decision (`.touch-controls` in
   nexus.css, say) keeps it.

   What is deliberately NOT here: `user-select: none` on `<body>` with no way
   back. Layer D is what keeps sign-in, the invite code, chat and every field
   in the Creator Hub fully editable — cursor placement, selection, copy and
   paste — on every device. */

/* ── A. the shell is an application ─────────────────────────────────────── */
:root {
  -webkit-user-select: none;
  user-select: none;
  /* WebKit's long-press sheet (Copy / Look Up / Share, and the magnifier that
     comes with it). Inherited, so declaring it once here reaches everything,
     and layer D hands it back where a paste menu is the point. */
  -webkit-touch-callout: none;
  /* The grey flash box a tap paints over a link or button on Android. Buttons
     get a real pressed state below instead, which is feedback the flash box
     never was. */
  -webkit-tap-highlight-color: transparent;
}

/* ── B. a mouse has no long press ───────────────────────────────────────── */
/* Selection on a desktop is deliberate — you have to press and drag across the
   words — and copying a game description, an error message or a handle is a
   thing people legitimately do. There is no callout and no magnifier to
   protect anyone from, so prose goes back to selecting exactly as it did.
   Coarse pointers get nothing here, which is the whole fix. */
@media (hover: hover) and (pointer: fine) {
  :where(p, li, dd, dt, td, th, pre, code, kbd, blockquote, figcaption,
         h1, h2, h3, h4, h5, h6, small, time, address) {
    -webkit-user-select: text;
    user-select: text;
  }
}

/* ── C. interaction surfaces are never text ─────────────────────────────── */
/* Anything you press, drag, aim or steer with — and the labels inside it. A
   button's label is part of the button, not a paragraph that happens to be
   sitting on one, so the descendant reach is intentional. */
:where(
  button, a, label, summary,
  [role="button"], [role="tab"], [role="menuitem"], [role="option"],
  [role="switch"], [role="slider"], [role="checkbox"], [role="radio"],
  canvas, img, svg, video,
  /* the 2D catalog, the 3D worlds and NEXUS */
  .game-layer, .game-shell, .game-canvas, .game-hud, .world-root, .world-mount,
  .world-canvas, .nexus-root, .nexus-canvas,
  /* every HUD, every on-screen control — the 3D worlds' `.touch-controls`
     and the 2D catalog's `.arcade-controls`, whose button captions ("JUMP",
     "FIRE") are the text a fast tapping thumb would otherwise select */
  .hud, .touch-controls, .tc-look, .tc-move, .tc-actions,
  .arcade-controls, .ac-surface, .ac-move, .ac-lanes, .ac-actions,
  /* platform chrome. Deliberately NOT `.rail`: a rail is a scroller with a
     heading on it, not a control, and taking its whole subtree would take the
     section headings with it. Its cards are links and are covered above. */
  .sidebar, .side-nav, .topbar, .tabs, .modal-head, .toast
),
:where(
  button, a, label, summary,
  [role="button"], [role="tab"], [role="menuitem"], [role="option"],
  [role="switch"], [role="slider"], [role="checkbox"], [role="radio"],
  .game-layer, .game-shell, .game-hud, .world-root, .world-mount,
  .nexus-root, .hud, .touch-controls, .arcade-controls, .sidebar, .side-nav, .topbar,
  .tabs, .modal-head, .toast
) * {
  -webkit-user-select: none;
  user-select: none;
}

/* A tap on a control is a tap, not a candidate double-tap-to-zoom. Removing
   that ambiguity is also what removes the ~300ms the browser spends waiting to
   find out, which is why rapid tapping feels immediate afterwards. `.tc-btn`
   and the canvases keep their own stricter `touch-action: none` — these rules
   carry no specificity and cannot overrule them. */
:where(button, a, label, summary, [role="button"], [role="tab"], [role="switch"]) {
  touch-action: manipulation;
}

/* Decoration should not be draggable, and a held finger on it should not offer
   to save or share it. */
:where(img, svg, video) {
  -webkit-user-drag: none;
}

/* With the tap flash gone, a press has to be visible some other way — on touch
   it was the ONLY feedback these controls had. A brightness step rather than a
   background change, so it reads the same on every one of them without any of
   them needing a rule of its own, and it costs no layout. */
:where(.btn, .hud-btn, .hud-chip, .chat-send, .chat-toggle):active:not(:disabled) {
  filter: brightness(1.25);
}

/* ── D. text editing, and anything that opts in ─────────────────────────── */
/* Last, so it wins wherever a field sits inside a surface layer C just made
   inert — a chat composer in a HUD, a search box in the sidebar, the handle
   and password fields on a sign-in card. Everything a text field needs is
   restored explicitly: the caret, the selection, and the callout that carries
   Paste. `[contenteditable="false"]` is excluded — that is a read-only region
   wearing an editing attribute, not a field.

   `.selectable` / `[data-selectable]` is the opt-in for read-only text that is
   there to be copied — a content hash, a transaction id, an invite code, a
   chat log. Add the class rather than reaching for `!important`. */
:where(
  input, textarea, select,
  [contenteditable]:not([contenteditable="false"]),
  .selectable, [data-selectable]
) {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.015em; line-height: 1.2; }
h1 { font-size: 1.9rem; }
h2 { font-size: 1.12rem; }
h3 { font-size: .95rem; }
p { margin: 0 0 .7em; }
a { color: inherit; text-decoration: none; }
code, kbd, pre { font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: .86em; }
code { background: var(--surface-2); padding: .1em .4em; border-radius: 4px; color: var(--lavender); }
kbd {
  background: var(--raised); border: 1px solid var(--line); border-bottom-width: 2px;
  border-radius: 5px; padding: .12em .48em; font-size: .8em; color: var(--text);
  display: inline-block; margin: 0 2px;
}
:focus-visible { outline: 2px solid var(--lavender); outline-offset: 2px; border-radius: 4px; }
::selection { background: rgba(200, 180, 228, .28); }
.muted { color: var(--text-2); }
.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ── scrollbars ──────────────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 6px; border: 2px solid var(--void); }
::-webkit-scrollbar-thumb:hover { background: #333340; }

/* ═══ boot & auth ════════════════════════════════════════════════════════ */

.boot {
  height: 100vh; display: grid; place-content: center; justify-items: center; gap: 20px;
  color: var(--text-2); letter-spacing: .3em; font-size: .78rem;
}
.boot-mark {
  width: 48px; height: 48px;
  background: linear-gradient(135deg, var(--lavender), #F1EBE2, var(--gold));
  clip-path: polygon(50% 6%, 92% 50%, 50% 94%, 8% 50%);
  animation: pulse 2s var(--ease) infinite;
}
@keyframes pulse { 0%, 100% { opacity: .5; transform: scale(.94); } 50% { opacity: 1; transform: scale(1); } }
.boot-error { letter-spacing: 0; max-width: 520px; text-align: center; }
.boot-error pre { background: var(--surface); padding: 12px; border-radius: var(--r); overflow-x: auto; max-width: 100%; }
.boot-hint { color: var(--text-3); }

.auth { display: grid; grid-template-columns: 1fr min(480px, 42vw); min-height: 100vh; }
.auth-art { position: relative; overflow: hidden; background: var(--void); }
.auth-grid {
  position: absolute; inset: -20%;
  background-image: linear-gradient(rgba(200,180,228,.06) 1px, transparent 1px),
                    linear-gradient(90deg, rgba(200,180,228,.06) 1px, transparent 1px);
  background-size: 64px 64px;
  transform: perspective(700px) rotateX(56deg);
  mask-image: radial-gradient(ellipse at 50% 30%, #000 20%, transparent 72%);
}
.auth-glow {
  position: absolute; left: 50%; top: 38%; width: 60vw; height: 60vw; translate: -50% -50%;
  background: radial-gradient(circle, rgba(200,180,228,.16), transparent 62%);
  filter: blur(30px);
}
.auth-panel { background: var(--black); border-left: 1px solid var(--line); padding: 48px 44px; overflow-y: auto; }
.auth-brand { margin-bottom: 40px; }
.auth-panel h1 { font-size: 1.7rem; margin-bottom: 8px; }
.auth-sub { color: var(--text-2); margin-bottom: 28px; }
.auth-tabs { display: flex; gap: 4px; background: var(--surface); padding: 4px; border-radius: var(--r); margin-bottom: 22px; }
.auth-tabs button {
  flex: 1; background: none; border: 0; color: var(--text-2); padding: 9px; border-radius: var(--r-sm);
  font: inherit; font-weight: 600; cursor: pointer; transition: var(--fast);
}
.auth-tabs button[aria-selected="true"] { background: var(--raised); color: var(--text); }
.auth-form { display: flex; flex-direction: column; }
.auth-divider { display: flex; align-items: center; gap: 12px; color: var(--text-3); margin: 22px 0; font-size: .82rem; }
.auth-divider::before, .auth-divider::after { content: ''; flex: 1; height: 1px; background: var(--line); }
.auth-guest { width: 100%; }
.auth-dev {
  margin-top: 30px; padding: 14px; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); display: flex; gap: 12px; font-size: .82rem; color: var(--text-2);
}
.auth-dev svg { width: 20px; height: 20px; flex: none; color: var(--warn); }


/* ── account recovery ─────────────────────────────────────────────────────
   Forgot-password and reset-password reuse the sign-in panel wholesale: same
   grid, same art, same field styles. What is added here is only what those
   two screens need and nothing else has — a link that sits on a label row, a
   result note, a show/hide affordance and a live requirements list. */

/* "Forgot password?" sits on the same line as the Password label, right-
   aligned, which is where every product that has one puts it. A BUTTON rather
   than an anchor because it changes a view and navigates nowhere; it is styled
   to read as a link and keeps a real focus ring. */
.field-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.field-label-row .field-label { margin-bottom: 5px; }
.link-btn {
  background: none; border: 0; padding: 2px 2px; margin: 0;
  font: inherit; font-size: .76rem; font-weight: 600;
  color: var(--lavender); cursor: pointer; border-radius: 5px;
}
.link-btn:hover { color: var(--text); text-decoration: underline; }
.link-btn:focus-visible { outline: 2px solid var(--lavender); outline-offset: 2px; }
.field-optional { font-weight: 500; color: var(--text-3); text-transform: none; }

/* A result panel — sent, reset, or a dead link. `align-items: flex-start` so
   the icon stays beside the first line as the prose wraps on a phone. */
.auth-note {
  margin: 18px 0; padding: 14px 16px; border-radius: var(--r);
  display: flex; align-items: flex-start; gap: 12px;
  background: var(--surface); border: 1px solid var(--line);
  font-size: .86rem; color: var(--text-2);
}
.auth-note b { color: var(--text); display: block; margin-bottom: 4px; }
.auth-note p { margin: 0 0 6px; line-height: 1.55; }
.auth-note p:last-child { margin-bottom: 0; }
.auth-note-icon { flex: none; display: block; width: 20px; height: 20px; margin-top: 1px; }
.auth-note-icon svg { width: 20px; height: 20px; }
.auth-note-ok { border-color: rgba(143,224,168,.32); background: rgba(143,224,168,.07); }
.auth-note-ok .auth-note-icon { color: var(--good); }
.auth-note-warn { border-color: rgba(233,185,107,.32); background: rgba(233,185,107,.07); }
.auth-note-warn .auth-note-icon { color: var(--warn); }
.auth-back { width: 100%; margin-top: 12px; justify-content: center; }

/* The reset screen has no second column of its own content, so it borrows the
   sign-in layout unchanged. Named rather than reusing `.auth` alone so the
   two can diverge later without one editing the other by accident. */
.auth-solo .auth-panel { display: flex; flex-direction: column; }

/* Show / hide. The button is positioned inside the field's own box, and the
   input is padded so long text never runs under it. 44px of touch target on
   both axes, which is the floor a thumb needs. */
.field-with-toggle { position: relative; display: block; }
.field-with-toggle .field { width: 100%; padding-right: 46px; }
.field-toggle {
  position: absolute; top: 50%; right: 4px; transform: translateY(-50%);
  width: 40px; height: 40px; display: grid; place-items: center;
  background: none; border: 0; border-radius: 8px;
  color: var(--text-3); cursor: pointer;
}
.field-toggle:hover { color: var(--text); }
.field-toggle:focus-visible { outline: 2px solid var(--lavender); outline-offset: -2px; }
.field-toggle svg { width: 18px; height: 18px; }

/* The live requirements list. The tick is dim until the rule passes, so the
   list reads as progress rather than as a wall of errors on an empty form. */
.pw-rules { list-style: none; margin: 14px 0 4px; padding: 0; display: grid; gap: 6px; }
.pw-rules li {
  display: flex; align-items: center; gap: 8px;
  font-size: .8rem; color: var(--text-3); transition: color .15s ease;
}
.pw-rules li.ok { color: var(--good); }
.pw-tick { display: grid; place-items: center; width: 16px; height: 16px; opacity: .35; }
.pw-tick svg { width: 14px; height: 14px; }
.pw-rules li.ok .pw-tick { opacity: 1; }

/* A button mid-request. Disabled already dims it; this stops the label
   shifting the layout as it changes length. */
.btn.is-busy { cursor: progress; }

/* ── the recovery email panel in Settings ────────────────────────────────── */
.recovery-state { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 4px 0 16px; }
.recovery-state code { font-size: .84rem; color: var(--text-2); word-break: break-all; }
.pill {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
  border-radius: 999px; font-size: .76rem; font-weight: 600; border: 1px solid var(--line);
}
.pill svg { width: 14px; height: 14px; }
.pill-ok { color: var(--good); border-color: rgba(143,224,168,.34); background: rgba(143,224,168,.08); }
.pill-warn { color: var(--warn); border-color: rgba(233,185,107,.34); background: rgba(233,185,107,.08); }
/* Two columns on a desktop, one on anything narrow. `minmax(0, …)` so a long
   address cannot push the grid wider than its container. */
.recovery-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); gap: 8px 18px; }
.recovery-form .field { width: 100%; }
.recovery-form button[type=submit] { margin-top: 14px; }

@media (max-width: 860px) {
  .auth { grid-template-columns: 1fr; }
  .auth-art { display: none; }
  .auth-panel { border-left: 0; padding: 32px 22px; }
}
/* A phone in portrait, and a tablet held the same way. The panel keeps its
   padding but the recovery screens are single-column everywhere below this. */
@media (max-width: 520px) {
  .auth-panel { padding: 26px 18px 34px; }
  .auth-panel h1 { font-size: 1.45rem; }
  .field-label-row { flex-wrap: wrap; gap: 2px 12px; }
}

/* ═══ shell ══════════════════════════════════════════════════════════════ */

/* A short screen — a phone held sideways — has no height to spare, so the
   controls and the band they reserve shrink together. Every rule that clears
   the band is written in terms of it, so this one declaration moves them all. */
@media (max-height: 500px) {
  :root {
    --tc-btn: calc(clamp(46px, 17vmin, 60px) * var(--touch-scale, 1));
    --tc-stick: calc(38px * var(--touch-scale, 1));
    --tc-band: max(calc(var(--tc-btn) * 1.3), 116px);
  }
}

.env-banner {
  background: linear-gradient(90deg, rgba(233,185,107,.14), rgba(233,185,107,.05));
  border-bottom: 1px solid rgba(233,185,107,.28);
  color: var(--warn); font-size: .74rem; letter-spacing: .04em;
  padding: 6px 16px; text-align: center;
}
.shell { display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); min-height: 100vh; }

.sidebar {
  position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column;
  background: var(--black); border-right: 1px solid var(--line); padding: 20px 12px;
}
.side-logo { display: block; padding: 4px 10px 22px; }
.vb-lockup { display: inline-flex; align-items: center; gap: 9px; }
.vb-word { font-size: .82rem; font-weight: 500; letter-spacing: .19em; color: var(--text-2); }
.vb-word b { font-weight: 700; color: var(--text); }
.side-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; overflow-y: auto; }
.side-nav a {
  display: flex; align-items: center; gap: 11px; padding: 9px 11px; border-radius: var(--r-sm);
  color: var(--text-2); font-size: .89rem; font-weight: 500; transition: var(--fast); position: relative;
}
.side-nav a svg { width: 18px; height: 18px; flex: none; }
.side-nav a:hover { background: var(--surface); color: var(--text); }
.side-nav a.active { background: var(--surface-2); color: var(--text); }
.side-nav a.active::before {
  content: ''; position: absolute; left: -12px; top: 50%; translate: 0 -50%;
  width: 3px; height: 18px; border-radius: 0 3px 3px 0;
  background: linear-gradient(var(--lavender), var(--gold));
}
.nav-badge {
  margin-left: auto; background: var(--danger); color: #170B0B; font-size: .64rem; font-weight: 700;
  min-width: 17px; height: 17px; border-radius: 9px; display: grid; place-items: center; padding: 0 5px;
}
/* `display` on a utility class outranks the UA `[hidden]` rule — restore it. */
[hidden] { display: none !important; }
.side-foot { display: flex; flex-direction: column; gap: 6px; padding-top: 12px; border-top: 1px solid var(--line); }
.vc-chip, .me-chip {
  display: flex; align-items: center; gap: 9px; padding: 8px 11px; border-radius: var(--r-sm);
  background: var(--surface); transition: var(--fast); font-size: .84rem;
}
.vc-chip:hover, .me-chip:hover { background: var(--surface-2); }
/* Sized and rounded so the equipped frame (.cos-frame) has an edge to draw on;
   the mark itself keeps its own size. */
.me-avatar { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 7px; flex: none; }
.me-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.me-name small { color: var(--text-3); font-weight: 400; }
.me-role {
  margin-left: auto; font-size: .6rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--gold); border: 1px solid rgba(239,220,163,.32); padding: 1px 5px; border-radius: 4px;
}
.vc-amount { display: inline-flex; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
.vc-amount svg { width: 16px; height: 16px; flex: none; }
.vc-amount b { font-weight: 600; }

.page { padding: 30px 34px 80px; max-width: 1560px; width: 100%; min-width: 0; outline: none; }
.page-head {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 22px;
  flex-wrap: wrap; margin-bottom: 26px;
}
.page-sub { color: var(--text-2); margin: 5px 0 0; font-size: .9rem; }
.head-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.page-loading, .page-error { padding: 60px 0; text-align: center; color: var(--text-2); }
.page-error h2 { margin-bottom: 8px; color: var(--text); }
.back-link { display: inline-flex; align-items: center; gap: 5px; color: var(--text-2); font-size: .85rem; margin-bottom: 14px; }
.back-link svg { width: 15px; height: 15px; }
.back-link:hover { color: var(--text); }
.sub-head { margin: 18px 0 8px; color: var(--text-2); font-size: .8rem; text-transform: uppercase; letter-spacing: .09em; }

@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar {
    position: fixed; bottom: 0; top: auto; left: 0; right: 0; height: auto; z-index: 40;
    flex-direction: row; border-right: 0; border-top: 1px solid var(--line);
    /* The home indicator lives in the bottom ~34px of the screen and swallows
       a tap meant for whatever is drawn there. The bar keeps its own 6px and
       adds the inset beneath it. */
    padding: 6px calc(6px + var(--safe-r)) calc(6px + var(--safe-b)) calc(6px + var(--safe-l));
  }
  .side-nav a { min-height: var(--tap); justify-content: center; }
  .side-logo, .side-foot { display: none; }
  .side-nav { flex-direction: row; overflow-x: auto; gap: 0; }
  .side-nav a { flex-direction: column; gap: 3px; font-size: .62rem; padding: 7px 10px; min-width: 60px; }
  .side-nav a.active::before { left: 50%; top: auto; bottom: -6px; translate: -50% 0; width: 18px; height: 3px; border-radius: 3px 3px 0 0; }
  /* Clear the bar AND the indicator under it, or the last row of any page is
     unreachable. */
  .page { padding: 20px calc(16px + var(--safe-r)) calc(96px + var(--safe-b)) calc(16px + var(--safe-l)); }
}

/* ═══ buttons & fields ═══════════════════════════════════════════════════ */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  background: var(--raised); color: var(--text); border: 1px solid var(--line);
  padding: 9px 15px; border-radius: var(--r-sm); font: inherit; font-size: .87rem; font-weight: 600;
  cursor: pointer; transition: var(--fast); white-space: nowrap;
}
.btn svg { width: 16px; height: 16px; flex: none; }
.btn:hover:not(:disabled) { background: #24242E; border-color: #33333F; transform: translateY(-1px); }
.btn:active:not(:disabled) { transform: translateY(0); }
.btn:disabled { opacity: .42; cursor: not-allowed; }
.btn-primary {
  background: linear-gradient(135deg, #E3D6F2, #F1EBE2 55%, #EFDCA3);
  color: #0B0B0E; border-color: transparent; font-weight: 700;
}
.btn-primary:hover:not(:disabled) { background: linear-gradient(135deg, #EDE3F8, #FFFFFF 55%, #F5E7BC); }
.btn-danger { background: rgba(240,122,122,.12); border-color: rgba(240,122,122,.34); color: #FFBCBC; }
.btn-danger:hover:not(:disabled) { background: rgba(240,122,122,.2); }
.btn-ghost { background: transparent; border-color: var(--line); color: var(--text-2); }
.btn-ghost:hover:not(:disabled) { color: var(--text); }
.btn-small { padding: 6px 11px; font-size: .8rem; }
.btn-lg { padding: 12px 22px; font-size: .95rem; }
.icon-btn {
  display: grid; place-items: center; width: 34px; height: 34px; border-radius: var(--r-sm);
  background: transparent; border: 1px solid transparent; color: var(--text-2); cursor: pointer;
  transition: var(--fast); padding: 0;
}
.icon-btn svg { width: 18px; height: 18px; }
.icon-btn:hover { background: var(--surface-2); color: var(--text); }

.field {
  width: 100%; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--text); padding: 9px 12px; font: inherit; font-size: .89rem; transition: var(--fast);
}
.field:focus { outline: none; border-color: var(--lavender); box-shadow: 0 0 0 3px rgba(200,180,228,.13); }
.field::placeholder { color: var(--text-3); }
textarea.field { resize: vertical; min-height: 76px; line-height: 1.5; }
select.field { cursor: pointer; }
input[type=color].field { padding: 3px; height: 38px; cursor: pointer; }
.field-label { display: block; font-size: .78rem; font-weight: 600; color: var(--text-2); margin: 14px 0 5px; }
.field-label:first-child { margin-top: 0; }
.field-hint { font-size: .76rem; color: var(--text-3); margin: 5px 0 0; }
.field-error { font-size: .78rem; color: var(--danger); margin: 5px 0 0; min-height: 1px; }
.field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.inline-form { display: flex; gap: 7px; margin-top: 10px; }
/* A grid, not a flex row. The label's trailing `<em>` aside is a third item,
   and as flex children the three of them split into two narrow columns of
   interleaved text on a phone — the confirmation and its footnote read as one
   broken sentence. Two columns and two rows says what is actually meant: the
   box, the label beside it, and the aside underneath the label. */
.check-row {
  display: grid; grid-template-columns: auto 1fr; gap: 3px 9px;
  align-items: start; padding: 6px 0; cursor: pointer; font-size: .87rem;
}
.check-row input { margin-top: 3px; accent-color: var(--lavender); grid-row: 1; }
.check-row em { grid-column: 2; color: var(--text-3); font-size: .82rem; font-style: italic; }
/* A checkbox is 13px of hit area in a dialog a player has to consent through.
   The whole row is the target on a finger. */
@media (pointer: coarse) { .check-row { min-height: var(--tap); align-items: center; } }
.code-input { text-align: center; letter-spacing: .35em; font-size: 1.3rem; font-weight: 700; }

/* ═══ panels, pills, notices ═════════════════════════════════════════════ */

.panel {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 20px 22px; margin-bottom: var(--gap);
}
.panel h2 { display: flex; align-items: baseline; gap: 9px; margin-bottom: 14px; }
.panel-note { font-size: .74rem; font-weight: 500; color: var(--text-3); letter-spacing: .03em; }
.panel-foot { font-size: .78rem; color: var(--text-3); margin: 14px 0 0; display: flex; gap: 7px; align-items: flex-start; }
.panel-foot svg { width: 15px; height: 15px; flex: none; margin-top: 2px; }
.panel-accent { border-color: rgba(200,180,228,.3); box-shadow: var(--glow); }
.panel-dev { border-color: rgba(233,185,107,.3); background: linear-gradient(var(--surface), rgba(233,185,107,.03)); }
.panel-dev h2 svg { width: 18px; height: 18px; color: var(--warn); }
.panel-head-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 12px; }
.panel-head-row h2 { margin: 0; }

.two-col { display: grid; grid-template-columns: minmax(0, 1.85fr) minmax(0, 1fr); gap: var(--gap); align-items: start; }
@media (max-width: 1080px) { .two-col { grid-template-columns: 1fr; } }

.pill {
  display: inline-flex; align-items: center; font-size: .7rem; font-weight: 600; letter-spacing: .03em;
  padding: 2px 8px; border-radius: 20px; background: var(--raised); color: var(--text-2);
  border: 1px solid var(--line); text-transform: capitalize;
}
.pill-good { background: rgba(143,224,168,.11); border-color: rgba(143,224,168,.3); color: var(--good); }
.pill-warn { background: rgba(233,185,107,.11); border-color: rgba(233,185,107,.3); color: var(--warn); }
.pill-bad { background: rgba(240,122,122,.11); border-color: rgba(240,122,122,.3); color: var(--danger); }
.pill-info { background: rgba(200,180,228,.11); border-color: rgba(200,180,228,.3); color: var(--lavender); }

.notice {
  display: flex; gap: 13px; align-items: flex-start; padding: 13px 16px; border-radius: var(--r);
  background: var(--surface-2); border: 1px solid var(--line); margin-bottom: var(--gap); font-size: .86rem;
}
.notice svg { width: 19px; height: 19px; flex: none; margin-top: 1px; color: var(--lavender); }
.notice-warn { background: rgba(233,185,107,.07); border-color: rgba(233,185,107,.28); }
.notice-warn svg { color: var(--warn); }
.notice-good { background: rgba(143,224,168,.07); border-color: rgba(143,224,168,.28); }
.notice-good svg { color: var(--good); }
/* The operator control plane's stopped/failing state. Louder than `warn`,
   because "paid AI is disabled" is the one banner in the Review Center that
   must not be mistaken for an advisory. */
.notice-danger { background: rgba(240,122,122,.08); border-color: rgba(240,122,122,.3); }
.notice-danger svg { color: var(--danger); }
.notice code { font-size: .8em; }
.notice .btn { margin-top: 8px; }

.empty-state { text-align: center; padding: 44px 20px; color: var(--text-3); }
.empty-state svg { width: 34px; height: 34px; margin-bottom: 12px; opacity: .5; }
.empty-state p { max-width: 460px; margin: 0 auto 14px; }
.card-skeleton {
  height: 250px; border-radius: var(--r-lg);
  background: linear-gradient(100deg, var(--surface) 30%, var(--surface-2) 50%, var(--surface) 70%);
  background-size: 220% 100%; animation: shimmer 1.5s linear infinite;
}
@keyframes shimmer { to { background-position: -220% 0; } }

.honest-list, .err-list, .a11y-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 9px; font-size: .87rem; }
.honest-list li { padding-left: 15px; position: relative; color: var(--text-2); }
.honest-list li::before { content: ''; position: absolute; left: 0; top: .62em; width: 5px; height: 5px; border-radius: 50%; background: var(--lavender); }
.honest-list.negative li::before { background: var(--danger); }
.honest-list b { color: var(--text); }
.err-list li { padding-left: 15px; position: relative; color: var(--text-2); }
.err-list li::before { content: '→'; position: absolute; left: 0; color: var(--warn); }
.a11y-list { flex-direction: row; flex-wrap: wrap; gap: 7px 16px; }
.a11y-list li { display: flex; align-items: center; gap: 6px; font-size: .82rem; color: var(--text-2); }
.a11y-list svg { width: 15px; height: 15px; color: var(--good); }
.a11y-list li:has(svg[viewBox]:not(:first-child)) { color: var(--text-3); }

.fact-list, .stat-list { margin: 0; display: flex; flex-direction: column; gap: 11px; }
.fact-list > div, .stat-list > div { display: grid; grid-template-columns: 130px 1fr; gap: 14px; font-size: .87rem; }
.fact-list.compact > div, .stat-list > div { grid-template-columns: 110px 1fr; }
.fact-list dt, .stat-list dt { color: var(--text-3); font-size: .8rem; }
.fact-list dd, .stat-list dd { margin: 0; color: var(--text); }
.stat-list dd { text-align: right; font-variant-numeric: tabular-nums; }

blockquote {
  margin: 8px 0; padding: 9px 13px; border-left: 2px solid var(--lavender);
  background: var(--surface-2); border-radius: 0 var(--r-sm) var(--r-sm) 0; font-size: .86rem; color: var(--text-2);
}
blockquote cite { display: block; margin-top: 5px; font-size: .74rem; color: var(--text-3); font-style: normal; }

/* ═══ hub ════════════════════════════════════════════════════════════════ */

.hub-head { align-items: center; }
.hub-search { display: flex; align-items: center; gap: 8px; position: relative; }
.hub-search > svg { position: absolute; left: 11px; width: 17px; height: 17px; color: var(--text-3); pointer-events: none; }
.hub-search .field { padding-left: 35px; min-width: 300px; }
.hub-search.compact .field { min-width: 220px; }
.hub-search .btn b {
  background: rgba(11,11,14,.24); border-radius: 10px; padding: 0 6px; font-size: .74rem; margin-left: 2px;
}

.filter-panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 20px; margin-bottom: var(--gap); }
.filter-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); gap: 20px; }
.filter-group h4 { font-size: .74rem; text-transform: uppercase; letter-spacing: .09em; color: var(--text-3); margin-bottom: 9px; }
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  background: var(--raised); border: 1px solid var(--line); color: var(--text-2);
  padding: 5px 11px; border-radius: 20px; font: inherit; font-size: .78rem; font-weight: 500;
  cursor: pointer; transition: var(--fast); text-transform: capitalize;
}
.chip:hover { border-color: #383846; color: var(--text); }
.chip.on { background: rgba(200,180,228,.15); border-color: var(--lavender); color: var(--lavender); }
.filter-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }
.filter-count { font-size: .82rem; color: var(--text-3); }

.results-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; color: var(--text-2); font-size: .86rem; }
.sort-select { display: inline-flex; align-items: center; gap: 7px; font-size: .8rem; color: var(--text-3); }
.sort-select select {
  background: var(--surface); border: 1px solid var(--line); color: var(--text);
  padding: 5px 9px; border-radius: var(--r-sm); font: inherit; font-size: .82rem; cursor: pointer;
}

.hero {
  position: relative; overflow: hidden; border-radius: var(--r-lg); border: 1px solid var(--line);
  background: var(--surface); margin-bottom: 28px; min-height: 260px; display: flex; align-items: flex-end;
}
.hero-art {
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse at 78% 24%, color-mix(in srgb, var(--c2) 40%, transparent), transparent 58%),
    linear-gradient(115deg, var(--c1), var(--void) 72%);
}
.hero-art::after {
  content: ''; position: absolute; inset: 0;
  background-image: linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
                    linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px);
  background-size: 44px 44px;
  mask-image: linear-gradient(90deg, transparent, #000 55%);
}
.hero-body { position: relative; padding: 34px; max-width: 620px; }
.hero-eyebrow { font-size: .68rem; letter-spacing: .18em; color: var(--gold); font-weight: 600; }
.hero-body h2 { font-size: 2.2rem; margin: 8px 0 10px; letter-spacing: -.02em; }
.hero-body p { color: var(--text-2); margin-bottom: 18px; }
.hero-actions { display: flex; gap: 9px; flex-wrap: wrap; }

.rail { margin-bottom: 32px; }
.rail-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 13px; }
.rail-head h2 { font-size: 1.05rem; }
.rail-head p { color: var(--text-3); font-size: .8rem; margin: 3px 0 0; }
.rail-more { display: inline-flex; align-items: center; gap: 3px; color: var(--text-2); font-size: .8rem; font-weight: 600; }
.rail-more svg { width: 14px; height: 14px; }
.rail-more:hover { color: var(--lavender); }
.rail-track {
  display: grid; grid-auto-flow: column; grid-auto-columns: 220px; gap: 14px;
  overflow-x: auto; overscroll-behavior-x: contain; padding-bottom: 8px;
  scroll-snap-type: x proximity;
}
.rail-track > * { scroll-snap-align: start; }
.rail-track.size-compact { grid-auto-columns: 178px; }
.rail-track.size-large { grid-auto-columns: 268px; }

.game-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.game-grid.size-compact { grid-template-columns: repeat(auto-fill, minmax(178px, 1fr)); }
.game-grid.size-large { grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); }

.game-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden; display: flex; flex-direction: column; transition: var(--fast); position: relative;
}
.game-card:hover { border-color: color-mix(in srgb, var(--c2) 45%, var(--line)); transform: translateY(-3px); box-shadow: var(--shadow-sm); }
.card-link { display: flex; flex-direction: column; flex: 1; }
.card-art {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  background: linear-gradient(140deg, var(--c1), var(--void) 78%);
  display: grid; place-items: center;
}
.card-art-glow {
  position: absolute; inset: -30%;
  background: radial-gradient(circle at 68% 28%, color-mix(in srgb, var(--c2) 48%, transparent), transparent 56%);
  filter: blur(14px);
}
.card-art-mark {
  position: relative; font-size: 3.1rem; font-weight: 700; letter-spacing: -.04em;
  color: color-mix(in srgb, var(--c2) 82%, #FFF); opacity: .9;
  text-shadow: 0 0 34px color-mix(in srgb, var(--c2) 55%, transparent);
}
.card-origin {
  position: absolute; top: 9px; right: 9px; font-size: .58rem; letter-spacing: .12em; font-weight: 700;
  background: rgba(11,11,14,.72); border: 1px solid rgba(255,255,255,.14);
  padding: 2px 7px; border-radius: 4px; color: var(--lavender); backdrop-filter: blur(4px);
}
.card-meta { padding: 13px 14px 10px; flex: 1; display: flex; flex-direction: column; gap: 7px; }
.card-meta h3 { font-size: .98rem; }
.card-desc {
  color: var(--text-2); font-size: .8rem; margin: 0; line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.card-tags { display: flex; gap: 5px; flex-wrap: wrap; }
.card-facts { display: flex; gap: 12px; flex-wrap: wrap; font-size: .74rem; color: var(--text-3); margin-top: auto; padding-top: 5px; }
.card-facts span { display: inline-flex; align-items: center; gap: 4px; }
.card-facts svg { width: 13px; height: 13px; }
.fact-vc .vc-amount svg { width: 13px; height: 13px; }
.card-live { display: flex; align-items: center; gap: 6px; font-size: .74rem; color: var(--good); }
.card-live i { width: 6px; height: 6px; border-radius: 50%; background: var(--good); box-shadow: 0 0 7px var(--good); }
.card-friends { display: flex; align-items: center; gap: 5px; font-size: .74rem; color: var(--lavender); }
.card-friends svg { width: 13px; height: 13px; }
.card-actions { display: flex; gap: 7px; padding: 0 14px 13px; align-items: center; }
.card-actions .btn { flex: 1; }
.fav-btn { color: var(--text-3); }
.fav-btn svg { width: 19px; height: 19px; }
.fav-btn:hover { color: var(--danger); }
.fav-btn.is-fav { color: var(--danger); --heart-fill: currentColor; }

/* ═══ game detail ════════════════════════════════════════════════════════ */

.game-hero {
  display: grid; grid-template-columns: 280px 1fr; gap: 26px; margin-bottom: var(--gap);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 26px; overflow: hidden;
}
.game-hero-art {
  aspect-ratio: 1; border-radius: var(--r); display: grid; place-items: center;
  background: linear-gradient(140deg, var(--c1), var(--void) 78%);
  border: 1px solid color-mix(in srgb, var(--c2) 26%, transparent);
}
.game-hero-art span {
  font-size: 5.5rem; font-weight: 700; color: color-mix(in srgb, var(--c2) 85%, #FFF);
  text-shadow: 0 0 46px color-mix(in srgb, var(--c2) 55%, transparent);
}
.game-hero-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
.game-hero h1 { font-size: 2.3rem; margin-bottom: 10px; }
.game-hero-desc { color: var(--text-2); max-width: 66ch; }
.game-hero-by { font-size: .82rem; color: var(--text-3); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--good); box-shadow: 0 0 7px var(--good); }
.game-hero-actions { display: flex; gap: 9px; flex-wrap: wrap; align-items: center; }
.game-columns { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); gap: var(--gap); align-items: start; }
@media (max-width: 1000px) {
  .game-hero { grid-template-columns: 1fr; }
  .game-hero-art { max-width: 190px; }
  .game-columns { grid-template-columns: 1fr; }
}

.control-table, .record-table, .ledger { width: 100%; border-collapse: collapse; font-size: .85rem; }
.control-table th, .record-table th, .ledger th { text-align: left; color: var(--text-3); font-weight: 500; font-size: .78rem; padding: 7px 10px 7px 0; }
.control-table td, .record-table td, .ledger td { padding: 7px 10px 7px 0; border-top: 1px solid var(--line-soft); }
.ledger thead th { border-bottom: 1px solid var(--line); }
.ledger .pos { color: var(--good); }
.ledger .neg { color: var(--danger); }
.record-table a { color: var(--lavender); }
.record-table a:hover { text-decoration: underline; }

.ach-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
.ach {
  display: flex; align-items: center; gap: 12px; padding: 11px 13px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); opacity: .62;
}
.ach.is-unlocked { opacity: 1; border-color: rgba(239,220,163,.28); background: rgba(239,220,163,.05); }
.ach-icon { display: grid; place-items: center; width: 30px; height: 30px; flex: none; color: var(--text-3); }
.ach.is-unlocked .ach-icon { color: var(--gold); }
.ach-icon svg { width: 19px; height: 19px; }
.ach div { flex: 1; display: flex; flex-direction: column; }
.ach b { font-size: .89rem; }
.ach span { font-size: .78rem; color: var(--text-2); }
.ach-vc { flex: none; }
.ach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(255px, 1fr)); gap: 9px; }
.ach-chip {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r);
  background: linear-gradient(120deg, color-mix(in srgb, var(--c1) 55%, var(--surface-2)), var(--surface-2));
  border: 1px solid var(--line);
}
.ach-chip svg { width: 18px; height: 18px; color: var(--gold); flex: none; }
.ach-chip div { flex: 1; min-width: 0; }
.ach-chip b { display: block; font-size: .84rem; }
.ach-chip span { font-size: .72rem; color: var(--text-3); }

.leaderboard { list-style: none; padding: 0; margin: 0; counter-reset: lb; }
.leaderboard li { display: flex; align-items: center; gap: 11px; padding: 7px 0; border-top: 1px solid var(--line-soft); font-size: .86rem; }
.leaderboard li:first-child { border-top: 0; }
.lb-rank { width: 22px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.lb-name { flex: 1; }
.lb-name.is-me { color: var(--gold); font-weight: 600; }
.lb-value { font-variant-numeric: tabular-nums; color: var(--text-2); }

/* ═══ social ═════════════════════════════════════════════════════════════ */

.person-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; }
.person-list li {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 0; border-top: 1px solid var(--line-soft); flex-wrap: wrap;
}
.person-list li:first-child { border-top: 0; }
.person-list.compact li { padding: 7px 0; font-size: .85rem; }
.person-main { display: flex; align-items: center; gap: 11px; min-width: 0; }
.person-main > div { display: flex; flex-direction: column; min-width: 0; }
.person-status { font-size: .76rem; color: var(--text-3); }
.tag { color: var(--text-3); font-weight: 400; font-size: .84em; }
.presence-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--text-3); flex: none; }
.presence-dot.online { background: var(--good); box-shadow: 0 0 7px rgba(143,224,168,.6); }
.presence-dot.in-game { background: var(--lavender); box-shadow: 0 0 7px rgba(200,180,228,.6); }
.row-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.handle-box { display: flex; align-items: center; gap: 8px; padding: 11px 13px; background: var(--surface-2); border-radius: var(--r); margin-bottom: 8px; }
.handle-box b { font-size: 1.05rem; }
.handle-box .btn { margin-left: auto; }
.privacy-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 0; font-size: .86rem; border-top: 1px solid var(--line-soft); }
.privacy-row:first-of-type { border-top: 0; }
.privacy-row select { background: var(--surface-2); border: 1px solid var(--line); color: var(--text); padding: 5px 9px; border-radius: var(--r-sm); font: inherit; font-size: .82rem; }

.party-games { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 7px; }
.party-games li { display: flex; align-items: center; gap: 11px; padding: 8px; border-radius: var(--r); background: var(--surface-2); }
.pg-mark {
  width: 34px; height: 34px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none;
  background: linear-gradient(135deg, var(--c1), var(--void)); color: var(--c2); font-weight: 700;
}
.party-games div { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.party-games b { font-size: .87rem; }
.party-games span { font-size: .74rem; color: var(--text-3); }

.server-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; }
.server-list li { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; border-top: 1px solid var(--line-soft); }
.server-list li:first-child { border-top: 0; }
.server-list.big li { padding: 13px 0; }
.server-list li > div:first-child { display: flex; flex-direction: column; }
.server-list li span { font-size: .78rem; color: var(--text-3); }
.server-line { display: flex; align-items: center; gap: 12px; min-width: 0; }
.server-mark { width: 38px; height: 38px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none; background: linear-gradient(135deg, var(--c1), var(--void)); font-weight: 700; }
.server-line > div { display: flex; flex-direction: column; min-width: 0; }
.server-friends { color: var(--lavender) !important; display: inline-flex; align-items: center; gap: 4px; }
.server-friends svg { width: 12px; height: 12px; }
.server-mine { background: linear-gradient(130deg, color-mix(in srgb, var(--c1) 30%, var(--surface)), var(--surface)); }
.server-meta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
.join-code { font-family: ui-monospace, monospace; letter-spacing: .22em; font-size: 1.05rem; font-weight: 700; color: var(--gold); }
.conn-status { font-size: .74rem; color: var(--text-3); margin-left: auto; }
.server-start { display: flex; align-items: center; gap: 13px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); flex-wrap: wrap; }

/* current game + host-only "Change game" (see src/platform/pages/server-game.js) */
.server-game-row {
  display: flex; align-items: center; justify-content: space-between; gap: 13px; flex-wrap: wrap;
  padding: 11px 13px; margin-bottom: 14px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .02);
}
.server-game-now { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.server-game-label { font-size: .68rem; letter-spacing: .16em; text-transform: uppercase; color: var(--text-3); }
.server-game-origin, .server-origin { font-size: .74rem; }

.game-choice-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; max-height: 52vh; overflow-y: auto; }
.game-choice {
  width: 100%; display: flex; align-items: center; gap: 12px; text-align: left; cursor: pointer;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: linear-gradient(120deg, color-mix(in srgb, var(--c1) 22%, var(--surface)), var(--surface));
  color: inherit; font: inherit;
}
.game-choice:hover:not([disabled]) { border-color: var(--lavender); }
.game-choice.is-selected { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold) inset; }
.game-choice[disabled], .game-choice.is-blocked { opacity: .45; cursor: not-allowed; }
.game-choice-mark { width: 34px; height: 34px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none; background: linear-gradient(135deg, var(--c1), var(--void)); font-weight: 700; }
.game-choice-main { display: flex; flex-direction: column; min-width: 0; }
.game-choice-sub { font-size: .76rem; color: var(--text-3); }
.game-choice-why { font-size: .74rem; color: var(--amber, var(--gold)); }
.game-choice.is-current b { color: var(--gold); }

/* ═══ wallet ═════════════════════════════════════════════════════════════ */

.wallet-hero {
  display: grid; grid-template-columns: minmax(220px, 300px) 1fr; gap: 26px; align-items: center;
  background: linear-gradient(125deg, rgba(239,220,163,.08), var(--surface) 55%);
  border: 1px solid rgba(239,220,163,.2); border-radius: var(--r-lg); padding: 26px; margin-bottom: var(--gap);
}
.wallet-label { font-size: .72rem; letter-spacing: .16em; color: var(--text-3); }
.wallet-amount { font-size: 2.6rem; font-weight: 700; margin: 5px 0; }
.wallet-amount .vc-amount svg { width: 32px; height: 32px; }
.wallet-sub { font-size: .8rem; color: var(--text-3); }
.wallet-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 16px; margin: 0; }
.wallet-stats div { display: flex; flex-direction: column; }
.wallet-stats dt { font-size: .72rem; color: var(--text-3); }
.wallet-stats dd { margin: 0; font-size: 1.15rem; font-weight: 600; font-variant-numeric: tabular-nums; }
@media (max-width: 760px) { .wallet-hero { grid-template-columns: 1fr; } }

.tabs { display: flex; gap: 3px; border-bottom: 1px solid var(--line); margin-bottom: var(--gap); overflow-x: auto; }
.tabs button {
  background: none; border: 0; border-bottom: 2px solid transparent; color: var(--text-2);
  padding: 10px 15px; font: inherit; font-size: .88rem; font-weight: 600; cursor: pointer;
  transition: var(--fast); white-space: nowrap;
}
.tabs button:hover { color: var(--text); }
.tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--lavender); }

.cosmetic-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
.cosmetic { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); padding: 14px; display: flex; flex-direction: column; gap: 7px; }
/* Hosts the shared item art (src/platform/item-art.js). The tile keeps the
   item's colour as the wash; the art sits on top as the single render instance. */
.cosmetic-swatch {
  height: 52px; border-radius: var(--r-sm); display: grid; place-items: center;
  background: linear-gradient(120deg, var(--c), var(--void) 88%);
  border: 1px solid color-mix(in srgb, var(--c) 32%, transparent);
}
/* The wash already carries the colour, so the art is a neutral overlay here
   rather than a second tinted tile stacked on a tinted tile. */
.cosmetic-swatch .item-art {
  width: 100%; height: 100%; background: none; border: none; border-radius: inherit;
  color: color-mix(in srgb, var(--c) 72%, #F1EBE2);
}
.cosmetic-swatch .item-art svg { width: 30px; height: 30px; }
.cosmetic h3 { font-size: .89rem; }
.cosmetic p { font-size: .76rem; color: var(--text-3); margin: 0; flex: 1; }
.cosmetic-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 5px; }
.owned { display: inline-flex; align-items: center; gap: 5px; font-size: .78rem; color: var(--good); }
.owned svg { width: 15px; height: 15px; }

.bundle-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
.bundle { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); padding: 18px; text-align: center; display: flex; flex-direction: column; gap: 9px; }
.bundle-amount .vc-amount { font-size: 1.25rem; }
.bundle-amount .vc-amount svg { width: 21px; height: 21px; }
.bundle-note { font-size: .72rem; color: var(--gold); }
.bundle-price { font-size: 1.05rem; font-weight: 600; color: var(--text-2); }

.stat-tile { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); padding: 15px; display: flex; flex-direction: column; gap: 3px; }
.stat-tile span { font-size: 1.45rem; font-weight: 700; font-variant-numeric: tabular-nums; display: flex; align-items: baseline; gap: 3px; }
.stat-tile span i { font-size: .82rem; font-style: normal; color: var(--text-3); font-weight: 400; }
.stat-tile span .vc-amount { font-size: 1.45rem; }
.stat-tile small { font-size: .72rem; color: var(--text-3); }
.profile-summary, .creator-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: var(--gap); }

.save-list, .mini-ledger, .project-list, .review-list { list-style: none; padding: 0; margin: 0; }
.save-list li, .mini-ledger li, .project-list li {
  display: flex; align-items: center; justify-content: space-between; gap: 11px;
  padding: 9px 0; border-top: 1px solid var(--line-soft); font-size: .86rem;
}
.save-list li:first-child, .mini-ledger li:first-child, .project-list li:first-child { border-top: 0; }
.mini-ledger li { flex-wrap: wrap; }
.mini-ledger span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mini-ledger small { color: var(--text-3); font-size: .74rem; }
.mini-ledger b.pos { color: var(--good); }
.mini-ledger b.neg { color: var(--danger); }
.mini-ledger blockquote { flex-basis: 100%; margin: 4px 0 0; }
.save-list li > div { display: flex; flex-direction: column; }
.project-row a { flex: 1; display: block; min-width: 0; }
.project-row a > div { display: flex; flex-direction: column; }
.project-row:hover b { color: var(--lavender); }

/* ═══ creator ════════════════════════════════════════════════════════════ */

.credit-packages { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.credit-pkg {
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); padding: 13px;
  display: flex; flex-direction: column; gap: 4px; cursor: pointer; font: inherit; color: var(--text);
  transition: var(--fast); text-align: left;
}
.credit-pkg:hover { border-color: var(--lavender); background: var(--raised); }
.credit-pkg b { font-size: .95rem; }
.pkg-note { font-size: .7rem; color: var(--gold); }
.template-grid { display: grid; gap: 8px; }
.template { display: block; cursor: pointer; }
.template input { position: absolute; opacity: 0; }
.template span {
  display: flex; flex-direction: column; gap: 3px; padding: 13px; border-radius: var(--r);
  border: 1px solid var(--line); background: var(--surface-2); transition: var(--fast);
}
.template input:checked + span { border-color: var(--lavender); background: rgba(200,180,228,.07); }
.template small { color: var(--text-3); font-size: .78rem; }
.estimate-box { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); padding: 13px; margin: 16px 0; font-size: .84rem; }
.estimate-box b { font-size: 1.05rem; display: block; margin-bottom: 7px; }
.estimate-box ul { list-style: none; padding: 0; margin: 0 0 7px; display: flex; flex-direction: column; gap: 3px; }
.estimate-box li { display: flex; justify-content: space-between; color: var(--text-3); font-size: .78rem; }
.estimate-box small { color: var(--text-3); }
.pub-form section { margin-bottom: var(--gap); }
.control-editor, .control-row { display: flex; flex-direction: column; gap: 7px; }
.control-row { flex-direction: row; align-items: center; }
.control-row .field { flex: 1; }
.ach-editor { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); padding: 14px; margin-bottom: 10px; }
.verify-row { display: grid; grid-template-columns: 2fr 90px 110px; gap: 7px; }
.checklist { display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); gap: 2px; }
.form-save-bar {
  position: sticky; bottom: 0; background: linear-gradient(transparent, var(--void) 35%);
  padding: 16px 0 8px; font-size: .78rem; color: var(--text-3); text-align: right;
}
.feedback-block { margin-bottom: 14px; }
.doc { border-top: 1px solid var(--line-soft); padding: 10px 0; }
.doc summary { cursor: pointer; font-weight: 600; font-size: .89rem; }
.doc[open] summary { margin-bottom: 9px; color: var(--lavender); }
.doc p { font-size: .85rem; color: var(--text-2); }
.doc pre { background: var(--surface-2); padding: 11px; border-radius: var(--r-sm); overflow-x: auto; }
.pipeline { margin: 0; padding-left: 20px; font-size: .86rem; color: var(--text-2); display: flex; flex-direction: column; gap: 5px; }

/* ═══ admin ══════════════════════════════════════════════════════════════ */

.review-list li { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 13px 0; border-top: 1px solid var(--line-soft); flex-wrap: wrap; }
.review-list li:first-child { border-top: 0; }
.review-main { display: flex; align-items: center; gap: 13px; flex: 1; min-width: 0; }
.review-main > div { display: flex; flex-direction: column; min-width: 0; }
.review-mark { width: 42px; height: 42px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none; background: linear-gradient(135deg, var(--c1), var(--void)); font-weight: 700; font-size: 1.15rem; }
.review-sub { font-size: .76rem; color: var(--text-3); }
.review-risk { font-size: .74rem; color: var(--warn); display: inline-flex; align-items: center; gap: 5px; margin-top: 3px; }
.review-risk svg { width: 13px; height: 13px; }
.risk-high .review-mark { box-shadow: 0 0 0 2px rgba(240,122,122,.4); }
.risk-badge { display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; border-radius: 20px; font-size: .8rem; font-weight: 600; text-transform: capitalize; background: var(--surface-2); border: 1px solid var(--line); }
.risk-badge svg { width: 16px; height: 16px; }
.risk-badge.risk-high { background: rgba(240,122,122,.11); border-color: rgba(240,122,122,.32); color: var(--danger); }
.risk-badge.risk-medium { background: rgba(233,185,107,.11); border-color: rgba(233,185,107,.32); color: var(--warn); }
.risk-badge.risk-low { color: var(--good); }
.decision-stack { display: flex; flex-direction: column; gap: 8px; }
.report-reason { margin: 6px 0 0; font-size: .82rem; }
.dev-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px; margin: 14px 0; }
.dev-log pre { background: var(--void); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 11px; font-size: .74rem; max-height: 210px; overflow: auto; color: var(--text-2); }
.dev-log pre.err { color: var(--danger); }

/* ═══ settings ═══════════════════════════════════════════════════════════ */

.settings-list { display: flex; flex-direction: column; }
.setting-row { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 12px 0; border-top: 1px solid var(--line-soft); }
.setting-row:first-child { border-top: 0; }
.setting-row > div { display: flex; flex-direction: column; }
.setting-row b { font-size: .89rem; font-weight: 600; }
.setting-row small { font-size: .76rem; color: var(--text-3); margin-top: 2px; max-width: 60ch; }
.opt-switch {
  width: 42px; height: 24px; border-radius: 13px; background: var(--raised); border: 1px solid var(--line);
  position: relative; cursor: pointer; transition: var(--fast); flex: none; padding: 0;
}
.opt-switch span, .opt-switch::after {
  content: ''; position: absolute; left: 3px; top: 50%; translate: 0 -50%;
  width: 16px; height: 16px; border-radius: 50%; background: var(--text-3); transition: var(--fast);
}
.opt-switch.on { background: rgba(200,180,228,.28); border-color: var(--lavender); }
.opt-switch.on span, .opt-switch.on::after { left: 21px; background: var(--lavender); }
.range-wrap { display: flex; align-items: center; gap: 11px; position: relative; }
.range-wrap input[type=range] { width: 150px; accent-color: var(--lavender); }
.range-wrap output { font-size: .8rem; color: var(--text-3); min-width: 42px; text-align: right; font-variant-numeric: tabular-nums; }
/* Default marker: a tick under the slider at the schema default, so a player
   can see where "normal" is without resetting to find out. */
.range-wrap .range-default {
  position: absolute; left: calc(6px + 138px * var(--at)); bottom: 2px;
  width: 2px; height: 6px; border-radius: 1px; background: var(--text-3); opacity: .55; pointer-events: none;
}
.btn.btn-tiny { padding: 3px 8px; font-size: .72rem; border-radius: var(--r-sm); }
.setting-row select { background: var(--surface-2); border: 1px solid var(--line); color: var(--text); padding: 6px 10px; border-radius: var(--r-sm); font: inherit; font-size: .84rem; }
.binding-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); gap: 8px; margin: 14px 0; }
.binding { display: flex; align-items: center; justify-content: space-between; gap: 11px; padding: 8px 11px; background: var(--surface-2); border-radius: var(--r-sm); font-size: .85rem; }
.key-btn { background: none; border: 0; cursor: pointer; font: inherit; padding: 0; }
.key-btn.listening { color: var(--lavender); font-size: .78rem; }
.binding-note { grid-column: 1 / -1; font-size: .78rem; color: var(--text-3); padding-top: 6px; }

/* ═══ toasts, modals, promos ═════════════════════════════════════════════ */

#toast-root { position: fixed; right: 18px; bottom: 18px; z-index: 200; display: flex; flex-direction: column; gap: 8px; align-items: flex-end; pointer-events: none; }
/* A toast carries a button (Join, Create account), so it takes pointer events —
   and it docks into the bottom-right corner, which on a touch HUD is the jump
   button. Inside a world on a coarse pointer it takes the TOP of the screen
   instead, where no control lives, and clears the notch on the way. */
@media (pointer: coarse) {
  body.in-game #toast-root {
    right: calc(var(--safe-r) + 12px); left: calc(var(--safe-l) + 12px);
    bottom: auto; top: calc(var(--safe-t) + 54px);
    align-items: center;
  }
  body.in-game .toast { max-width: min(420px, 100%); }
}
.toast {
  display: flex; align-items: center; gap: 11px; background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--r); padding: 11px 15px; box-shadow: var(--shadow); font-size: .87rem;
  max-width: 400px; opacity: 0; transform: translateY(12px); transition: var(--slow); pointer-events: auto;
}
.toast.show { opacity: 1; transform: none; }
.toast-icon { display: grid; place-items: center; }
.toast-icon svg { width: 18px; height: 18px; color: var(--lavender); }
.toast-text { flex: 1; }
.toast-warn { border-color: rgba(233,185,107,.4); }
.toast-error { border-color: rgba(240,122,122,.4); }

.modal-backdrop {
  position: fixed; inset: 0; z-index: 300; background: rgba(3,3,5,.72); backdrop-filter: blur(7px);
  display: grid; place-items: center; opacity: 0; transition: var(--fast);
  /* `minmax(0, 1fr)`, not the default `auto`: an auto row grows to fit its
     content, which makes the dialog's own `max-height: 100%` resolve against a
     row the dialog itself sized — so it never clamps, and a tall body (the
     Stripe form) pushes the actions row off the bottom of the screen. */
  grid-template-rows: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
  /* Inside the safe area, so a dialog is never partly under a notch or a home
     indicator, and never behind the software keyboard. */
  padding: calc(22px + var(--safe-t)) calc(22px + var(--safe-r))
           calc(22px + var(--safe-b) + var(--kb-inset, 0px)) calc(22px + var(--safe-l));
}
.modal-backdrop.show { opacity: 1; }
.modal {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  width: min(520px, 100%); display: flex; flex-direction: column;
  box-shadow: var(--shadow); transform: scale(.97); transition: var(--fast);
  /* `100%` of a backdrop that is already inset by the safe area and the
     keyboard, rather than `88vh`: on iOS `vh` is the height the page has with
     the address bar RETRACTED, so a dialog sized in vh runs off the bottom of
     the screen it is actually being shown on. */
  max-height: 100%;
}
.modal-wide { width: min(760px, 100%); }
.modal-backdrop.show .modal { transform: none; }
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 18px 20px 12px; flex: none; }
/* `min-height: 0` is what makes `overflow-y: auto` actually work here. A flex
   item's default floor is its content height, so a tall body — the Stripe
   iframe is the one that exposed this — refuses to shrink, the modal grows
   past its own `max-height`, and the actions row (the Pay button) ends up
   below the bottom of the screen with nothing scrollable to reach it. */
.modal-body { padding: 0 20px 18px; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; flex: 1 1 auto; min-height: 0; }
.modal-text { color: var(--text-2); font-size: .89rem; white-space: pre-line; }
/* Consequence lists in a confirmation. A destructive-looking admin action is
   far easier to judge as a short list of effects than as a paragraph. */
.modal-list { margin: 10px 0 12px 18px; color: var(--text-2); font-size: .84rem; display: flex; flex-direction: column; gap: 5px; }
/* Never shrinks and never scrolls away: the confirm button has to stay on
   screen whatever the body does above it. */
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 20px; border-top: 1px solid var(--line); flex-wrap: wrap; flex: none; }
/* On a phone a row of three actions is three targets too narrow to hit; they
   become full-width rows in the order they were declared. */
@media (max-width: 560px) {
  .modal-actions { flex-direction: column; }
  .modal-actions .btn { width: 100%; justify-content: center; min-height: var(--tap); }
}

.promo {
  display: flex; align-items: center; gap: 16px; padding: 15px 18px; margin-bottom: var(--gap);
  border-radius: var(--r-lg); border: 1px solid rgba(200,180,228,.22);
  background: linear-gradient(120deg, rgba(200,180,228,.07), var(--surface) 62%);
  transition: var(--slow);
}
.promo.dismissed { opacity: 0; transform: translateY(-6px); }
.promo-body { flex: 1; min-width: 0; }
.promo-body h4 { font-size: .92rem; margin-bottom: 3px; }
.promo-body p { font-size: .82rem; color: var(--text-2); margin: 0; }
.promo-price { display: flex; align-items: center; gap: 9px; margin-top: 7px; font-size: .82rem; }
.promo-price span { color: var(--text-3); font-size: .74rem; }
.promo-actions { display: flex; align-items: center; gap: 6px; flex: none; }
.promo-dismiss { width: 28px; height: 28px; }
.promo-dismiss svg { width: 15px; height: 15px; }

/* ═══ game layer ═════════════════════════════════════════════════════════ */

.game-layer { position: fixed; inset: 0; z-index: 100; background: var(--void); }
.game-shell { position: absolute; inset: 0; pointer-events: none; }
.game-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; }
/* ── the chrome band, on a screen with no room to spare ────────────────────
   The platform's HUD bar floats over the top of the canvas, and every game
   draws its own title near the top of what it is given. On a desktop there is
   width enough for the two to sit side by side; on a phone they land on each
   other — measured on a 375px screen, UMBRA's chamber name straight through
   the platform's game title.

   Stepping the CANVAS down is what fixes it for all fifteen games at once,
   rather than fifteen games each learning about a bar they should not have to
   know exists. The games keep their whole coordinate space, one bar shorter;
   `createCanvas` measures the canvas itself, so what they are told matches
   what they are drawing into.

   Only where it is needed: a desktop keeps the full-bleed canvas it has always
   had, and so does anything with room for both. */
@media (max-width: 720px) {
  .game-canvas { top: calc(var(--safe-t) + 46px); }
}
/* The same argument at the other end. While on-screen controls are up they own
   the bottom of the screen, and a game drawing its own footer there is drawing
   under the player's thumbs — measured in NOCTURNE SYNC, whose score and
   calibration line sat behind the four lane pads.

   `--tc-band` is the size those controls reserve, and it is the same token the
   3D worlds' chrome clears. Keyed on `[data-touch-controls]`, which is written
   only while the controls are actually VISIBLE, so a controller player gets the
   whole canvas back the moment they pick one up. */
:root[data-touch-controls] .game-canvas {
  bottom: calc(var(--safe-b) + var(--tc-edge) + var(--tc-band));
}
.sandbox-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: var(--black); }

.game-hud {
  /* Below the notch, not under it — `viewport-fit=cover` puts the layer edge
     to edge, so the bar has to step down by whatever the hardware took. Zero
     on every device without one, so this is the same 46px bar on a desktop.
     Its height stays a literal 46px: `--shell-bar-h` in styles/world3d.css
     mirrors it, and `--shell-band-h` there adds the inset back for anything
     that has to clear the whole band. */
  position: absolute; top: var(--safe-t); left: 0; right: 0; height: 46px; z-index: 3;
  display: flex; align-items: center; gap: 12px; pointer-events: auto;
  padding: 0 calc(12px + var(--safe-r)) 0 calc(12px + var(--safe-l));
  background: linear-gradient(rgba(5,5,7,.82), transparent);
}
.hud-btn {
  display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--r-sm);
  background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.09);
  color: var(--text-2); cursor: pointer; padding: 0; transition: var(--fast); flex: none;
}
.hud-btn svg { width: 17px; height: 17px; }
/* 32px is a mouse target. On a finger the button keeps its drawn size and
   grows an invisible hit area to the floor, so the bar does not get taller. */
@media (pointer: coarse) {
  .hud-btn { position: relative; }
  .hud-btn::after { content: ''; position: absolute; top: 50%; left: 50%; translate: -50% -50%; width: var(--tap); height: var(--tap); }
}
.hud-btn:hover { background: rgba(255,255,255,.11); color: var(--text); }
.hud-title { font-size: .8rem; font-weight: 600; letter-spacing: .1em; color: var(--text-2); }
.hud-status { font-size: .78rem; color: var(--text-3); }
.hud-spacer { flex: 1; }
.hud-custom { font-size: .78rem; color: var(--text-2); }
.hud-score { font-size: 1.05rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.hud-fps { font-size: .72rem; color: var(--text-3); font-variant-numeric: tabular-nums; }

.game-overlay {
  position: absolute; inset: 0; z-index: 5; display: grid; place-items: center; padding: 22px;
  background: rgba(4,4,6,.86); backdrop-filter: blur(9px); pointer-events: auto; overflow-y: auto;
}
.ov-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 30px; width: min(660px, 100%); box-shadow: var(--shadow); max-height: 90vh; overflow-y: auto;
}
.ov-narrow { width: min(400px, 100%); text-align: center; }
.ov-menu {
  background:
    radial-gradient(ellipse at 82% 12%, color-mix(in srgb, var(--c2) 17%, transparent), transparent 58%),
    linear-gradient(150deg, color-mix(in srgb, var(--c1) 45%, var(--surface)), var(--surface) 62%);
}
.ov-eyebrow { font-size: .68rem; letter-spacing: .2em; color: var(--gold); font-weight: 600; }
.ov-title { font-size: 2.4rem; margin: 7px 0 12px; letter-spacing: -.02em; }
.ov-desc { color: var(--text-2); font-size: .92rem; max-width: 62ch; }
.ov-sub { color: var(--text-2); }
.ov-facts { display: flex; flex-direction: column; gap: 8px; margin: 18px 0; }
.ov-facts > div { display: grid; grid-template-columns: 82px 1fr; gap: 13px; font-size: .85rem; }
.ov-facts dt { color: var(--text-3); font-size: .78rem; }
.ov-facts dd { margin: 0; color: var(--text-2); }
.ov-best { font-size: .82rem; color: var(--gold); margin-bottom: 12px; }
.ov-vc, .ov-warn { display: flex; align-items: center; gap: 9px; font-size: .82rem; color: var(--text-2); margin-bottom: 12px; }
.ov-vc svg, .ov-warn svg { width: 18px; height: 18px; flex: none; }
.ov-warn { color: var(--warn); }
.ov-actions { display: flex; gap: 9px; flex-wrap: wrap; margin-top: 22px; }
.ov-actions.ov-stack { flex-direction: column; }
.ov-narrow .ov-actions { justify-content: center; }
.ov-controls { width: 100%; border-collapse: collapse; font-size: .87rem; margin: 14px 0; }
.ov-controls th { text-align: left; color: var(--text-2); font-weight: 500; padding: 7px 12px 7px 0; }
.ov-controls td { padding: 7px 0; }
.ov-note { font-size: .78rem; color: var(--text-3); }
.ov-error { background: var(--void); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 11px; font-size: .74rem; color: var(--danger); overflow-x: auto; }
.ov-result.is-win .ov-eyebrow { color: var(--good); }
.ov-result.is-lose .ov-eyebrow { color: var(--danger); }
.res-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(115px, 1fr)); gap: 14px; margin: 20px 0; }
.res-stats > div { display: flex; flex-direction: column; }
.res-stats dt { font-size: .72rem; color: var(--text-3); }
.res-stats dd { margin: 0; font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.res-rewards { display: flex; flex-direction: column; gap: 10px; }
.res-pending { color: var(--text-3); font-size: .85rem; }
.res-vc {
  display: flex; align-items: center; gap: 10px; padding: 13px 16px; border-radius: var(--r);
  background: rgba(239,220,163,.09); border: 1px solid rgba(239,220,163,.3);
}
.res-vc svg { width: 24px; height: 24px; }
.res-vc b { font-size: 1.15rem; color: var(--gold); }
.res-vc span { font-size: .8rem; color: var(--text-2); }
.res-unlocked h4 { font-size: .78rem; text-transform: uppercase; letter-spacing: .09em; color: var(--text-3); margin-bottom: 7px; }
.res-ach { display: flex; align-items: center; gap: 10px; padding: 9px 12px; background: var(--surface-2); border-radius: var(--r); margin-bottom: 6px; }
.res-ach svg { width: 18px; height: 18px; color: var(--gold); flex: none; }
.res-ach span { font-size: .78rem; color: var(--text-3); }
.res-note { font-size: .82rem; color: var(--text-3); }
.res-error { color: var(--warn); }
.opt-group { margin-bottom: 20px; }
.opt-group h3 { font-size: .74rem; text-transform: uppercase; letter-spacing: .09em; color: var(--text-3); margin-bottom: 9px; }
.opt-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%; background: none; border: 0; border-top: 1px solid var(--line-soft); color: var(--text); font: inherit; padding: 10px 0; cursor: pointer; text-align: left; }
.opt-label { display: flex; flex-direction: column; }
.opt-label small { font-size: .74rem; color: var(--text-3); }
.opt-slider { display: flex; align-items: center; gap: 12px; padding: 8px 0; font-size: .87rem; }
.opt-slider span { flex: 1; }
.opt-slider input { width: 160px; accent-color: var(--lavender); }
.opt-slider output { min-width: 42px; text-align: right; color: var(--text-3); font-size: .8rem; }

/* ═══ in-game chat ═══════════════════════════════════════════════════════ */

/* `pointer-events: none` on the WRAPPER, `auto` on its children — the same
   contract `.hud` uses in nexus.css. The box is 296px wide and completely
   transparent, so while collapsed it painted a 74px toggle and silently took
   every click across the remaining 222px of empty space, including clicks
   meant for whatever was underneath it. */
.chatbox { position: absolute; top: 54px; left: 12px; z-index: 4; width: 296px; pointer-events: none; font-size: .84rem; }
.chatbox > * { pointer-events: auto; }
.chat-toggle {
  display: flex; align-items: center; gap: 7px; background: rgba(11,11,14,.78); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.1); border-radius: var(--r-sm); color: var(--text-2);
  padding: 6px 11px; font: inherit; font-size: .8rem; font-weight: 600; cursor: pointer; transition: var(--fast);
}
.chat-toggle svg { width: 16px; height: 16px; }
.chat-toggle:hover { color: var(--text); border-color: rgba(255,255,255,.2); }
.chat-unread { background: var(--danger); color: #170B0B; font-size: .64rem; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; display: grid; place-items: center; padding: 0 4px; }
.chat-panel {
  margin-top: 6px; background: rgba(11,11,14,.9); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,.1); border-radius: var(--r); overflow: hidden;
  display: flex; flex-direction: column; max-height: 300px;
}
.chatbox.is-typing .chat-panel { border-color: var(--lavender); box-shadow: 0 0 0 2px rgba(200,180,228,.2); }
.chat-tabs { display: flex; border-bottom: 1px solid rgba(255,255,255,.07); }
.chat-tabs button { flex: 1; background: none; border: 0; color: var(--text-3); padding: 7px; font: inherit; font-size: .76rem; font-weight: 600; cursor: pointer; }
.chat-tabs button[aria-selected="true"] { color: var(--text); background: rgba(255,255,255,.05); }
.chat-log { flex: 1; overflow-y: auto; padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; min-height: 110px; }
.chat-msg { display: flex; gap: 6px; align-items: baseline; line-height: 1.4; font-size: .8rem; }
.chat-msg.is-system { color: var(--text-3); font-style: italic; font-size: .74rem; }
.chat-who { color: var(--lavender); font-weight: 600; flex: none; }
.chat-msg.is-mine .chat-who { color: var(--gold); }
.chat-text { color: var(--text-2); word-break: break-word; flex: 1; }
.chat-flag { font-size: .62rem; color: var(--warn); flex: none; }
.chat-mute { background: none; border: 0; color: var(--text-3); cursor: pointer; opacity: 0; padding: 0 3px; font-size: .9rem; }
.chat-msg:hover .chat-mute { opacity: 1; }
.chat-form { display: flex; gap: 5px; padding: 7px; border-top: 1px solid rgba(255,255,255,.07); }
.chat-input { flex: 1; background: rgba(255,255,255,.05); border: 1px solid transparent; border-radius: var(--r-sm); color: var(--text); padding: 6px 9px; font: inherit; font-size: .8rem; }
.chat-input:focus { outline: none; border-color: var(--lavender); background: rgba(255,255,255,.09); }
.chat-send { display: grid; place-items: center; width: 30px; background: rgba(255,255,255,.06); border: 0; border-radius: var(--r-sm); color: var(--text-2); cursor: pointer; }
.chat-send svg { width: 15px; height: 15px; }
.chat-note { padding: 0 10px 7px; font-size: .72rem; color: var(--warn); }
.chat-note:empty { display: none; }

@media (max-width: 620px) {
  .chatbox { width: calc(100vw - 24px); max-width: 296px; }
  .ov-card { padding: 22px 18px; }
  .ov-title { font-size: 1.7rem; }
}

/* ═══ NEXUS entry & Avatar Studio ════════════════════════════════════════ */

.side-nexus {
  background: linear-gradient(120deg, rgba(200,180,228,.16), rgba(239,220,163,.08));
  border: 1px solid rgba(200,180,228,.3);
  margin-bottom: 8px;
  color: var(--text) !important;
  font-weight: 600;
}
.side-nexus:hover { background: linear-gradient(120deg, rgba(200,180,228,.24), rgba(239,220,163,.14)); }
.side-nexus svg { color: var(--lavender); }

.avatar-studio { min-height: 60vh; }
.nexus-mount { position: absolute; inset: 0; }
.nexus-mount canvas { display: block; width: 100%; height: 100%; }

@media (max-width: 900px) {
  .side-nexus { margin-bottom: 0; border-radius: var(--r-sm); }
}

/* ── light theme: surfaces that paint their own colour ────────────────────
   A handful of places do not resolve their background through the token block
   — they blend a per-game accent (`--c1`/`--c2`, set inline on the element)
   into `--void`, or overlay white at low alpha. Both assume a dark base, so on
   the light palette the featured hero came out as a dark slab with the new
   near-black text on top of it: unreadable.

   These rules re-mix the same accents as a LIGHT wash. The game's colour still
   identifies the card; it just sits under the text instead of fighting it. */
:root[data-theme="light"] .hero-art {
  background:
    radial-gradient(ellipse at 78% 24%, color-mix(in srgb, var(--c2) 22%, transparent), transparent 60%),
    linear-gradient(115deg, color-mix(in srgb, var(--c1) 26%, var(--black)), var(--black) 74%);
}
/* The decorative grid was white-on-dark; invert it so it still reads. */
:root[data-theme="light"] .hero-art::after {
  background-image: linear-gradient(rgba(26, 24, 32, .05) 1px, transparent 1px),
                    linear-gradient(90deg, rgba(26, 24, 32, .05) 1px, transparent 1px);
}
:root[data-theme="light"] .game-hero-art,
:root[data-theme="light"] .card-art {
  background:
    radial-gradient(ellipse at 70% 30%, color-mix(in srgb, var(--c2) 30%, transparent), transparent 62%),
    linear-gradient(135deg, color-mix(in srgb, var(--c1) 32%, var(--black)), var(--black) 78%);
}
/* The big letter mark on an art tile is mixed towards WHITE in the dark theme,
   which vanishes on a light wash. Mix it towards the text colour instead. */
:root[data-theme="light"] .card-art-mark {
  color: color-mix(in srgb, var(--c2) 55%, var(--text));
  text-shadow: none;
  opacity: .85;
}
:root[data-theme="light"] .game-hero-art span { color: color-mix(in srgb, var(--c1) 55%, var(--text)); }

/* ── equipped cosmetics ───────────────────────────────────────────────────
   Victory effects. The card owns a positioned stage; each effect id drives
   the three <i> elements differently. `victory_default` is a single flash and
   is also what reduced-motion falls back to, so the stage is never the reason
   a result screen moves when the player asked it not to. */
.ov-result { position: relative; overflow: hidden; }
.ov-result > *:not(.vfx-stage) { position: relative; z-index: 1; }
.vfx-stage {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  display: grid; place-items: center;
}
.vfx-stage i { position: absolute; display: block; border-radius: 50%; opacity: 0; }

.vfx-victory_default .vfx-stage i:first-child {
  inset: 0; border-radius: 0; background: var(--vfx);
  animation: vfx-flash .42s ease-out forwards;
}
@keyframes vfx-flash { from { opacity: .22; } to { opacity: 0; } }

/* Slow Bloom — an unfolding lattice: three rings easing outward in sequence. */
.vfx-victory_bloom .vfx-stage i {
  width: 40px; height: 40px; border: 1px solid var(--vfx);
  animation: vfx-bloom 2.6s cubic-bezier(.16,.84,.34,1) forwards;
}
.vfx-victory_bloom .vfx-stage i:nth-child(2) { animation-delay: .3s; }
.vfx-victory_bloom .vfx-stage i:nth-child(3) { animation-delay: .6s; }
@keyframes vfx-bloom {
  0%   { opacity: .55; transform: scale(.2) rotate(0deg); }
  100% { opacity: 0;   transform: scale(15) rotate(45deg); }
}

/* Terminal Shatter — the card fractures outward: hard shards, no easing back. */
.vfx-victory_shatter .vfx-stage i {
  width: 3px; height: 260px; border-radius: 0; background: var(--vfx);
  transform-origin: 50% 0;
  animation: vfx-shatter .75s cubic-bezier(.2,.9,.3,1) forwards;
}
.vfx-victory_shatter .vfx-stage i:nth-child(1) { animation-delay: 0s;    rotate: -34deg; }
.vfx-victory_shatter .vfx-stage i:nth-child(2) { animation-delay: .06s;  rotate: 12deg; }
.vfx-victory_shatter .vfx-stage i:nth-child(3) { animation-delay: .12s;  rotate: 58deg; }
@keyframes vfx-shatter {
  0%   { opacity: .8; transform: translateY(-40%) scaleY(.1); }
  100% { opacity: 0;  transform: translateY(-40%) scaleY(3); }
}

/* Profile frames. The border is the product, so it is drawn as an outline that
   does not move the portrait it wraps. */
.cos-frame { position: relative; border-radius: inherit; }
.cos-frame::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; border: 1px solid var(--frame, #3A3A44);
}
.cos-frame.frame-bevel::after { border-width: 2px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.55); }
.cos-frame.frame-gilt::after  { border-width: 2px; box-shadow: 0 0 0 1px rgba(0,0,0,.6), inset 0 0 7px rgba(239,220,163,.35); }
.cos-frame.frame-eclipse::after {
  border-width: 2px; box-shadow: 0 0 12px var(--frame), inset 0 0 9px var(--frame);
}

/* Store: the equip control sits beside the price, so owning and wearing read
   as two different states rather than one. */
.cosmetic-foot .btn-equip[aria-pressed="true"] { border-color: var(--good); color: var(--good); }
.cosmetic.is-worn { outline: 1px solid var(--c); outline-offset: -1px; }
.cosmetic .worn-tag { font-size: .7rem; color: var(--good); letter-spacing: .06em; }

/* Profile identity plate — the portrait the equipped frame wraps. */
.page-head .profile-ident { display: flex; align-items: center; gap: 16px; }
.profile-portrait {
  display: grid; place-items: center; width: 54px; height: 54px; border-radius: 12px;
  background: var(--surface-2); color: var(--text-2); font-size: 1.4rem; font-weight: 700;
}
.worn-list dd { display: flex; align-items: center; gap: 7px; }
.worn-dot { width: 9px; height: 9px; border-radius: 3px; flex: none; }
.cosmetic-effect { font-size: .74rem; color: var(--text-3); margin: 6px 0 0; }

/* ═══ input method: keyboard · touch · controller ═════════════════════════
   `data-input` is written by src/input/router.js and is whichever device the
   player most recently DROVE — not what is connected. Everything below is
   presentation: the same page, dressed for the hands on it.

   The rule these follow: nothing here changes what a control DOES, only how
   big it is and how obvious the selection is. A layout that only works with a
   controller is a second interface, and a second interface is the thing this
   whole layer exists to avoid. */

/* ── the controller's selection ───────────────────────────────────────────
   `:focus-visible` is the browser's own idea of "the keyboard is driving", and
   it is deliberately conservative: a click focuses a button WITHOUT showing a
   ring, because a mouse user does not need one. A controller user needs one
   permanently and from three metres away, so `.gp-focus` — written by
   src/input/focus.js onto exactly the element it has selected — is a second,
   louder ring that does not wait to be asked.

   Drawn with `outline` rather than a border or a box-shadow so it cannot
   change any element's size: a selection that reflows the list it is moving
   through is a list that moves away from the player as they navigate it. */
html[data-input="gamepad"] .gp-focus,
html[data-input="gamepad"] .gp-focus:focus {
  outline: 3px solid var(--lavender);
  outline-offset: 3px;
  border-radius: 6px;
  box-shadow: 0 0 0 6px rgba(200, 180, 228, .16), 0 0 22px rgba(200, 180, 228, .22);
  scroll-margin: 24px;
}
/* The ring must beat whatever the element itself is doing, including the hover
   and pressed states a mouse leaves behind. */
html[data-input="gamepad"] .gp-focus { position: relative; z-index: 2; }

/* No pointer is driving, so nothing should look like it is under one. Hover
   states on a controller read as a second, phantom selection fighting the
   real one. */
html[data-input="gamepad"] *:hover { cursor: default; }

/* Controller targets are selected, not aimed at, so they do not need to be
   bigger to be HIT — but they do need to be readable at a distance, and a
   32px row of settings is not. Every interactive row grows to the same floor
   a finger gets, which is the number both guidelines already agree on. */
html[data-input="gamepad"] .btn,
html[data-input="gamepad"] .side-nav a,
html[data-input="gamepad"] .setting-row,
html[data-input="gamepad"] .opt-row { min-height: var(--tap); }

/* ── on-screen controls belong to a finger ────────────────────────────────
   Hidden by the control layer itself (`TouchControls.setHidden`), which also
   retracts `--tc-band`. This is the belt to that braces: a stale layer left
   behind by a HUD mid-teardown must never sit under a controller player's
   view of the game. */
html[data-input="gamepad"] .touch-controls { display: none; }

/* ═══ television and console browsers ═════════════════════════════════════
   `data-device="tv"` is written by src/input/platform.js. The player is three
   metres away, holding a pad, with no pointer at all, and a great many
   televisions still overscan — so the outer edge of the panel is not a place
   anything may live.

   This is the only device rule in the stylesheet that changes SIZE globally,
   and it does it through the two variables the whole interface is already
   built on (`--text-scale`, `--ui-scale`) rather than by restyling anything. */
html[data-device="tv"] {
  /* A ten-foot multiplier ON TOP of the player's own Text size, never instead
     of it. `--text-scale` is written to this element as an inline style by
     src/sdk/settings.js — it is the player's setting, it outranks every
     stylesheet rule by construction, and a television is not a reason to
     ignore it. So the boost is its own token and multiplies theirs. */
  --tv-text-boost: 1.28;
  /* The title-safe margin every broadcast guideline uses: 5% of each edge.
     Added to the hardware safe-area insets rather than replacing them, so a
     console browser that reports its own insets is respected too. */
  --safe-t: calc(env(safe-area-inset-top, 0px) + 3.5vh);
  --safe-b: calc(env(safe-area-inset-bottom, 0px) + 3.5vh);
  --safe-l: calc(env(safe-area-inset-left, 0px) + 3.5vw);
  --safe-r: calc(env(safe-area-inset-right, 0px) + 3.5vw);
}
/* Both elements, because the base rule sets the size on both and `body` would
   otherwise win over an `html`-only override. */
html[data-device="tv"], html[data-device="tv"] body {
  font-size: calc(15px * var(--text-scale) * var(--tv-text-boost, 1));
}
html[data-device="tv"] .shell {
  padding: var(--safe-t) var(--safe-r) var(--safe-b) var(--safe-l);
}
html[data-device="tv"] .sidebar { height: auto; min-height: 100vh; }
/* A ten-foot menu is a short list of large rows. Nothing is removed — the
   same navigation, at a size that can be read across a room. */
html[data-device="tv"] .side-nav a { min-height: 56px; font-size: 1rem; }
html[data-device="tv"] .btn { min-height: 52px; padding-inline: 20px; }
html[data-device="tv"] .modal { max-width: min(760px, 80vw); }
/* The HUD's own corners already inset by `--safe-*`, so raising the tokens
   above moves every one of them at once — the reason they were made tokens. */
html[data-device="tv"] .toast-root, html[data-device="tv"] #toast-root {
  bottom: calc(var(--safe-b) + 12px); right: calc(var(--safe-r) + 12px);
}

/* ── the graphics report ──────────────────────────────────────────────────
   Settings -> Graphics. A short list of what the last 3D world managed to do,
   for the devices that have no console to open. */
.gfx-report .gfx-stages,
.gfx-report .gfx-previous { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.gfx-report .gfx-stage { font-variant-numeric: tabular-nums; font-size: 12px; color: var(--text-3); }
.gfx-report .gfx-stage.is-bad { color: var(--warn); }

/* A television has no touch and no mouse: the on-screen stick would be an
   unusable control drawn over the game. */
html[data-device="tv"] .touch-controls { display: none; }

/* ═══ on-screen controls for the 2D catalog ═══════════════════════════════
   The same layout contract as the 3D worlds' control layer
   (styles/nexus.css → .touch-controls): movement bottom-left, actions
   bottom-right, ONE control per grid cell so two can never be in a position to
   overlap, every size derived from `--tc-btn` so nothing is smaller than a
   thumb, and the whole layer inside the safe area.

   Sharing the tokens rather than the rules, because the two layers hold
   different controls — a d-pad quantised to four directions, lane pads for a
   rhythm game — but reserve the SAME band, which is what everything else near
   the bottom of the screen clears (`--tc-band`).

   What is drawn comes from the game's manifest; see src/sdk/touch-controls.js. */
.arcade-controls {
  position: absolute; inset: 0; z-index: 4;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr auto;
  grid-template-areas: 'free free' 'move actions';
  /* The layer itself is inert; only its controls take a press, so the game
     underneath keeps every tap that is not on one. That is what lets
     Flashpoint be aimed at by touching the play area. */
  pointer-events: none;
  padding: 0 calc(var(--tc-edge) + var(--safe-r)) calc(var(--tc-edge) + var(--safe-b)) calc(var(--tc-edge) + var(--safe-l));
  touch-action: none;
}
.arcade-controls.ac-hidden { display: none; }

/* A full-area press target — "hold anywhere to tether". Sits BEHIND the
   controls, so a game that has both still has working buttons. */
.ac-surface {
  grid-area: 1 / 1 / -1 / -1;
  pointer-events: auto;
  background: none; border: 0; padding: 0; margin: 0;
  color: var(--text-2); font: inherit; font-size: .78rem; letter-spacing: .08em;
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: calc(var(--safe-t) + 54px);
  -webkit-tap-highlight-color: transparent;
}
.ac-surface span {
  background: rgba(11, 11, 20, .55); border: 1px solid var(--line);
  border-radius: 999px; padding: 6px 14px;
  transition: opacity var(--fast);
}
.ac-surface.is-down span { opacity: .25; }

/* ── movement ───────────────────────────────────────────────────────────
   A floating stick: the base snaps to wherever the thumb lands, so there is no
   fixed target to find without looking. A miss on a fixed stick is not a
   smaller input, it is NO input — the touch never started on the control. */
.ac-move {
  grid-area: move;
  position: relative; pointer-events: auto;
  min-height: var(--tc-band);
  align-self: end;
}
.ac-stick-home {
  position: absolute; left: 50%; top: 50%; translate: -50% -50%;
  width: calc(var(--tc-stick) * 2); height: calc(var(--tc-stick) * 2);
  border-radius: 50%; border: 1px solid var(--line);
  background: rgba(11, 11, 20, .3);
  opacity: 0; transition: opacity var(--fast);
  display: grid; place-items: center;
}
.ac-stick-home.active { opacity: 1; }
.ac-stick {
  width: calc(var(--tc-stick) * .78); height: calc(var(--tc-stick) * .78);
  border-radius: 50%; background: rgba(200, 180, 228, .34);
  border: 1px solid var(--lavender);
}
/* A game that only moves along one axis says so, and gets a track rather than
   a circle — the shape tells the player what the control can do. */
.arcade-controls[data-move="horizontal"] .ac-stick-home { width: calc(var(--tc-stick) * 2.6); border-radius: 999px; }
.arcade-controls[data-move="vertical"] .ac-stick-home { height: calc(var(--tc-stick) * 2.6); border-radius: 999px; }

/* ── action buttons ─────────────────────────────────────────────────────
   A grid, so two buttons cannot collide, and sized from `--tc-btn` so they
   cannot become too small to hit. At most four: more than that under one thumb
   is a keyboard, and a worse one. */
.ac-actions {
  grid-area: actions;
  pointer-events: none;
  align-self: end; justify-self: end;
  display: grid; gap: var(--tc-gap);
  grid-template-columns: repeat(2, var(--tc-btn));
  justify-items: end; align-items: end;
}
.ac-btn {
  pointer-events: auto;
  width: var(--tc-btn); height: var(--tc-btn);
  min-width: var(--tap); min-height: var(--tap);
  border-radius: 50%; border: 1px solid var(--line-strong);
  background: rgba(11, 11, 20, .55);
  color: var(--text); font: inherit;
  display: grid; place-items: center; gap: 2px;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--fast), transform var(--fast);
}
.ac-btn .ac-glyph { display: grid; place-items: center; }
.ac-btn .ac-glyph svg { width: 40%; height: 40%; min-width: 20px; }
.ac-btn .ac-cap { font-size: clamp(9px, 2.6vmin, 12px); letter-spacing: .04em; line-height: 1; }
/* With a glyph the caption is a subtitle; without one it IS the button. */
.ac-btn:not(:has(.ac-glyph)) .ac-cap { font-size: clamp(10px, 3vmin, 14px); }
.ac-btn.is-down { background: rgba(200, 180, 228, .3); transform: scale(.94); }

/* ── lane pads ──────────────────────────────────────────────────────────
   For a rhythm game, where four discrete pads in a row are the instrument and
   a joystick is unplayable. They take the whole width, because hitting the
   right lane in time is the entire game. */
/* Lanes take the full width, so they need a ROW of their own — sharing the
   bottom row with the action buttons put a "Sync" button straight on top of
   the right-hand lane pad, which is the mis-tap a rhythm game can least
   afford. */
.arcade-controls:has(.ac-lanes) {
  grid-template-rows: 1fr auto auto;
  grid-template-areas: 'free free' 'extra extra' 'lanes lanes';
}
.ac-lanes {
  grid-area: lanes;
  pointer-events: none;
  align-self: end;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: var(--tc-gap);
}
.ac-lane {
  pointer-events: auto;
  min-height: max(var(--tc-btn), var(--tap));
  border-radius: var(--r); border: 1px solid var(--line-strong);
  background: rgba(11, 11, 20, .55); color: var(--text);
  font: inherit; font-size: clamp(15px, 5vmin, 24px);
  -webkit-tap-highlight-color: transparent;
  transition: background var(--fast);
}
.ac-lane.is-down { background: rgba(200, 180, 228, .34); }
/* A rhythm game's own action button sits in its own row above the pads. */
.arcade-controls:has(.ac-lanes) .ac-actions {
  grid-area: extra;
  justify-self: end; align-self: end;
  margin-bottom: var(--tc-gap);
}

/* A menu is on top: the controls beneath it are not the thing being pressed,
   and a jump button under a Resume button is a mis-tap waiting to happen. */
.game-shell.has-overlay .arcade-controls { display: none; }

/* A controller or a keyboard is driving — see GameShell.applyInputMethod.
   Belt to that braces, for a layer left behind by a shell mid-teardown. */
html[data-input="gamepad"] .arcade-controls,
html[data-input="keyboard"] .arcade-controls { display: none; }

/* ── checkout ────────────────────────────────────────────────────────────── */
/* The VC purchase dialog. The Payment Element inside `.checkout-element` is a
   Stripe-served iframe: it is themed through the appearance API in
   src/platform/checkout.js, not from here, because CSS cannot cross into it. */
/* Width comes from `.modal` (min(520px, 100%)), which is already inset by the
   safe area and the software keyboard. A min-width here would fight that and
   push the dialog off a narrow screen. */
.checkout { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.checkout-summary {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r); padding: 13px 15px;
}
.checkout-what { display: block; font-weight: 600; font-size: .95rem; }
.checkout-detail { display: block; font-size: .76rem; color: var(--text-3); margin-top: 2px; }
.checkout-price { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Reserved height, so the dialog does not jump when Stripe's iframe finishes
   loading and the buttons move out from under the pointer. */
.checkout-element { min-height: 190px; }
.checkout-element[hidden] { display: none; }
.checkout-status { margin: 0; font-size: .82rem; color: var(--text-2); min-height: 1.2em; }
.checkout-status.is-error { color: var(--danger); }
.checkout-status.is-warn { color: var(--warn); }
.checkout-status.is-good { color: var(--good); }
/* The busy states get a spinner rather than only a sentence, because the wait
   they cover is the one where a player wonders whether their click registered. */
.checkout-status.is-busy { color: var(--text); display: flex; align-items: center; gap: 8px; }
.checkout-status.is-busy::before {
  content: ''; width: 13px; height: 13px; flex: none; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--lavender);
  animation: checkout-spin .7s linear infinite;
}
@keyframes checkout-spin { to { transform: rotate(360deg); } }
/* The payment RETURN screen (src/platform/pages/payment-return.js) is a full
   page rather than a dialog, so it wears the auth panel's chrome — but it says
   the same set of things a checkout dialog says, and they have to read the same
   way. Same four states, same colours, same spinner. */
.auth-sub.is-error { color: var(--danger); }
.auth-sub.is-warn { color: var(--warn); }
.auth-sub.is-good { color: var(--good); }
.auth-sub.is-busy { color: var(--text); display: flex; align-items: center; gap: 8px; }
.auth-sub.is-busy::before {
  content: ''; width: 13px; height: 13px; flex: none; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--lavender);
  animation: checkout-spin .7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .auth-sub.is-busy::before { animation: none; border-top-color: var(--text-3); }
}
/* Motion is decoration here; the sentence beside it carries the meaning. */
@media (prefers-reduced-motion: reduce) {
  .checkout-status.is-busy::before { animation: none; border-top-color: var(--text-3); }
}
/* Paid states get a quiet panel, so "we have your money, the coins are coming"
   reads as a resolved state rather than as an error. */
.checkout[data-state="credited"] .checkout-status,
.checkout[data-state="delayed"] .checkout-status {
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r); padding: 11px 13px; line-height: 1.5;
}
.checkout-fineprint {
  display: flex; align-items: flex-start; gap: 7px;
  margin: 0; font-size: .74rem; line-height: 1.45; color: var(--text-3);
}
.checkout-fineprint svg { width: 14px; height: 14px; flex: none; margin-top: 2px; }

/* Console and TV browsers render at 1080p with the viewer sitting metres away,
   and phones give the dialog the full width. Both want the summary stacked
   rather than squeezed into two columns. */
@media (max-width: 560px) {
  .checkout-summary { flex-direction: column; align-items: flex-start; gap: 6px; }
  .checkout-price { font-size: 1.4rem; }
}
@media (pointer: coarse) {
  /* Stripe sizes its own controls inside the iframe; this is the room they need
     so a thumb does not land between the card fields and the wallet buttons. */
  .checkout-element { min-height: 230px; }
}
/* On a short viewport the reserved height is worth more as room to see the
   form than as anti-jump padding — the iframe grows to its own height anyway,
   and the body scrolls to it. */
@media (max-height: 720px) {
  .checkout { gap: 11px; }
  .checkout-element { min-height: 0; }
  .checkout-summary { padding: 10px 13px; }
  .checkout-fineprint { font-size: .7rem; }
}
