@charset "UTF-8";
/* ==========================================================================
   PALLWALL — "NEON GRID" DESIGN SYSTEM
   static/css/pallwall.css
   --------------------------------------------------------------------------
   The single stylesheet for the whole product. No build step, no webfonts,
   no external requests. Dark is the only committed theme (SPEC 7.2).

   HOW THIS FILE IS ORGANISED
     00  Reset & normalise
     01  Design tokens (:root)          <- SPEC 7.2 palette contract
     02  Base elements & typography
     03  Ambient cyberpunk treatment    (grid drift, scanlines, vignette)
     04  Utilities (.sr-only, layout helpers)
     05  App shell & page layout
     06  Top bar / nav / wordmark
     07  Buttons
     08  Forms, fields & validation
     09  Surfaces: cards, panels, neon frames
     10  Play screen layout
     11  Board canvas & overlays
     12  Player cards, wall pips, turn indicator
     13  Action bar (CONFIRM / CANCEL)
     14  Move log & history / replay
     15  Toasts & aria-live regions
     16  Modals & dialogs
     17  Board configurator (sliders, presets, live preview)
     18  Game list cards
     19  Rules page (prose)
     20  Settings page & toggle switches
     21  Audio toggle & volume popover
     22  Status pills, badges, code chips, skeletons
     23  No-JS fallback
     24  Print
     25  Reduced motion
     26  Keyframes

   CLASS-NAME CONTRACT (templates should use these; every one of them also has
   an element/ARIA-driven fallback so unclassed semantic markup still looks
   right):
     .topbar .topbar__inner .wordmark .nav .nav__link
     .page .page__head .page__title .page__lede
     .btn .btn--primary .btn--ghost .btn--danger .btn--icon .btn--sm .btn--lg
     .card .panel .panel__head .panel__title .panel__body .neon-frame
     .play-main .board-area .board-frame .board-canvas .board-overlay
     .side-rail .action-bar .action-bar__status .action-bar__confirm
     .player-card .player-card__name .pips .pip .turn-dot
     .move-log .move-log__row .move-log__ply .move-log__note
     #toasts .toast .toast--ok|--error|--info  .live-region
     .modal (or <dialog>) .modal__head .modal__body .modal__foot
     .configurator .config-preview .field .range-field .preset-grid .preset-chip
     .game-grid .game-card .mini-board .status-pill .short-code
     .prose .settings-group .setting-row .switch
     .audio-control .volume-popover
   State hooks: [data-open] [data-turn] [data-seat] [data-status] [data-valid]
                .is-open .is-active .is-turn .is-pending .is-spent [hidden]
   ========================================================================== */


/* ==========================================================================
   00 — RESET & NORMALISE
   ========================================================================== */

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

* {
  margin: 0;
}

html {
  /* body's overflow-x:hidden is ignored by mobile browsers; clip here does
     not create a scroll container and stops any stray overhang widening the
     layout viewport (which stretches every fixed background with it). */
  overflow-x: clip;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  -moz-tab-size: 2;
  tab-size: 2;
  background-color: #05060d;
  color-scheme: dark;
}

body {
  min-height: 100vh;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

img,
video {
  height: auto;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
}

p,
h1,
h2,
h3,
h4,
h5,
h6 {
  overflow-wrap: break-word;
}

ul[class],
ol[class] {
  list-style: none;
  padding: 0;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

[hidden] {
  display: none !important;
}

::selection {
  background: rgba(34, 224, 255, 0.28);
  color: #ffffff;
}


/* ==========================================================================
   01 — DESIGN TOKENS
   The palette block below is the SPEC 7.2 contract. Names and values are
   verbatim; nothing here may be renamed.

   CONTRAST AUDIT (WCAG 2.1 non-linear, against --bg #05060d / --bg-elev):
     --text      #d7e6ff  16.04 : 1  /  15.12 : 1   AAA body text
     --text-dim  #6f86b8   5.56 : 1  /   5.25 : 1   AA body text  (unchanged)
     --cyan      #22e0ff  12.69 : 1                 AAA
     --magenta   #ff2bd1   6.28 : 1                 AA
     --violet    #8b5cf6   4.78 : 1                 AA (borderline — see below)
     --lime      #9dff3c  16.18 : 1                 AAA
     --amber     #ffb020  11.06 : 1                 AAA
     --danger    #ff3b6b   5.87 : 1                 AA
   The spec's --text-dim was measured before use and PASSES 4.5:1, so it is
   kept exactly as specified. --violet is 4.78:1, which passes but leaves no
   headroom, so violet is used for wall/decorative chrome and --violet-text
   (#a78bfa, 7.43:1) is used any time violet must carry words. --text-mute
   (#54689a, 3.68:1) FAILS 4.5:1 by design and is restricted to non-text
   decoration (hairlines, disabled glyphs, spent pips) — never for content.
   ========================================================================== */

:root {
  /* ---- SPEC 7.2 palette (do not rename) ---------------------------------- */
  --bg: #05060d;
  --bg-elev: #0b0f1e;
  --grid-line: #14203c;
  --cyan: #22e0ff;
  --magenta: #ff2bd1;
  --violet: #8b5cf6;
  --lime: #9dff3c;
  --amber: #ffb020;
  --danger: #ff3b6b;
  --text: #d7e6ff;
  --text-dim: #6f86b8;
  --p0: var(--cyan);
  --p1: var(--magenta);
  --wall-glow: var(--violet);

  /* ---- Derived channel triplets (for rgba() composition) ----------------- */
  --bg-rgb: 5, 6, 13;
  --bg-elev-rgb: 11, 15, 30;
  --cyan-rgb: 34, 224, 255;
  --magenta-rgb: 255, 43, 209;
  --violet-rgb: 139, 92, 246;
  --lime-rgb: 157, 255, 60;
  --amber-rgb: 255, 176, 32;
  --danger-rgb: 255, 59, 107;
  --text-rgb: 215, 230, 255;
  --grid-rgb: 20, 32, 60;

  /* ---- Extended surface & ink ------------------------------------------- */
  --bg-sunken: #03040a;
  --surface: #0b0f1e;
  --surface-2: #101733;
  --surface-3: #16204a;
  --surface-glass: rgba(11, 15, 30, 0.72);
  --hairline: rgba(34, 224, 255, 0.14);
  --hairline-strong: rgba(34, 224, 255, 0.32);
  --hairline-soft: rgba(215, 230, 255, 0.08);
  --text-mute: #54689a;          /* decoration only — fails 4.5:1 on purpose */
  --violet-text: #a78bfa;        /* violet that is legible as text (7.43:1)  */
  --danger-text: #ff6b8f;        /* danger that is legible as text (7.47:1)  */
  --on-neon: #05060d;            /* ink placed on a saturated neon fill      */

  /* ---- Typography -------------------------------------------------------- */
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", "JetBrains Mono",
    "Roboto Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace;
  --font-sans: "Avenir Next", Avenir, "Segoe UI Variable Text", "Segoe UI",
    system-ui, -apple-system, Roboto, "Helvetica Neue", "Nimbus Sans", Arial,
    sans-serif;
  --font-display: var(--font-mono);

  --fs-3xs: 0.625rem;   /* 10px — pip labels, superscripts        */
  --fs-2xs: 0.6875rem;  /* 11px — micro labels, timestamps        */
  --fs-xs: 0.75rem;     /* 12px — HUD labels, table headers       */
  --fs-sm: 0.8125rem;   /* 13px — dense UI, captions              */
  --fs-md: 0.9375rem;   /* 15px — default UI text                 */
  --fs-base: 1rem;      /* 16px — prose                           */
  --fs-lg: 1.125rem;    /* 18px                                    */
  --fs-xl: 1.375rem;    /* 22px                                    */
  --fs-2xl: 1.75rem;    /* 28px                                    */
  --fs-3xl: 2.25rem;    /* 36px                                    */
  --fs-hero: clamp(2.25rem, 9vw, 4rem);

  --lh-tight: 1.15;
  --lh-snug: 1.35;
  --lh-normal: 1.55;
  --lh-loose: 1.75;

  --tracking-tight: -0.01em;
  --tracking-normal: 0;
  --tracking-wide: 0.06em;
  --tracking-wider: 0.14em;
  --tracking-widest: 0.24em;

  --fw-normal: 400;
  --fw-medium: 500;
  --fw-bold: 700;

  /* ---- Spacing scale (4px base) ------------------------------------------ */
  --space-0: 0;
  --space-1: 0.25rem;   /*  4px */
  --space-2: 0.5rem;    /*  8px */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-5: 1.5rem;    /* 24px */
  --space-6: 2rem;      /* 32px */
  --space-7: 3rem;      /* 48px */
  --space-8: 4rem;      /* 64px */
  --space-9: 6rem;      /* 96px */

  /* ---- Radii ------------------------------------------------------------- */
  --r-xs: 3px;
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 16px;
  --r-xl: 22px;
  --r-full: 999px;

  /* ---- Borders ----------------------------------------------------------- */
  --border: 1px solid var(--hairline);
  --border-soft: 1px solid var(--hairline-soft);
  --border-strong: 1px solid var(--hairline-strong);

  /* ---- Elevation & glow -------------------------------------------------- */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.5);
  --shadow-2: 0 6px 18px -6px rgba(0, 0, 0, 0.7);
  --shadow-3: 0 18px 48px -14px rgba(0, 0, 0, 0.85);
  --shadow-inset: inset 0 1px 0 rgba(215, 230, 255, 0.05);

  --glow-cyan: 0 0 14px rgba(var(--cyan-rgb), 0.32),
    0 0 44px rgba(var(--cyan-rgb), 0.12);
  --glow-magenta: 0 0 14px rgba(var(--magenta-rgb), 0.32),
    0 0 44px rgba(var(--magenta-rgb), 0.12);
  --glow-violet: 0 0 14px rgba(var(--violet-rgb), 0.34),
    0 0 44px rgba(var(--violet-rgb), 0.14);
  --glow-danger: 0 0 14px rgba(var(--danger-rgb), 0.34),
    0 0 44px rgba(var(--danger-rgb), 0.12);
  --glow-soft: 0 0 0 1px var(--hairline), 0 8px 30px -12px rgba(0, 0, 0, 0.9);

  --text-glow-cyan: 0 0 6px rgba(var(--cyan-rgb), 0.55),
    0 0 22px rgba(var(--cyan-rgb), 0.28);
  --text-glow-magenta: 0 0 6px rgba(var(--magenta-rgb), 0.5),
    0 0 22px rgba(var(--magenta-rgb), 0.25);

  /* ---- Focus ------------------------------------------------------------- */
  --focus-color: var(--cyan);
  --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--focus-color),
    0 0 18px rgba(var(--cyan-rgb), 0.45);

  /* ---- Motion ------------------------------------------------------------ */
  --dur-1: 110ms;
  --dur-2: 190ms;
  --dur-3: 320ms;
  --dur-4: 520ms;
  --dur-slow: 900ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in: cubic-bezier(0.7, 0, 0.84, 0);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
  --ease-linear: linear;

  /* ---- Z-index scale ----------------------------------------------------- */
  --z-under-3: -3;     /* static horizon washes  (html::before)  */
  --z-under-2: -2;     /* drifting coarse grid   (html::after)   */
  --z-under: -1;       /* drifting fine grid     (body::before)  */
  --z-base: 0;
  --z-raised: 10;
  --z-sticky: 30;
  --z-nav: 40;
  --z-fx: 60;          /* scanlines / vignette overlay */
  --z-actionbar: 80;
  --z-popover: 120;
  --z-modal: 200;
  --z-toast: 300;

  /* ---- Sizing & safe areas ----------------------------------------------- */
  --sa-t: env(safe-area-inset-top, 0px);
  --sa-r: env(safe-area-inset-right, 0px);
  --sa-b: env(safe-area-inset-bottom, 0px);
  --sa-l: env(safe-area-inset-left, 0px);

  --tap: 44px;                 /* minimum touch target */
  --topbar-h: 54px;
  --actionbar-h: 76px;
  --rail-w: clamp(288px, 27vw, 384px);
  --page-max: 1180px;
  --prose-max: 68ch;
  --board-gutter: var(--space-3);

  /* ---- Ambient FX intensity ----------------------------------------------
     --fx-scanline-alpha is the alpha of the 1px dark line in a 3px period, so
     the average darkening of the page is roughly a third of it. 0.16 => ~5%,
     which is visible as texture but changes no contrast ratio meaningfully. */
  --fx-scanline-alpha: 0.16;
  --fx-grid-opacity: 0.55;
  --fx-vignette: 0.62;

  /* ---- Per-seat colour (overridden on scoped components) ------------------ */
  --seat-color: var(--cyan);
  --seat-rgb: var(--cyan-rgb);
}

/* Seat colour scoping — any element can carry the seat identity. */
[data-seat="0"],
.seat-0 {
  --seat-color: var(--p0);
  --seat-rgb: var(--cyan-rgb);
}

[data-seat="1"],
.seat-1 {
  --seat-color: var(--p1);
  --seat-rgb: var(--magenta-rgb);
}


/* ==========================================================================
   02 — BASE ELEMENTS & TYPOGRAPHY
   Monospace carries numerals, labels, HUD and codes; the geometric sans stack
   carries prose. No webfonts anywhere (offline-first).
   ========================================================================== */

body {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
  color: var(--text);
  background-color: transparent;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-normal);
  padding-left: var(--sa-l);
  padding-right: var(--sa-r);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text);
}

h1 {
  font-size: var(--fs-2xl);
  letter-spacing: var(--tracking-wider);
  text-shadow: var(--text-glow-cyan);
}

h2 {
  font-size: var(--fs-xl);
  letter-spacing: var(--tracking-wide);
}

h3 {
  font-size: var(--fs-lg);
}

h4 {
  font-size: var(--fs-md);
}

h5,
h6 {
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-widest);
  color: var(--text-dim);
}

p,
li,
dd,
dt,
figcaption,
blockquote {
  font-family: var(--font-sans);
}

a {
  color: var(--cyan);
  text-decoration: none;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
  border-radius: var(--r-xs);
  transition: color var(--dur-1) var(--ease-out),
    text-shadow var(--dur-1) var(--ease-out);
}

a:hover {
  color: #7ceeff;
  text-decoration: underline;
  text-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.5);
}

strong,
b {
  font-weight: var(--fw-bold);
  color: #eaf3ff;
}

em,
i {
  font-style: italic;
}

small,
.text-small {
  font-size: var(--fs-xs);
}

code,
kbd,
samp,
pre {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

code {
  padding: 0.1em 0.36em;
  border-radius: var(--r-xs);
  background: rgba(var(--cyan-rgb), 0.08);
  border: 1px solid var(--hairline);
  color: #a9f2ff;
}

pre {
  padding: var(--space-4);
  border-radius: var(--r-md);
  background: var(--bg-sunken);
  border: var(--border);
  overflow-x: auto;
}

pre code {
  padding: 0;
  background: none;
  border: 0;
}

kbd {
  display: inline-block;
  min-width: 1.9em;
  padding: 0.15em 0.45em;
  text-align: center;
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--text);
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
  border: 1px solid var(--hairline-strong);
  border-bottom-width: 2px;
  border-radius: var(--r-sm);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.6);
}

hr {
  height: 1px;
  border: 0;
  background: linear-gradient(
    90deg,
    transparent,
    var(--hairline-strong),
    transparent
  );
  margin-block: var(--space-5);
}

abbr[title] {
  text-decoration: underline dotted;
  cursor: help;
}

mark {
  background: rgba(var(--amber-rgb), 0.22);
  color: var(--amber);
  padding: 0 0.2em;
  border-radius: var(--r-xs);
}

time,
.numeric {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* Global focus treatment. Pointer users never see it; keyboard users always
   do. Both an outline (shape-following in modern engines) and a glow. */
:focus {
  outline: none;
}

:focus-visible {
  outline: 2px solid var(--focus-color);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* Anything genuinely interactive also gets the neon halo. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  box-shadow: var(--focus-ring);
}

/* Fallback for engines without :focus-visible support (older iOS Safari). */
@supports not selector(:focus-visible) {
  a:focus,
  button:focus,
  input:focus,
  select:focus,
  textarea:focus,
  summary:focus,
  [tabindex]:focus {
    outline: 2px solid var(--focus-color);
    outline-offset: 2px;
  }
}

::placeholder {
  color: var(--text-mute);
  opacity: 1;
}

/* Icon convention: inline SVG inherits currentColor and sizes to the text. */
.icon {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.icon--filled {
  fill: currentColor;
  stroke: none;
}

/* Section eyebrow / HUD label. */
.label,
.eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-dim);
}

/* Sparing neon: headings and the wordmark only. */
.neon,
.neon-cyan {
  color: var(--cyan);
  text-shadow: var(--text-glow-cyan);
}

.neon-magenta {
  color: var(--magenta);
  text-shadow: var(--text-glow-magenta);
}

.dim {
  color: var(--text-dim);
}

.mono {
  font-family: var(--font-mono);
}

.sans {
  font-family: var(--font-sans);
}


/* ==========================================================================
   03 — AMBIENT CYBERPUNK TREATMENT
   html::before = static horizon washes (never animate).
   html::after  = coarse grid, drifting 240px/60s on the compositor.
   body::before = the drifting fine grid, moved with transform (compositor only).
   body::after  = scanlines + CRT vignette (above content, pointer-transparent).
   All are decorative and are switched off under prefers-reduced-motion
   (drift stops; the static texture remains, at reduced strength).

   PERFORMANCE — why this is two elements and not one.
   These six layers used to live on a single full-viewport `body::before` whose
   `background-position` was animated. `background-position` is not a
   compositable property, so every frame repainted and re-rasterised all six
   gradients across the whole viewport: measured at ~150 ms/frame (6-7 fps) and
   it starved the audio scheduler's setTimeout by 150-950 ms.
   The stack is therefore split by whether it moves:
     - the two radial washes never moved in the keyframes, and the coarse grid
       is the layer we deliberately hold still, so both sit on a static element
       that is painted once;
     - only the fine grid drifts, and it drifts via `transform: translate3d()`,
       which the compositor runs on the GPU with no repaint at all.
   Paint order is unchanged: fine grid over coarse grid over the washes,
   and both grids keep the 5:1 speed ratio (parallax) they had at HEAD.
   ========================================================================== */

/* Static backdrop: the two horizon washes. Exactly the viewport, no overhang. */
html::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-under-3);
  pointer-events: none;
  opacity: var(--fx-grid-opacity);
  background-color: var(--bg);
  background-image:
    /* horizon wash */
    /* Same two washes as the old inset:-10% box, re-expressed against the
       viewport so this layer needs no overhang: centres (50%,-22%) and
       (2%,122%), sizes 144%x96% and 120%x84%. */
    radial-gradient(
      144% 96% at 50% -22%,
      rgba(var(--violet-rgb), 0.16),
      transparent 62%
    ),
    radial-gradient(
      120% 84% at 2% 122%,
      rgba(var(--cyan-rgb), 0.1),
      transparent 60%
    );
  background-repeat: no-repeat, no-repeat;
}

/* Drifting coarse grid. Travels +240px x/y over 60s — one full coarse period,
   so the loop is seamless. At HEAD this layer moved 240px while the fine grid
   moved 48px over the same 60s, i.e. 5x faster; that speed difference IS the
   parallax in the backdrop, and 4px/s is the more perceptible of the two
   motions. It therefore gets its own composited layer rather than being frozen.
   Needs >= 240px of overhang past the top/left edges; 480px (two periods) is
   used, in px so it is correct at every viewport size. */
html::after {
  content: "";
  position: fixed;
  top: -480px;
  left: -480px;
  right: 0;
  bottom: 0;
  z-index: var(--z-under-2);
  pointer-events: none;
  opacity: var(--fx-grid-opacity);
  background-image:
    /* coarse grid */
    repeating-linear-gradient(
      to right,
      rgba(var(--cyan-rgb), 0.05) 0 1px,
      transparent 1px 240px
    ),
    repeating-linear-gradient(
      to bottom,
      rgba(var(--violet-rgb), 0.05) 0 1px,
      transparent 1px 240px
    );
  background-repeat: repeat;
  animation: pw-grid-drift-coarse 60s linear infinite;
  will-change: transform;
}

/* Drifting fine grid. It travels +48px x/y over 60s — one full grid period, so
   the loop is seamless — which means it needs at least 48px of overhang past
   the top and left viewport edges or the drift would expose a bare edge.
   96px (two periods) is used, in px rather than %, so the overhang is correct
   at every viewport size (the old `inset: -10%` was only 48px at 480px wide). */
body::before {
  content: "";
  position: fixed;
  top: -96px;
  left: -96px;
  right: 0;
  bottom: 0;
  z-index: var(--z-under);
  pointer-events: none;
  opacity: var(--fx-grid-opacity);
  background-image:
    /* fine grid */
    repeating-linear-gradient(
      to right,
      rgba(var(--grid-rgb), 0.85) 0 1px,
      transparent 1px 48px
    ),
    repeating-linear-gradient(
      to bottom,
      rgba(var(--grid-rgb), 0.85) 0 1px,
      transparent 1px 48px
    );
  background-repeat: repeat;
  animation: pw-grid-drift 60s linear infinite;
  will-change: transform;
}

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-fx);
  pointer-events: none;
  background-image:
    /* scanlines — one dark line every 3px, kept faint enough that body text
       loses no measurable contrast */
    repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, var(--fx-scanline-alpha)) 0 1px,
      transparent 1px 3px
    ),
    /* CRT vignette */
    radial-gradient(
      130% 105% at 50% 50%,
      transparent 52%,
      rgba(0, 0, 0, 0.3) 82%,
      rgba(0, 0, 0, var(--fx-vignette)) 100%
    );
  background-repeat: repeat, no-repeat;
  background-size: 100% 3px, 100% 100%;
}

/* Screens that want no texture at all (e.g. a data-dense admin view). */
.no-fx::before,
.no-fx::after {
  display: none;
}

/* The static half of the backdrop hangs off <html>, so .no-fx on <body> has to
   reach up for it. Own rule: if :has() is unsupported the selector is dropped
   without taking the rule above with it. */
html:has(> body.no-fx)::before,
html:has(> body.no-fx)::after {
  display: none;
}


/* ==========================================================================
   04 — UTILITIES
   ========================================================================== */

.sr-only,
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link: hidden until focused, then a real neon button. */
.skip-link {
  position: absolute;
  top: calc(var(--sa-t) + var(--space-2));
  left: var(--space-3);
  z-index: calc(var(--z-toast) + 1);
  padding: var(--space-2) var(--space-4);
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--on-neon);
  background: var(--cyan);
  border-radius: var(--r-sm);
  transform: translateY(-200%);
  transition: transform var(--dur-2) var(--ease-out);
}

.skip-link:focus,
.skip-link:focus-visible {
  transform: translateY(0);
  text-decoration: none;
  color: var(--on-neon);
}

.stack > * + * {
  margin-top: var(--stack-gap, var(--space-4));
}

.stack-sm {
  --stack-gap: var(--space-2);
}

.stack-lg {
  --stack-gap: var(--space-6);
}

.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cluster-gap, var(--space-3));
}

.cluster--tight {
  --cluster-gap: var(--space-2);
}

.cluster--between {
  justify-content: space-between;
}

.grow {
  flex: 1 1 auto;
  min-width: 0;
}

.center {
  text-align: center;
}

.right {
  text-align: right;
}

.nowrap {
  white-space: nowrap;
}

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.full {
  width: 100%;
}

.divider {
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    var(--hairline-strong),
    transparent
  );
}

.scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}

/* Thin neon scrollbars where the engine allows them. */
.scroll-x::-webkit-scrollbar,
.scroll-y::-webkit-scrollbar,
.side-rail::-webkit-scrollbar,
.move-log::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

.scroll-x::-webkit-scrollbar-track,
.scroll-y::-webkit-scrollbar-track,
.side-rail::-webkit-scrollbar-track,
.move-log::-webkit-scrollbar-track {
  background: transparent;
}

.scroll-x::-webkit-scrollbar-thumb,
.scroll-y::-webkit-scrollbar-thumb,
.side-rail::-webkit-scrollbar-thumb,
.move-log::-webkit-scrollbar-thumb {
  background: rgba(var(--cyan-rgb), 0.22);
  border-radius: var(--r-full);
}

.scroll-x::-webkit-scrollbar-thumb:hover,
.scroll-y::-webkit-scrollbar-thumb:hover,
.side-rail::-webkit-scrollbar-thumb:hover,
.move-log::-webkit-scrollbar-thumb:hover {
  background: rgba(var(--cyan-rgb), 0.4);
}

.scroll-y {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  scrollbar-width: thin;
}


/* ==========================================================================
   05 — APP SHELL & PAGE LAYOUT
   ========================================================================== */

.app,
.app-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  position: relative;
  z-index: var(--z-base);
}

main {
  display: block;
}

.page {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-5) var(--space-4)
    calc(var(--space-8) + var(--sa-b));
}

.page--narrow {
  max-width: 720px;
}

.page--wide {
  max-width: 1400px;
}

.page__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-4);
  border-bottom: var(--border);
}

.page__title {
  font-size: var(--fs-2xl);
  margin: 0;
}

.page__lede {
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  color: var(--text-dim);
  max-width: var(--prose-max);
  margin-top: var(--space-2);
}

.page__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* Generic two-column split for content pages. */
.split {
  display: grid;
  gap: var(--space-5);
}

@media (min-width: 860px) {
  .split {
    grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
    align-items: start;
  }

  .split--even {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Site footer. */
.site-footer {
  margin-top: auto;
  padding: var(--space-5) var(--space-4)
    calc(var(--space-5) + var(--sa-b));
  border-top: var(--border);
  color: var(--text-dim);
  font-size: var(--fs-xs);
  text-align: center;
  letter-spacing: var(--tracking-wide);
}

.site-footer a {
  color: var(--text-dim);
}

.site-footer a:hover {
  color: var(--cyan);
}


/* ==========================================================================
   06 — TOP BAR / NAV / WORDMARK
   Sticky (not fixed) so no page needs offset compensation; visually identical.
   The nav scrolls horizontally on narrow screens instead of collapsing into a
   JS-dependent hamburger — it must work with JS disabled.
   ========================================================================== */

.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  padding-top: var(--sa-t);
  background: linear-gradient(
    180deg,
    rgba(var(--bg-rgb), 0.94),
    rgba(var(--bg-rgb), 0.82)
  );
  border-bottom: 1px solid var(--hairline);
  box-shadow: 0 1px 0 rgba(var(--cyan-rgb), 0.06),
    0 10px 30px -22px rgba(0, 0, 0, 0.95);
}

@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .topbar {
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
    background: linear-gradient(
      180deg,
      rgba(var(--bg-rgb), 0.78),
      rgba(var(--bg-rgb), 0.6)
    );
  }
}

/* Animated neon hairline under the bar.
   The gradient sits on a box twice the bar's width and is translated rather
   than having its background-position animated, so it composites instead of
   repainting a full-width strip of the (backdrop-filtered) bar every frame.
   The overhang runs off the right of the viewport; body's overflow-x: hidden
   propagates to the viewport and clips it, exactly as it does for .nav. */
.topbar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(var(--cyan-rgb), 0.6) 20%,
    rgba(var(--violet-rgb), 0.6) 50%,
    rgba(var(--magenta-rgb), 0.6) 80%,
    transparent 100%
  );
  /* Animated via background-position on purpose. It is a 1px-tall line, so
     the repaint is negligible - and the transform-based alternative needs an
     element wider than the bar, which body's overflow-x:hidden clips on
     desktop but NOT on mobile, where it made the page twice the viewport wide
     and let the whole game scroll away. Never oversize this element. */
  background-size: 200% 100%;
  animation: pw-sweep 4.5s linear infinite;
  pointer-events: none;
}

.topbar__inner {
  max-width: var(--page-max);
  margin-inline: auto;
  min-height: var(--topbar-h);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-inline: var(--space-3);
}

/* Wordmark — the one place with unapologetic neon. */
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  flex: none;
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text);
  text-decoration: none;
  padding: var(--space-2) var(--space-1);
  border-radius: var(--r-sm);
  white-space: nowrap;
}

.wordmark:hover {
  text-decoration: none;
  color: var(--text);
}

.wordmark__mark {
  width: 1.35em;
  height: 1.35em;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  border: 1px solid rgba(var(--cyan-rgb), 0.5);
  background: linear-gradient(
    135deg,
    rgba(var(--cyan-rgb), 0.2),
    rgba(var(--magenta-rgb), 0.16)
  );
  box-shadow: inset 0 0 12px rgba(var(--cyan-rgb), 0.28), var(--glow-violet);
  font-size: 0.62em;
  letter-spacing: 0;
  color: var(--cyan);
}

.wordmark__text {
  background-image: linear-gradient(
    92deg,
    var(--cyan) 0%,
    #cfefff 38%,
    var(--magenta) 78%,
    var(--cyan) 100%
  );
  background-size: 220% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: pw-sweep 14s linear infinite;
  text-shadow: none;
}

/* Engines that cannot clip background to text still get a readable wordmark. */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .wordmark__text {
    color: var(--cyan);
    -webkit-text-fill-color: currentColor;
    text-shadow: var(--text-glow-cyan);
  }
}

.wordmark__suffix {
  color: var(--text-dim);
  font-size: 0.62em;
  letter-spacing: var(--tracking-widest);
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-inline-start: auto;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-block: var(--space-1);
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 12px,
    #000 calc(100% - 12px),
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 12px,
    #000 calc(100% - 12px),
    transparent 100%
  );
}

.nav::-webkit-scrollbar {
  display: none;
}

.nav__link,
.nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0 var(--space-3);
  border-radius: var(--r-sm);
  color: var(--text-dim);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
}

.nav__link:hover,
.nav a:hover {
  color: var(--text);
  background: rgba(var(--cyan-rgb), 0.08);
  text-decoration: none;
  text-shadow: none;
}

.nav__link[aria-current],
.nav a[aria-current],
.nav__link.is-active {
  color: var(--cyan);
  background: rgba(var(--cyan-rgb), 0.1);
}

.nav__link[aria-current]::after,
.nav a[aria-current]::after,
.nav__link.is-active::after {
  content: "";
  position: absolute;
  left: var(--space-3);
  right: var(--space-3);
  bottom: 4px;
  height: 2px;
  border-radius: var(--r-full);
  background: var(--cyan);
  box-shadow: 0 0 10px rgba(var(--cyan-rgb), 0.8);
}

.topbar__tools {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}

/* Compact identity chip in the bar (user or guest). */
.identity {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--r-full);
  border: var(--border);
  background: rgba(var(--bg-elev-rgb), 0.7);
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wide);
  color: var(--text-dim);
  max-width: 12ch;
}

.identity__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (min-width: 700px) {
  .topbar__inner {
    padding-inline: var(--space-4);
  }

  .wordmark {
    font-size: var(--fs-xl);
  }

  .identity {
    max-width: 20ch;
  }
}


/* ==========================================================================
   07 — BUTTONS
   .btn is the base; --primary (neon fill), --ghost (outline), --danger,
   --icon (square) are the variants. Every one clears 44px on touch.
   ========================================================================== */

.btn,
button.btn,
a.btn,
input[type="submit"],
input[type="button"],
input[type="reset"] {
  --btn-bg: rgba(var(--bg-elev-rgb), 0.9);
  --btn-fg: var(--text);
  --btn-border: var(--hairline-strong);
  --btn-glow: none;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0 var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  color: var(--btn-fg);
  background: var(--btn-bg);
  border: 1px solid var(--btn-border);
  border-radius: var(--r-md);
  box-shadow: var(--btn-glow), var(--shadow-inset);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-1) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out),
    background-color var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}

.btn:hover,
input[type="submit"]:hover {
  text-decoration: none;
  color: var(--btn-fg);
  border-color: rgba(var(--cyan-rgb), 0.5);
  box-shadow: var(--btn-glow), 0 0 0 1px rgba(var(--cyan-rgb), 0.16);
}

.btn:active,
input[type="submit"]:active {
  transform: translateY(1px) scale(0.99);
}

.btn:disabled,
.btn[aria-disabled="true"],
.btn.is-disabled,
input[type="submit"]:disabled {
  opacity: 0.42;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
  filter: saturate(0.5);
}

/* --- Primary: solid neon, dark ink (12.7:1) ------------------------------ */
.btn--primary,
input[type="submit"] {
  --btn-fg: var(--on-neon);
  --btn-border: transparent;
  --btn-glow: 0 0 18px rgba(var(--cyan-rgb), 0.35),
    0 10px 26px -14px rgba(var(--cyan-rgb), 0.7);
  background-image: linear-gradient(135deg, #5ff0ff 0%, var(--cyan) 46%, #14b9df 100%);
  background-color: var(--cyan);
  font-weight: var(--fw-bold);
  overflow: hidden;
}

/* Specular sweep across primary buttons. The gradient box is 260% of the button
   (what background-size used to be) and is translated instead of having its
   background-position animated; .btn--primary is overflow:hidden so it clips. */
.btn--primary::after,
input[type="submit"]::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 260%;
  background: linear-gradient(
    100deg,
    transparent 30%,
    rgba(255, 255, 255, 0.5) 46%,
    transparent 62%
  );
  transform: translate3d(-98.5%, 0, 0);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
}

.btn--primary:hover::after,
.btn--primary:focus-visible::after,
input[type="submit"]:hover::after {
  opacity: 0.5;
  animation: pw-specular 900ms var(--ease-out) 1;
  will-change: transform;
}

.btn--primary:hover,
input[type="submit"]:hover {
  --btn-glow: 0 0 26px rgba(var(--cyan-rgb), 0.5),
    0 12px 30px -12px rgba(var(--cyan-rgb), 0.8);
  border-color: transparent;
  filter: brightness(1.06);
}

/* --- Ghost: outline only -------------------------------------------------- */
.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--text);
  --btn-border: var(--hairline-strong);
  box-shadow: none;
}

.btn--ghost:hover {
  --btn-bg: rgba(var(--cyan-rgb), 0.09);
  --btn-fg: var(--cyan);
  border-color: rgba(var(--cyan-rgb), 0.6);
}

/* --- Quiet: no chrome until hover ---------------------------------------- */
.btn--quiet {
  --btn-bg: transparent;
  --btn-fg: var(--text-dim);
  --btn-border: transparent;
  box-shadow: none;
}

.btn--quiet:hover {
  --btn-bg: rgba(var(--text-rgb), 0.06);
  --btn-fg: var(--text);
}

/* --- Danger --------------------------------------------------------------- */
.btn--danger {
  --btn-bg: rgba(var(--danger-rgb), 0.12);
  --btn-fg: var(--danger-text);
  --btn-border: rgba(var(--danger-rgb), 0.5);
}

.btn--danger:hover {
  --btn-bg: rgba(var(--danger-rgb), 0.2);
  --btn-fg: #ffd0dc;
  border-color: var(--danger);
  box-shadow: var(--glow-danger);
}

.btn--danger:focus-visible {
  --focus-color: var(--danger);
}

.btn--danger-solid {
  --btn-bg: var(--danger);
  --btn-fg: var(--on-neon);
  --btn-border: transparent;
  background-image: linear-gradient(135deg, #ff6d92, var(--danger));
  --btn-glow: 0 0 18px rgba(var(--danger-rgb), 0.4);
}

/* --- Success / accent variants ------------------------------------------- */
.btn--lime {
  --btn-bg: rgba(var(--lime-rgb), 0.12);
  --btn-fg: var(--lime);
  --btn-border: rgba(var(--lime-rgb), 0.45);
}

.btn--violet {
  --btn-bg: rgba(var(--violet-rgb), 0.14);
  --btn-fg: var(--violet-text);
  --btn-border: rgba(var(--violet-rgb), 0.5);
}

/* --- Sizes ---------------------------------------------------------------- */
.btn--sm {
  min-height: 36px;
  padding: 0 var(--space-3);
  font-size: var(--fs-2xs);
  border-radius: var(--r-sm);
}

.btn--lg {
  min-height: 56px;
  padding: 0 var(--space-6);
  font-size: var(--fs-md);
  border-radius: var(--r-lg);
}

.btn--block,
.btn--full {
  display: flex;
  width: 100%;
}

/* Coarse pointers get the full 44px on every button, small ones included. */
@media (pointer: coarse) {
  .btn--sm {
    min-height: var(--tap);
    padding-inline: var(--space-4);
  }
}

/* --- Icon button ---------------------------------------------------------- */
.btn--icon,
.icon-btn {
  width: var(--tap);
  height: var(--tap);
  min-height: var(--tap);
  padding: 0;
  display: inline-grid;
  place-items: center;
  border-radius: var(--r-md);
  color: var(--text-dim);
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out);
}

.btn--icon:hover,
.icon-btn:hover {
  color: var(--cyan);
  background: rgba(var(--cyan-rgb), 0.1);
  border-color: rgba(var(--cyan-rgb), 0.3);
}

.btn--icon .icon,
.icon-btn .icon {
  width: 20px;
  height: 20px;
}

.btn--icon[aria-pressed="true"],
.icon-btn[aria-pressed="true"],
.icon-btn.is-active {
  color: var(--cyan);
  background: rgba(var(--cyan-rgb), 0.14);
  border-color: rgba(var(--cyan-rgb), 0.45);
  box-shadow: inset 0 0 12px rgba(var(--cyan-rgb), 0.16);
}

/* --- Busy / transmitting state ------------------------------------------- */
.btn.is-busy,
.btn[data-busy="true"] {
  color: transparent !important;
  pointer-events: none;
  position: relative;
}

.btn.is-busy::before,
.btn[data-busy="true"]::before {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(var(--bg-rgb), 0.35);
  border-top-color: currentColor;
  color: var(--on-neon);
  animation: pw-spin 700ms linear infinite;
}

.btn--ghost.is-busy::before,
.btn--quiet.is-busy::before,
.btn--danger.is-busy::before {
  border-color: rgba(var(--text-rgb), 0.2);
  border-top-color: var(--cyan);
}

/* Button group / segmented control. */
.btn-group {
  display: inline-flex;
  padding: 3px;
  gap: 3px;
  border-radius: var(--r-md);
  border: var(--border);
  background: rgba(var(--bg-rgb), 0.6);
}

.btn-group .btn {
  border-radius: var(--r-sm);
  border-color: transparent;
  background: transparent;
  box-shadow: none;
  min-height: 38px;
}

.btn-group .btn[aria-pressed="true"],
.btn-group .btn.is-active {
  background: rgba(var(--cyan-rgb), 0.16);
  border-color: rgba(var(--cyan-rgb), 0.4);
  color: var(--cyan);
}


/* ==========================================================================
   08 — FORMS, FIELDS & VALIDATION
   Styles both hand-written markup (.field) and Django's default form output
   (label / input / ul.errorlist / .helptext / ul.messages).
   ========================================================================== */

form {
  min-width: 0;
}

.form,
.form-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.field,
.form-row,
form p {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

label,
.field__label {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-dim);
}

.field__label .req,
label .req {
  color: var(--magenta);
  margin-inline-start: 0.25em;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="url"],
input[type="tel"],
input[type="number"],
input[type="date"],
input[type="datetime-local"],
select,
textarea,
.input {
  width: 100%;
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--text);
  background: rgba(var(--bg-rgb), 0.75);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-md);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.55);
  transition: border-color var(--dur-1) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out),
    background-color var(--dur-1) var(--ease-out);
  appearance: none;
  -webkit-appearance: none;
}

textarea {
  min-height: 7rem;
  line-height: var(--lh-normal);
  resize: vertical;
}

input:hover:not(:disabled),
select:hover:not(:disabled),
textarea:hover:not(:disabled) {
  border-color: rgba(var(--cyan-rgb), 0.4);
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--cyan);
  background: rgba(var(--bg-elev-rgb), 0.9);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.55),
    0 0 0 3px rgba(var(--cyan-rgb), 0.18);
}

input:disabled,
select:disabled,
textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Native select needs its own arrow once appearance is stripped. */
select {
  padding-inline-end: var(--space-6);
  background-image: linear-gradient(45deg, transparent 50%, var(--cyan) 50%),
    linear-gradient(135deg, var(--cyan) 50%, transparent 50%);
  background-position: calc(100% - 18px) calc(50% + 2px),
    calc(100% - 13px) calc(50% + 2px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

select option {
  background: var(--bg-elev);
  color: var(--text);
}

/* Number inputs: keep the spinners out of the neon aesthetic. */
input[type="number"] {
  -moz-appearance: textfield;
}

input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
}

/* Native checkbox & radio, restyled. */
input[type="checkbox"]:not(.switch):not([role="switch"]),
input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  min-height: 0;
  flex: none;
  display: inline-grid;
  place-content: center;
  margin: 0;
  padding: 0;
  background: rgba(var(--bg-rgb), 0.9);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-xs);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out),
    box-shadow var(--dur-1) var(--ease-out);
}

input[type="radio"] {
  border-radius: 50%;
}

input[type="checkbox"]:not(.switch):not([role="switch"])::before {
  content: "";
  width: 10px;
  height: 10px;
  transform: scale(0);
  transform-origin: center;
  transition: transform var(--dur-1) var(--ease-spring);
  background: var(--cyan);
  clip-path: polygon(14% 46%, 0 62%, 39% 100%, 100% 22%, 84% 8%, 38% 68%);
  box-shadow: 0 0 8px rgba(var(--cyan-rgb), 0.8);
}

input[type="radio"]::before {
  content: "";
  width: 9px;
  height: 9px;
  border-radius: 50%;
  transform: scale(0);
  transition: transform var(--dur-1) var(--ease-spring);
  background: var(--cyan);
  box-shadow: 0 0 8px rgba(var(--cyan-rgb), 0.8);
}

input[type="checkbox"]:checked,
input[type="radio"]:checked {
  border-color: var(--cyan);
  background: rgba(var(--cyan-rgb), 0.12);
}

input[type="checkbox"]:not(.switch):not([role="switch"]):checked::before,
input[type="radio"]:checked::before {
  transform: scale(1);
}

/* Checkbox/radio rows keep a 44px hit area via the label. */
.check,
.check-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap);
  cursor: pointer;
  text-transform: none;
  letter-spacing: var(--tracking-normal);
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  color: var(--text);
}

.check input,
.check-row input {
  flex: none;
}

/* Help text & errors. */
.helptext,
.field__help,
.help {
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  color: var(--text-dim);
  line-height: var(--lh-snug);
}

.errorlist,
.field__error,
.form-error {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  list-style: none;
  padding: 0;
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  color: var(--danger-text);
}

.errorlist li,
.field__error {
  position: relative;
  padding-inline-start: 1.4em;
}

.errorlist li::before,
.field__error::before {
  content: "!";
  position: absolute;
  left: 0;
  top: 0.05em;
  width: 1em;
  height: 1em;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 0.8em;
  font-weight: var(--fw-bold);
  color: var(--on-neon);
  background: var(--danger);
  border-radius: 50%;
}

.errorlist.nonfield,
.form-error--summary {
  padding: var(--space-3);
  padding-inline-start: var(--space-4);
  border: 1px solid rgba(var(--danger-rgb), 0.45);
  background: rgba(var(--danger-rgb), 0.1);
  border-radius: var(--r-md);
}

/* Invalid inputs. */
input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"],
.field.has-error input,
.field.has-error select,
.field.has-error textarea {
  border-color: var(--danger);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.55),
    0 0 0 3px rgba(var(--danger-rgb), 0.16);
}

/* Django messages framework. */
.messages,
ul.messages {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.messages li {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--r-md);
  border: 1px solid var(--hairline-strong);
  background: rgba(var(--bg-elev-rgb), 0.9);
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  border-inline-start: 3px solid var(--cyan);
}

.messages li.success {
  border-inline-start-color: var(--lime);
  color: #e6ffd0;
}

.messages li.warning {
  border-inline-start-color: var(--amber);
  color: #ffe9c2;
}

.messages li.error {
  border-inline-start-color: var(--danger);
  color: #ffd6e0;
}

/* Fieldsets. */
fieldset {
  border: var(--border);
  border-radius: var(--r-lg);
  padding: var(--space-4);
  min-width: 0;
}

legend {
  padding-inline: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--cyan);
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  margin-top: var(--space-2);
}

.form-actions--end {
  justify-content: flex-end;
}

/* Input paired with a trailing button (invite link + copy). */
.input-group {
  display: flex;
  gap: var(--space-2);
  align-items: stretch;
}

.input-group input {
  flex: 1 1 auto;
  min-width: 0;
}

.input-group .btn {
  flex: none;
}


/* ==========================================================================
   09 — SURFACES: CARDS, PANELS, NEON FRAMES
   ========================================================================== */

.card,
.panel {
  position: relative;
  background: linear-gradient(
    180deg,
    rgba(var(--bg-elev-rgb), 0.95),
    rgba(var(--bg-rgb), 0.92)
  );
  border: var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-2), var(--shadow-inset);
}

.card {
  padding: var(--space-4);
}

.panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
}

.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border);
  background: rgba(var(--bg-rgb), 0.45);
}

.panel__title {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0;
  text-shadow: none;
}

.panel__body {
  padding: var(--space-4);
  min-width: 0;
}

.panel__body--flush {
  padding: 0;
}

.panel__foot {
  padding: var(--space-3) var(--space-4);
  border-top: var(--border);
  background: rgba(var(--bg-rgb), 0.45);
}

/* Corner ticks: a cheap, static HUD flourish. */
.card--ticked::before,
.card--ticked::after,
.panel--ticked::before,
.panel--ticked::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  border: 1px solid rgba(var(--cyan-rgb), 0.5);
  pointer-events: none;
}

.card--ticked::before,
.panel--ticked::before {
  top: -1px;
  left: -1px;
  border-right: 0;
  border-bottom: 0;
  border-top-left-radius: var(--r-lg);
}

.card--ticked::after,
.panel--ticked::after {
  bottom: -1px;
  right: -1px;
  border-left: 0;
  border-top: 0;
  border-bottom-right-radius: var(--r-lg);
}

/* Animated gradient border for primary surfaces.
   Implemented as a rotating conic layer clipped by overflow, with an inner
   fill on ::after — no mask-composite, so it works everywhere.
   NOTE: .neon-frame clips overflow; do not put popovers inside one. */
.neon-frame {
  position: relative;
  isolation: isolate;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow-3);
}

.neon-frame::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 200%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%) rotate(0turn);
  background: conic-gradient(
    from 0turn,
    transparent 0 0.06turn,
    rgba(var(--cyan-rgb), 0.95) 0.15turn,
    rgba(var(--violet-rgb), 0.9) 0.24turn,
    transparent 0.36turn,
    transparent 0.56turn,
    rgba(var(--magenta-rgb), 0.9) 0.68turn,
    rgba(var(--violet-rgb), 0.9) 0.78turn,
    transparent 0.9turn
  );
  animation: pw-orbit 11s linear infinite;
  z-index: 0;
}

.neon-frame::after {
  content: "";
  position: absolute;
  inset: 1px;
  border-radius: inherit;
  background: linear-gradient(
    180deg,
    rgba(var(--bg-elev-rgb), 0.97),
    rgba(var(--bg-rgb), 0.98)
  );
  z-index: 1;
}

.neon-frame > * {
  position: relative;
  z-index: 2;
}

.neon-frame--thick::after {
  inset: 2px;
}

/* Static hairline variant for when motion is off or a calm surface is wanted. */
.neon-frame--static::before {
  animation: none;
  transform: translate(-50%, -50%) rotate(0.08turn);
}

/* Hero / callout. */
.hero {
  position: relative;
  padding: var(--space-6) var(--space-4);
  text-align: center;
  border-radius: var(--r-xl);
  overflow: hidden;
}

.hero__title {
  font-size: var(--fs-hero);
  letter-spacing: var(--tracking-wider);
  text-shadow: 0 0 10px rgba(var(--cyan-rgb), 0.45),
    0 0 40px rgba(var(--violet-rgb), 0.35);
}

.hero__lede {
  font-family: var(--font-sans);
  font-size: var(--fs-lg);
  color: var(--text-dim);
  max-width: 46ch;
  margin: var(--space-4) auto 0;
  line-height: var(--lh-normal);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
  margin-top: var(--space-6);
}

/* Empty state. */
.empty-state {
  padding: var(--space-7) var(--space-4);
  text-align: center;
  color: var(--text-dim);
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--r-lg);
  background: rgba(var(--bg-rgb), 0.4);
}

.empty-state__title {
  font-size: var(--fs-md);
  color: var(--text);
  margin-bottom: var(--space-2);
}

.empty-state p {
  font-family: var(--font-sans);
  max-width: 44ch;
  margin-inline: auto;
}


/* ==========================================================================
   10 — PLAY SCREEN LAYOUT
   Mobile: full-height flex column — topbar (sticky, outside), board area that
   sizes a SQUARE canvas into whatever vertical space is left, action bar last.
   Wide: two columns, board left, side rail right.
   ========================================================================== */

.play-main,
.play-shell {
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - var(--topbar-h) - var(--sa-t));
  min-height: calc(100dvh - var(--topbar-h) - var(--sa-t));
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--space-2);
}

/* Slim strip above the board: turn state, opponent, timers. */
.play-hud {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-1);
  min-height: 40px;
  flex: none;
}

/* Both rows stay exactly one line tall, no matter what text is swapped in:
   min-height floors each row at its own single-line height, and min-width: 0
   lets the turn row (whose #turn-indicator no longer wraps — see 12 — PLAYER
   CARDS, WALL PIPS, TURN INDICATOR below) actually shrink and ellipsis-
   truncate instead of pushing "Move NN" out of the strip or overflowing it. */
.play-hud .cluster {
  min-width: 0;
  min-height: 1lh;
}

.board-area {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  place-items: center;
  padding: var(--board-gutter) 0;
}

/* The square. min() of the available width and the leftover viewport height.
   --play-chrome is everything vertical that is NOT the board. */
.board-frame {
  --play-chrome: calc(
    var(--topbar-h) + var(--actionbar-h) + var(--sa-t) + var(--sa-b) +
      (var(--board-gutter) * 2) + 40px
  );
  position: relative;
  width: 100%;
  max-width: min(100%, 720px);
  aspect-ratio: 1 / 1;
  border-radius: var(--r-lg);
  overflow: hidden;
  isolation: isolate;
  background: radial-gradient(
      120% 120% at 50% 0%,
      rgba(var(--violet-rgb), 0.1),
      transparent 60%
    ),
    var(--bg-sunken);
  box-shadow: 0 0 0 1px var(--hairline-strong),
    0 0 60px -20px rgba(var(--cyan-rgb), 0.4), var(--shadow-3);
}

@supports (width: min(1px, 1vh)) {
  .board-frame {
    width: min(100%, calc(100vh - var(--play-chrome)));
    width: min(100%, calc(100dvh - var(--play-chrome)));
  }
}

/* Rotating gradient border on the board. ::before is the orbiting conic
   sweep, ::after is the opaque inner fill that leaves only a 1px rim of it
   showing, and the canvas (a `> *`, z-index 2) paints over both. Absolutely
   positioned children declared later — .board-overlay, .board-veil,
   .board-fallback — override the `position: relative` on equal specificity. */
.board-frame::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 200%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%) rotate(0turn);
  background: conic-gradient(
    from 0turn,
    transparent 0 0.08turn,
    rgba(var(--cyan-rgb), 0.9) 0.17turn,
    transparent 0.3turn,
    transparent 0.58turn,
    rgba(var(--magenta-rgb), 0.9) 0.67turn,
    transparent 0.8turn
  );
  animation: pw-orbit 14s linear infinite;
  z-index: 0;
  opacity: 0.85;
}

.board-frame::after {
  content: "";
  position: absolute;
  inset: 1px;
  border-radius: inherit;
  background: var(--bg-sunken);
  z-index: 1;
}

.board-frame > * {
  position: relative;
  z-index: 2;
}

/* Side rail — stacked below the board on mobile, a column on wide screens. */
.side-rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  padding-block: var(--space-2);
}

@media (min-width: 960px) {
  .play-main,
  .play-shell {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--rail-w);
    grid-template-rows: auto minmax(0, 1fr) auto;
    gap: var(--space-4);
    padding-inline: var(--space-4);
    align-items: stretch;
  }

  .play-hud {
    grid-column: 1;
    grid-row: 1;
  }

  .board-area {
    grid-column: 1;
    grid-row: 2;
  }

  .action-bar {
    grid-column: 1;
    grid-row: 3;
    position: static;
    border-radius: var(--r-lg);
    border: var(--border);
    border-top: var(--border);
    margin-bottom: var(--space-4);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  .side-rail {
    grid-column: 2;
    grid-row: 1 / -1;
    overflow-y: auto;
    max-height: calc(100vh - var(--topbar-h) - var(--sa-t) - var(--space-4));
    max-height: calc(100dvh - var(--topbar-h) - var(--sa-t) - var(--space-4));
    padding-bottom: var(--space-6);
    padding-inline: 2px;
  }

  .board-frame {
    --play-chrome: calc(
      var(--topbar-h) + var(--actionbar-h) + var(--sa-t) + var(--sa-b) +
        (var(--board-gutter) * 2) + 88px
    );
  }
}

@media (min-width: 1280px) {
  .board-frame {
    max-width: min(100%, 820px);
  }
}

/* Landscape phones: the board must not be squeezed to nothing. */
@media (orientation: landscape) and (max-height: 560px) and (max-width: 959px) {
  .play-main,
  .play-shell {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(220px, 300px);
    grid-template-rows: minmax(0, 1fr) auto;
    gap: var(--space-2);
  }

  .play-hud {
    display: none;
  }

  .board-area {
    grid-column: 1;
    grid-row: 1;
    padding: var(--space-1) 0;
  }

  .action-bar {
    grid-column: 1;
    grid-row: 2;
    position: static;
  }

  .side-rail {
    grid-column: 2;
    grid-row: 1 / -1;
    overflow-y: auto;
    font-size: var(--fs-xs);
  }

  .board-frame {
    --play-chrome: calc(var(--actionbar-h) + var(--sa-t) + var(--sa-b) + 24px);
  }
}


/* ==========================================================================
   11 — BOARD CANVAS & OVERLAYS
   The canvas is draw-only (board.js); input.js owns gestures. touch-action:none
   is mandatory so a drag from a post never scrolls the page.
   ========================================================================== */

.board-canvas,
canvas.board,
#board {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  cursor: crosshair;
  border-radius: calc(var(--r-lg) - 2px);
  background: transparent;
}

.board-canvas:focus-visible,
canvas.board:focus-visible,
#board:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: -2px;
  box-shadow: inset 0 0 0 1px var(--cyan), 0 0 26px rgba(var(--cyan-rgb), 0.45);
}

/* Wall-placement mode changes the cursor affordance. */
.board-canvas[data-mode="wall"],
canvas.board[data-mode="wall"] {
  cursor: cell;
}

.board-canvas[data-busy="true"],
canvas.board[data-busy="true"] {
  cursor: progress;
}

/* Absolutely-positioned HUD layer over the canvas. */
.board-overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--space-2);
}

.board-overlay > * {
  pointer-events: auto;
}

.board-badge {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-full);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text);
  background: var(--surface-glass);
  border: var(--border-strong);
  box-shadow: var(--glow-soft);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* Full-board veil for "game over" / "waiting for opponent". */
.board-veil {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: grid;
  place-items: center;
  gap: var(--space-4);
  padding: var(--space-5);
  text-align: center;
  background: radial-gradient(
    120% 120% at 50% 50%,
    rgba(var(--bg-rgb), 0.72),
    rgba(var(--bg-rgb), 0.94)
  );
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  animation: pw-fade-in var(--dur-4) var(--ease-out) both;
}

.board-veil__title {
  font-size: var(--fs-2xl);
  letter-spacing: var(--tracking-widest);
}

.board-veil--win .board-veil__title {
  color: var(--lime);
  text-shadow: 0 0 10px rgba(var(--lime-rgb), 0.5),
    0 0 36px rgba(var(--lime-rgb), 0.28);
}

.board-veil--lose .board-veil__title {
  color: var(--danger-text);
  text-shadow: 0 0 10px rgba(var(--danger-rgb), 0.5);
}

.board-veil__body {
  font-family: var(--font-sans);
  color: var(--text-dim);
  max-width: 34ch;
}

/* Invalid-preview shake. input.js may toggle this class for ~320 ms. */
.board-frame.is-invalid,
.board-canvas.is-invalid {
  animation: pw-shake 320ms var(--ease-in-out) 1;
}

/* Coordinate legend under / beside the board (optional). */
.board-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
  padding-top: var(--space-2);
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wider);
  color: var(--text-dim);
  text-transform: uppercase;
}

.board-legend__key {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.board-legend__swatch {
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: var(--seat-color);
  box-shadow: 0 0 10px rgba(var(--seat-rgb), 0.7);
}

.board-legend__swatch--wall {
  width: 18px;
  height: 6px;
  background: var(--wall-glow);
  box-shadow: var(--glow-violet);
}


/* ==========================================================================
   12 — PLAYER CARDS, WALL PIPS, TURN INDICATOR
   ========================================================================== */

.players {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

@media (max-width: 959px) {
  .players--row {
    flex-direction: row;
  }

  .players--row .player-card {
    flex: 1 1 0;
    min-width: 0;
  }
}

.player-card {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--r-md);
  border: var(--border-soft);
  background: linear-gradient(
    100deg,
    rgba(var(--seat-rgb), 0.09),
    rgba(var(--bg-elev-rgb), 0.92) 46%
  );
  box-shadow: var(--shadow-1);
  overflow: hidden;
  transition: border-color var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}

/* Colour spine on the leading edge — colour is never the only signal, the
   turn state also carries a dot, a label and a pulsing ring. */
.player-card::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block: 0;
  width: 3px;
  background: var(--seat-color);
  box-shadow: 0 0 14px rgba(var(--seat-rgb), 0.8);
}

.player-card[data-turn="true"],
.player-card.is-turn {
  border-color: rgba(var(--seat-rgb), 0.6);
  box-shadow: 0 0 0 1px rgba(var(--seat-rgb), 0.35),
    0 0 26px -6px rgba(var(--seat-rgb), 0.45);
}

.player-card[data-turn="true"]::after,
.player-card.is-turn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(var(--seat-rgb), 0.55);
  pointer-events: none;
  animation: pw-pulse-ring 2.2s var(--ease-in-out) infinite;
}

.player-card[data-you="true"] .player-card__name::after,
.player-card.is-you .player-card__name::after {
  content: "YOU";
  margin-inline-start: 0.6em;
  padding: 0.1em 0.4em;
  font-size: 0.68em;
  letter-spacing: var(--tracking-wide);
  color: var(--on-neon);
  background: var(--seat-color);
  border-radius: var(--r-xs);
  vertical-align: middle;
}

.player-card__avatar,
.seat-dot {
  width: 34px;
  height: 34px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--seat-color);
  background: rgba(var(--seat-rgb), 0.12);
  border: 1px solid rgba(var(--seat-rgb), 0.45);
  box-shadow: inset 0 0 12px rgba(var(--seat-rgb), 0.2);
  text-transform: uppercase;
}

.player-card__main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.player-card__name {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.player-card__meta {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wide);
  color: var(--text-dim);
  text-transform: uppercase;
}

.player-card__side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
}

/* Turn indicator dot — pulses on the active player. */
.turn-dot {
  width: 9px;
  height: 9px;
  flex: none;
  border-radius: 50%;
  background: var(--text-mute);
  box-shadow: none;
  transition: background-color var(--dur-2) var(--ease-out);
}

[data-turn="true"] .turn-dot,
.is-turn .turn-dot {
  background: var(--seat-color);
  box-shadow: 0 0 10px rgba(var(--seat-rgb), 0.9);
  animation: pw-blink 1.6s var(--ease-in-out) infinite;
}

.turn-label {
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-dim);
}

[data-turn="true"] .turn-label,
.is-turn .turn-label {
  color: var(--seat-color);
}

/* #turn-indicator only — not the plain `.turn-label` span in the player-card
   rows below, which never gets the `thinking` class or long opponent text.
   It permanently reserves the trailing space `.thinking::after` paints its
   dots into (see 22 — STATUS PILLS, BADGES, CODE CHIPS, SKELETONS below,
   where `.thinking::after` is defined) and never wraps or grows:
   toggling `thinking`, or swapping the label between e.g. "Your move" and
   "Nemesis-9000 is thinking", must never resize this box, or the HUD strip
   above the board resizes with it and the whole board jumps. */
#turn-indicator {
  display: inline-block;
  position: relative;
  max-width: 100%;
  padding-inline-end: 1.2em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* AI badge / difficulty. */
.ai-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding: 0.1em 0.45em;
  border-radius: var(--r-xs);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--violet-text);
  background: rgba(var(--violet-rgb), 0.14);
  border: 1px solid rgba(var(--violet-rgb), 0.4);
}

/* Wall counter as a row of pips. Spent pips stay visible but hollow, so the
   total is always readable — count, not just colour, carries the meaning. */
.pips,
.wall-pips {
  display: flex;
  align-items: center;
  gap: 3px;
  flex-wrap: wrap;
  justify-content: flex-end;
  color: var(--seat-color);
  max-width: 148px;
}

.pip {
  width: 6px;
  height: 15px;
  flex: none;
  border-radius: 2px;
  background: currentColor;
  box-shadow: 0 0 8px rgba(var(--seat-rgb), 0.65);
  transition: background-color var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out), opacity var(--dur-2) var(--ease-out);
}

.pip.is-spent,
.pip[data-spent="true"] {
  background: transparent;
  border: 1px solid rgba(var(--text-rgb), 0.22);
  box-shadow: none;
  opacity: 0.6;
}

/* Very high wall counts (colossus preset: 20) shrink the pips instead of
   wrapping into a mess. */
.pips[data-count="high"] .pip,
.pips--dense .pip {
  width: 4px;
  height: 12px;
}

.pips__count {
  margin-inline-start: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--text);
}

/* Shortest-path readout. */
.path-stat {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35em;
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wide);
  color: var(--text-dim);
  text-transform: uppercase;
}

.path-stat__value {
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--text);
}


/* ==========================================================================
   13 — ACTION BAR (CONFIRM / CANCEL)
   Always present so the layout never jumps. It shows a status line by default;
   when a preview is pending the confirm row slides up over the status line.
   Large, thumb-reachable, safe-area aware.
   ========================================================================== */

.action-bar {
  position: sticky;
  bottom: 0;
  z-index: var(--z-actionbar);
  flex: none;
  display: grid;
  align-items: center;
  min-height: var(--actionbar-h);
  padding: var(--space-2) var(--space-3)
    calc(var(--space-2) + var(--sa-b));
  border-top: 1px solid var(--hairline-strong);
  background: linear-gradient(
    180deg,
    rgba(var(--bg-rgb), 0.86),
    rgba(var(--bg-rgb), 0.98)
  );
  box-shadow: 0 -18px 40px -28px rgba(0, 0, 0, 1);
}

@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .action-bar {
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    backdrop-filter: blur(16px) saturate(140%);
  }
}

/* Both children occupy the same grid cell and cross-fade / slide. */
.action-bar__status,
.action-bar__confirm {
  grid-area: 1 / 1;
  min-width: 0;
}

.action-bar__status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-2) var(--ease-out),
    visibility 0s linear 0s;
}

.action-bar__hint {
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  color: var(--text-dim);
  line-height: var(--lh-snug);
  min-width: 0;
}

.action-bar__hint strong {
  color: var(--text);
  font-family: var(--font-mono);
  letter-spacing: var(--tracking-wide);
}

.action-bar__tools {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}

.action-bar__confirm {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
  gap: var(--space-3);
  transform: translateY(130%);
  opacity: 0;
  visibility: hidden;
  transition: transform var(--dur-3) var(--ease-out),
    opacity var(--dur-2) var(--ease-out), visibility 0s linear var(--dur-3);
}

.action-bar[data-open="true"] .action-bar__confirm,
.action-bar.is-open .action-bar__confirm,
.action-bar[data-preview="pending"] .action-bar__confirm {
  transform: translateY(0);
  opacity: 1;
  visibility: visible;
  transition: transform var(--dur-3) var(--ease-spring),
    opacity var(--dur-2) var(--ease-out), visibility 0s linear 0s;
}

.action-bar[data-open="true"] .action-bar__status,
.action-bar.is-open .action-bar__status,
.action-bar[data-preview="pending"] .action-bar__status {
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-1) var(--ease-out),
    visibility 0s linear var(--dur-1);
}

/* The two big targets. */
.action-bar__confirm .btn {
  min-height: 52px;
  font-size: var(--fs-sm);
  border-radius: var(--r-md);
}

.btn-confirm {
  --btn-fg: var(--on-neon);
  --btn-border: transparent;
  background-image: linear-gradient(135deg, #b6ff6e 0%, var(--lime) 55%, #62d21b 100%);
  background-color: var(--lime);
  color: var(--on-neon);
  box-shadow: 0 0 22px rgba(var(--lime-rgb), 0.35),
    0 10px 26px -14px rgba(var(--lime-rgb), 0.8);
}

.btn-confirm:hover {
  filter: brightness(1.06);
  border-color: transparent;
  box-shadow: 0 0 30px rgba(var(--lime-rgb), 0.5);
}

.btn-confirm:focus-visible {
  --focus-color: var(--lime);
}

.btn-cancel {
  --btn-bg: rgba(var(--bg-elev-rgb), 0.9);
  --btn-fg: var(--text-dim);
  --btn-border: var(--hairline-strong);
}

.btn-cancel:hover {
  --btn-fg: var(--text);
}

/* A wall preview turns the confirm row violet; an invalid one turns it red. */
.action-bar[data-preview-kind="wall"] .btn-confirm {
  background-image: linear-gradient(135deg, #c4aefe, var(--violet));
  background-color: var(--violet);
  box-shadow: 0 0 22px rgba(var(--violet-rgb), 0.4);
}

.action-bar[data-valid="false"] .btn-confirm,
.action-bar[data-preview="invalid"] .btn-confirm {
  background-image: none;
  background-color: rgba(var(--danger-rgb), 0.18);
  color: var(--danger-text);
  box-shadow: inset 0 0 0 1px rgba(var(--danger-rgb), 0.55);
  pointer-events: none;
}

.action-bar__preview-label {
  grid-column: 1 / -1;
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-dim);
  text-align: center;
  margin-bottom: 2px;
}

/* Floating variant, if a template renders the bar detached from the column. */
.action-bar--floating {
  position: fixed;
  left: var(--sa-l);
  right: var(--sa-r);
  bottom: 0;
}


/* ==========================================================================
   14 — MOVE LOG & HISTORY / REPLAY
   ========================================================================== */

.move-log {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 320px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}

.move-log--tall {
  max-height: none;
}

.move-log__row,
.move-log li {
  display: grid;
  grid-template-columns: 3.2em auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--hairline-soft);
  color: var(--text-dim);
  transition: background-color var(--dur-1) var(--ease-out);
}

.move-log li:last-child,
.move-log__row:last-child {
  border-bottom: 0;
}

.move-log__row:hover,
.move-log li:hover {
  background: rgba(var(--cyan-rgb), 0.05);
}

.move-log__ply {
  font-size: var(--fs-2xs);
  color: var(--text-mute);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.move-log__seat {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  background: var(--seat-color);
  box-shadow: 0 0 8px rgba(var(--seat-rgb), 0.7);
}

.move-log__note,
.move-log__notation {
  color: var(--text);
  letter-spacing: var(--tracking-wide);
  font-weight: var(--fw-bold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Walls read differently from pawn moves — shape, not just colour. */
.move-log__row[data-kind="wall"] .move-log__note::before {
  content: "▚ ";
  color: var(--violet-text);
}

.move-log__row[data-kind="move"] .move-log__note::before {
  content: "→ ";
  color: var(--text-dim);
}

.move-log__row[data-kind="resign"] .move-log__note {
  color: var(--danger-text);
}

.move-log__row[data-kind="resign"] .move-log__note::before {
  content: "✕ ";
}

.move-log__time {
  font-size: var(--fs-3xs);
  color: var(--text-mute);
  white-space: nowrap;
}

.move-log__row[aria-current="true"],
.move-log__row.is-current,
.move-log li[aria-current="true"] {
  background: rgba(var(--cyan-rgb), 0.12);
  color: var(--text);
  box-shadow: inset 3px 0 0 var(--cyan);
}

/* Newly appended row flashes once. */
.move-log__row.is-new {
  animation: pw-flash-in var(--dur-4) var(--ease-out) 1;
}

/* Replay scrubber. */
.replay {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
}

.replay__transport {
  display: flex;
  gap: var(--space-1);
  flex: none;
}

.replay__scrub {
  flex: 1 1 auto;
  min-width: 0;
}

.replay__counter {
  flex: none;
  font-size: var(--fs-xs);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  min-width: 6ch;
  text-align: right;
}


/* ==========================================================================
   15 — TOASTS & ARIA-LIVE REGIONS
   Toasts sit at the top so they never fight the action bar or the iOS home
   indicator; top-centre on mobile, top-right from tablet up.
   ========================================================================== */

#toasts,
.toasts {
  position: fixed;
  z-index: var(--z-toast);
  top: calc(var(--sa-t) + var(--topbar-h) + var(--space-2));
  left: 50%;
  transform: translateX(-50%);
  width: min(440px, calc(100vw - var(--space-6)));
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  pointer-events: none;
}

@media (min-width: 760px) {
  #toasts,
  .toasts {
    left: auto;
    right: calc(var(--sa-r) + var(--space-4));
    transform: none;
  }
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--r-md);
  border: var(--border-strong);
  border-inline-start: 3px solid var(--cyan);
  background: rgba(var(--bg-elev-rgb), 0.96);
  box-shadow: var(--shadow-3), 0 0 24px -14px rgba(var(--cyan-rgb), 0.9);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  animation: pw-toast-in var(--dur-3) var(--ease-spring) both;
}

@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .toast {
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    background: rgba(var(--bg-elev-rgb), 0.82);
  }
}

.toast__icon {
  flex: none;
  margin-top: 1px;
  color: var(--cyan);
}

.toast__body {
  flex: 1 1 auto;
  min-width: 0;
}

.toast__title {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--cyan);
  margin-bottom: 2px;
}

.toast__close {
  flex: none;
  width: 28px;
  height: 28px;
  margin: -2px -6px 0 0;
  display: grid;
  place-items: center;
  border-radius: var(--r-sm);
  color: var(--text-dim);
  cursor: pointer;
  background: none;
  border: 0;
}

.toast__close:hover {
  color: var(--text);
  background: rgba(var(--text-rgb), 0.08);
}

.toast--ok,
.toast--success,
.toast[data-kind="ok"],
.toast[data-kind="success"] {
  border-inline-start-color: var(--lime);
}

.toast--ok .toast__title,
.toast--success .toast__title,
.toast[data-kind="ok"] .toast__title,
.toast--ok .toast__icon {
  color: var(--lime);
}

.toast--warn,
.toast[data-kind="warn"] {
  border-inline-start-color: var(--amber);
}

.toast--warn .toast__title,
.toast--warn .toast__icon,
.toast[data-kind="warn"] .toast__title {
  color: var(--amber);
}

.toast--error,
.toast--danger,
.toast[data-kind="error"] {
  border-inline-start-color: var(--danger);
}

.toast--error .toast__title,
.toast--error .toast__icon,
.toast[data-kind="error"] .toast__title {
  color: var(--danger-text);
}

.toast.is-leaving {
  animation: pw-toast-out var(--dur-2) var(--ease-in) both;
}

/* Visible aria-live region (move announcements, connection state). */
.live-region,
[aria-live].is-visible {
  display: block;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-sm);
  border: var(--border);
  background: rgba(var(--bg-rgb), 0.55);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--text-dim);
  min-height: 2.2em;
}

.live-region:empty {
  visibility: hidden;
}

.live-region strong {
  color: var(--cyan);
}

/* Offline / reconnecting strip. */
.net-status {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2);
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--amber);
  background: rgba(var(--amber-rgb), 0.1);
  border-block: 1px solid rgba(var(--amber-rgb), 0.3);
}

.net-status--offline {
  color: var(--danger-text);
  background: rgba(var(--danger-rgb), 0.1);
  border-color: rgba(var(--danger-rgb), 0.3);
}

.net-status__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  animation: pw-blink 1.4s var(--ease-in-out) infinite;
}


/* ==========================================================================
   16 — MODALS & DIALOGS
   Styles <dialog> natively and mirrors it on a .modal div for JS that
   cannot use <dialog> (older iOS Safari < 15.4).
   ========================================================================== */

dialog {
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  max-width: min(560px, calc(100vw - var(--space-6)));
  max-height: calc(100vh - var(--space-8));
  max-height: calc(100dvh - var(--space-8));
  overflow: visible;
}

dialog::backdrop {
  background: rgba(2, 3, 8, 0.72);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}

dialog[open] {
  animation: pw-modal-in var(--dur-3) var(--ease-spring) both;
}

/* Fallback overlay + panel. */
.modal-backdrop,
.overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: var(--space-4);
  background: rgba(2, 3, 8, 0.72);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  animation: pw-fade-in var(--dur-2) var(--ease-out) both;
}

.modal,
dialog > .modal,
.modal__panel {
  position: relative;
  width: min(560px, 100%);
  max-height: calc(100vh - var(--space-8));
  max-height: calc(100dvh - var(--space-8));
  display: flex;
  flex-direction: column;
  border-radius: var(--r-lg);
  border: 1px solid var(--hairline-strong);
  background: linear-gradient(
    180deg,
    rgba(var(--bg-elev-rgb), 0.99),
    rgba(var(--bg-rgb), 0.99)
  );
  box-shadow: var(--shadow-3), 0 0 60px -24px rgba(var(--cyan-rgb), 0.55);
  overflow: hidden;
}

.modal--wide {
  width: min(860px, 100%);
}

.modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: var(--border);
}

.modal__title {
  font-size: var(--fs-lg);
  letter-spacing: var(--tracking-wider);
  margin: 0;
  text-shadow: none;
}

.modal__body {
  padding: var(--space-4);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
  color: var(--text);
}

.modal__body p + p {
  margin-top: var(--space-3);
}

.modal__foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: flex-end;
  padding: var(--space-4);
  padding-bottom: calc(var(--space-4) + var(--sa-b));
  border-top: var(--border);
  background: rgba(var(--bg-rgb), 0.5);
}

.modal__close {
  flex: none;
}

/* Mobile: modals become bottom sheets. */
@media (max-width: 599px) {
  .modal-backdrop,
  .overlay {
    align-items: end;
    padding: 0;
  }

  .modal,
  .modal__panel {
    width: 100%;
    max-height: 88vh;
    max-height: 88dvh;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    border-bottom: 0;
    animation: pw-sheet-in var(--dur-3) var(--ease-out) both;
  }

  dialog {
    max-width: 100vw;
    width: 100%;
    margin: auto auto 0;
  }
}

/* Native <details> as a lightweight disclosure. */
details.disclosure {
  border: var(--border);
  border-radius: var(--r-md);
  background: rgba(var(--bg-elev-rgb), 0.7);
  overflow: hidden;
}

details.disclosure > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  list-style: none;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-dim);
}

details.disclosure > summary::-webkit-details-marker {
  display: none;
}

details.disclosure > summary::after {
  content: "▾";
  color: var(--cyan);
  transition: transform var(--dur-2) var(--ease-out);
}

details.disclosure[open] > summary::after {
  transform: rotate(180deg);
}

details.disclosure > summary:hover {
  color: var(--text);
  background: rgba(var(--cyan-rgb), 0.06);
}

details.disclosure > .disclosure__body {
  padding: var(--space-3);
  border-top: var(--border);
}


/* ==========================================================================
   17 — BOARD CONFIGURATOR (SLIDERS, PRESETS, LIVE PREVIEW)
   ========================================================================== */

.configurator {
  display: grid;
  gap: var(--space-5);
  align-items: start;
}

@media (min-width: 800px) {
  .configurator {
    grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
  }

  .config-preview {
    position: sticky;
    top: calc(var(--topbar-h) + var(--sa-t) + var(--space-4));
  }
}

.config-preview {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--r-lg);
  border: var(--border);
  background: rgba(var(--bg-rgb), 0.55);
}

.config-preview__stage {
  aspect-ratio: 1 / 1;
  width: 100%;
  border-radius: var(--r-md);
  background: var(--bg-sunken);
  border: 1px solid var(--hairline-strong);
  box-shadow: inset 0 0 40px rgba(var(--violet-rgb), 0.12);
  overflow: hidden;
  display: grid;
  place-items: stretch;
}

.config-preview__stage canvas,
.config-preview__stage .mini-board {
  width: 100%;
  height: 100%;
}

.config-preview__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(78px, 1fr));
  gap: var(--space-2);
}

.stat {
  padding: var(--space-2);
  border-radius: var(--r-sm);
  background: rgba(var(--bg-elev-rgb), 0.8);
  border: var(--border-soft);
  text-align: center;
}

.stat__label {
  display: block;
  font-size: var(--fs-3xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-dim);
}

.stat__value {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--cyan);
  line-height: 1.2;
}

/* --- Range slider --------------------------------------------------------
   JS may set --fill (0–100%) on the input for a neon progress fill; without
   JS the track still reads correctly. Vendor pseudo-elements MUST live in
   separate rules — one unknown selector invalidates a whole list.          */

.range-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.range-field__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.range-field__value {
  font-family: var(--font-mono);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--cyan);
  text-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.4);
  font-variant-numeric: tabular-nums;
}

.range-field__scale {
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-3xs);
  color: var(--text-mute);
  letter-spacing: var(--tracking-wide);
}

input[type="range"] {
  --fill: 50%;
  width: 100%;
  height: var(--tap);
  min-height: 0;
  padding: 0;
  margin: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  touch-action: pan-y;
}

input[type="range"]:focus {
  box-shadow: none;
  background: transparent;
}

input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: var(--r-full);
  background: linear-gradient(
    90deg,
    var(--cyan) 0 var(--fill),
    var(--grid-line) var(--fill) 100%
  );
  border: 1px solid var(--hairline);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
}

input[type="range"]::-moz-range-track {
  height: 6px;
  border-radius: var(--r-full);
  background: var(--grid-line);
  border: 1px solid var(--hairline);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
}

input[type="range"]::-moz-range-progress {
  height: 6px;
  border-radius: var(--r-full);
  background: var(--cyan);
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 24px;
  height: 24px;
  margin-top: -10px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 32%, #ffffff, var(--cyan) 62%);
  border: 1px solid rgba(var(--bg-rgb), 0.9);
  box-shadow: 0 0 0 4px rgba(var(--cyan-rgb), 0.16),
    0 0 16px rgba(var(--cyan-rgb), 0.7);
  transition: box-shadow var(--dur-1) var(--ease-out),
    transform var(--dur-1) var(--ease-out);
}

input[type="range"]::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 32%, #ffffff, var(--cyan) 62%);
  border: 1px solid rgba(var(--bg-rgb), 0.9);
  box-shadow: 0 0 0 4px rgba(var(--cyan-rgb), 0.16),
    0 0 16px rgba(var(--cyan-rgb), 0.7);
}

input[type="range"]:hover::-webkit-slider-thumb {
  box-shadow: 0 0 0 6px rgba(var(--cyan-rgb), 0.2),
    0 0 22px rgba(var(--cyan-rgb), 0.9);
}

input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.08);
}

input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 4px var(--bg), 0 0 0 6px var(--cyan);
}

input[type="range"]:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 4px var(--bg), 0 0 0 6px var(--cyan);
}

input[type="range"]:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* --- Preset chips (radio group) ------------------------------------------
   Works with `input + label` sibling markup, and with :has() when the input
   is nested inside the label.                                              */

.preset-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: var(--space-2);
}

.preset-chip {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 64px;
  padding: var(--space-3);
  border-radius: var(--r-md);
  border: 1px solid var(--hairline-strong);
  background: rgba(var(--bg-elev-rgb), 0.75);
  cursor: pointer;
  text-transform: none;
  letter-spacing: var(--tracking-normal);
  transition: border-color var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}

.preset-chip:hover {
  border-color: rgba(var(--cyan-rgb), 0.5);
  background: rgba(var(--cyan-rgb), 0.07);
}

.preset-chip__name {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text);
}

.preset-chip__spec {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--cyan);
  letter-spacing: var(--tracking-wide);
}

.preset-chip__desc {
  font-family: var(--font-sans);
  font-size: var(--fs-2xs);
  color: var(--text-dim);
  line-height: var(--lh-snug);
}

/* Visually hide the radio but keep it focusable & operable. */
.preset-grid input[type="radio"],
.preset-chip input[type="radio"] {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  inset: 0;
  margin: 0;
  cursor: pointer;
}

.preset-grid input[type="radio"]:checked + .preset-chip,
.preset-grid input[type="radio"]:checked + label {
  border-color: var(--cyan);
  background: rgba(var(--cyan-rgb), 0.12);
  box-shadow: 0 0 0 1px rgba(var(--cyan-rgb), 0.4),
    0 0 24px -10px rgba(var(--cyan-rgb), 0.9);
}

.preset-grid input[type="radio"]:focus-visible + .preset-chip,
.preset-grid input[type="radio"]:focus-visible + label {
  box-shadow: var(--focus-ring);
}

@supports selector(:has(*)) {
  .preset-chip:has(input[type="radio"]:checked) {
    border-color: var(--cyan);
    background: rgba(var(--cyan-rgb), 0.12);
    box-shadow: 0 0 0 1px rgba(var(--cyan-rgb), 0.4),
      0 0 24px -10px rgba(var(--cyan-rgb), 0.9);
  }

  .preset-chip:has(input[type="radio"]:focus-visible) {
    box-shadow: var(--focus-ring);
  }
}

/* Difficulty selector reuses the chip look at a smaller size. */
.difficulty-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
  gap: var(--space-2);
}

.difficulty-grid .preset-chip {
  min-height: var(--tap);
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-2);
}

/* Warning shown when a config is legal but eccentric (huge board, long wall). */
.config-warning {
  display: flex;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--r-md);
  border: 1px solid rgba(var(--amber-rgb), 0.4);
  background: rgba(var(--amber-rgb), 0.09);
  color: #ffe6bd;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
}


/* ==========================================================================
   18 — GAME LIST CARDS
   ========================================================================== */

.game-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-3);
}

.game-card {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-3);
  border-radius: var(--r-lg);
  border: var(--border);
  background: linear-gradient(
    180deg,
    rgba(var(--bg-elev-rgb), 0.95),
    rgba(var(--bg-rgb), 0.9)
  );
  box-shadow: var(--shadow-1);
  color: inherit;
  text-decoration: none;
  transition: transform var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}

a.game-card:hover,
.game-card:hover {
  text-decoration: none;
  transform: translateY(-2px);
  border-color: rgba(var(--cyan-rgb), 0.45);
  box-shadow: var(--shadow-2), 0 0 34px -18px rgba(var(--cyan-rgb), 0.9);
}

a.game-card:focus-visible {
  box-shadow: var(--focus-ring);
}

/* Whole card clickable via a stretched link, keeping nested buttons usable. */
.game-card__link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.game-card__thumb {
  grid-row: 1 / span 2;
  width: 68px;
  height: 68px;
  flex: none;
}

.game-card__main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.game-card__title {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-shadow: none;
  text-transform: none;
}

.game-card__opponent {
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.game-card__meta {
  grid-column: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wide);
  color: var(--text-dim);
  text-transform: uppercase;
}

.game-card__ago {
  font-family: var(--font-mono);
  color: var(--text-mute);
  white-space: nowrap;
}

.game-card[data-your-turn="true"],
.game-card.is-your-turn {
  border-color: rgba(var(--lime-rgb), 0.42);
  box-shadow: 0 0 0 1px rgba(var(--lime-rgb), 0.2),
    0 0 30px -16px rgba(var(--lime-rgb), 0.9);
}

.game-card[data-your-turn="true"] .game-card__thumb,
.game-card.is-your-turn .game-card__thumb {
  box-shadow: 0 0 0 1px rgba(var(--lime-rgb), 0.5),
    0 0 18px -6px rgba(var(--lime-rgb), 0.8);
}

/* Mini board thumbnail slot. Renders as a neon grid even when empty, and
   accepts a <canvas> or <img> child that fills it. */
.mini-board {
  aspect-ratio: 1 / 1;
  border-radius: var(--r-sm);
  overflow: hidden;
  background-color: var(--bg-sunken);
  background-image: repeating-linear-gradient(
      to right,
      rgba(var(--cyan-rgb), 0.16) 0 1px,
      transparent 1px calc(100% / var(--mini-cells, 9))
    ),
    repeating-linear-gradient(
      to bottom,
      rgba(var(--cyan-rgb), 0.16) 0 1px,
      transparent 1px calc(100% / var(--mini-cells, 9))
    );
  border: 1px solid var(--hairline);
  box-shadow: inset 0 0 18px rgba(var(--violet-rgb), 0.16);
  position: relative;
}

.mini-board > canvas,
.mini-board > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
}

/* Goal-edge tint on the thumbnail so it reads as a Pallwall board. */
.mini-board::before,
.mini-board::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 3px;
  pointer-events: none;
}

.mini-board::before {
  top: 0;
  background: var(--p0);
  box-shadow: 0 0 10px rgba(var(--cyan-rgb), 0.8);
}

.mini-board::after {
  bottom: 0;
  background: var(--p1);
  box-shadow: 0 0 10px rgba(var(--magenta-rgb), 0.8);
}

/* List (non-grid) variant. */
.game-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.game-list .game-card {
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto;
  align-items: center;
}

.game-list .game-card__thumb {
  grid-row: 1;
  width: 52px;
  height: 52px;
}

.game-list .game-card__meta {
  grid-column: 3;
  grid-row: 1;
  margin-top: 0;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}


/* ==========================================================================
   19 — RULES PAGE (PROSE)
   ========================================================================== */

.prose {
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: var(--lh-loose);
  color: var(--text);
  max-width: var(--prose-max);
}

.prose > * + * {
  margin-top: var(--space-4);
}

.prose h2 {
  margin-top: var(--space-7);
  padding-bottom: var(--space-2);
  border-bottom: var(--border);
  font-size: var(--fs-xl);
  color: var(--cyan);
  text-shadow: var(--text-glow-cyan);
}

.prose h3 {
  margin-top: var(--space-5);
  font-size: var(--fs-md);
  letter-spacing: var(--tracking-wider);
  color: var(--text);
}

.prose h2:first-child,
.prose h3:first-child {
  margin-top: 0;
}

.prose p {
  color: var(--text);
}

.prose ul,
.prose ol {
  padding-inline-start: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.prose ul {
  list-style: none;
  padding-inline-start: var(--space-4);
}

.prose ul > li {
  position: relative;
  padding-inline-start: var(--space-4);
}

.prose ul > li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.62em;
  width: 6px;
  height: 6px;
  border-radius: 1px;
  background: var(--cyan);
  box-shadow: 0 0 8px rgba(var(--cyan-rgb), 0.8);
}

.prose ol {
  list-style: decimal;
}

.prose ol > li::marker {
  font-family: var(--font-mono);
  color: var(--cyan);
  font-weight: var(--fw-bold);
}

.prose blockquote {
  padding: var(--space-3) var(--space-4);
  border-inline-start: 3px solid var(--violet);
  background: rgba(var(--violet-rgb), 0.08);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  color: var(--text-dim);
  font-style: italic;
}

.prose figure {
  margin: var(--space-5) 0;
}

.prose figcaption {
  margin-top: var(--space-2);
  font-size: var(--fs-sm);
  color: var(--text-dim);
  text-align: center;
}

.prose table {
  width: 100%;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}

.prose th,
.prose td,
table th,
table td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-bottom: 1px solid var(--hairline-soft);
}

table th {
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-dim);
  border-bottom-color: var(--hairline-strong);
}

/* Rules diagrams: a labelled figure with a neon frame. */
.rule-figure {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--r-lg);
  border: var(--border);
  background: rgba(var(--bg-rgb), 0.5);
}

.rule-figure__stage {
  aspect-ratio: 1 / 1;
  width: min(100%, 320px);
  margin-inline: auto;
  border-radius: var(--r-md);
  background: var(--bg-sunken);
  border: 1px solid var(--hairline-strong);
  overflow: hidden;
}

/* Table of contents for long rules. */
.toc {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--r-md);
  border: var(--border);
  background: rgba(var(--bg-elev-rgb), 0.6);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}

.toc ol,
.toc ul {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.toc a {
  display: block;
  padding: var(--space-1) 0;
  color: var(--text-dim);
}

.toc a:hover {
  color: var(--cyan);
}


/* ==========================================================================
   20 — SETTINGS PAGE & TOGGLE SWITCHES
   The switch works on: input[type=checkbox].switch, input[role=switch],
   a nested `.switch > input`, and button[role=switch][aria-checked].
   ========================================================================== */

.settings-group,
.setting-list {
  display: flex;
  flex-direction: column;
  border-radius: var(--r-lg);
  border: var(--border);
  background: rgba(var(--bg-elev-rgb), 0.7);
  overflow: hidden;
}

.settings-group + .settings-group {
  margin-top: var(--space-5);
}

.settings-group__title {
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border);
  background: rgba(var(--bg-rgb), 0.45);
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-widest);
  color: var(--text-dim);
  margin: 0;
}

.setting-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  min-height: 60px;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--hairline-soft);
}

.setting-row:last-child {
  border-bottom: 0;
}

.setting-row__label {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  color: var(--text);
}

.setting-row__desc {
  display: block;
  margin-top: 2px;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  color: var(--text-dim);
  line-height: var(--lh-snug);
  letter-spacing: normal;
  text-transform: none;
}

.setting-row__control {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Full-width control (a select or a slider) drops to its own line. */
.setting-row--stacked {
  grid-template-columns: minmax(0, 1fr);
}

/* --- The switch -----------------------------------------------------------
   Three markup patterns are supported:
     <input type="checkbox" class="switch">
     <input type="checkbox" role="switch">
     <label class="switch"><input type="checkbox"> Label text</label>
     <button role="switch" aria-checked="true">
   The nested pattern's selector deliberately repeats [type="checkbox"] to
   raise its specificity to (0,3,1)/(0,4,1) — matching the generic checkbox
   rules in section 08 so that source order (this section is later) wins.
   Without the padding the generic `:not(.switch)` rules would beat it and the
   nested input would render as a tick box inside a pill.               */

input[type="checkbox"].switch,
input[type="checkbox"][role="switch"],
.switch > input[type="checkbox"][type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: none;
  width: 54px;
  height: 30px;
  min-height: 0;
  margin: 0;
  padding: 0;
  border-radius: var(--r-full);
  border: 1px solid var(--hairline-strong);
  background: rgba(var(--bg-rgb), 0.9);
  box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.7);
  cursor: pointer;
  transition: background-color var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}

/* Enlarge the effective hit area to 44px without changing the visual size.
   A pseudo-element's pointer events target its originating element, so the
   inflated ::after makes the whole 44px box toggle the input. */
input[type="checkbox"].switch::after,
input[type="checkbox"][role="switch"]::after,
.switch > input[type="checkbox"][type="checkbox"]::after {
  content: "";
  position: absolute;
  inset: -7px;
  border-radius: var(--r-full);
}

input[type="checkbox"].switch::before,
input[type="checkbox"][role="switch"]::before,
.switch > input[type="checkbox"][type="checkbox"]::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  clip-path: none;
  background: linear-gradient(180deg, #b9c9e8, #6f86b8);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
  transform: translateX(0);
  transition: transform var(--dur-2) var(--ease-spring),
    background-color var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}

input[type="checkbox"].switch:checked,
input[type="checkbox"][role="switch"]:checked,
.switch > input[type="checkbox"][type="checkbox"]:checked {
  background: linear-gradient(
    90deg,
    rgba(var(--cyan-rgb), 0.25),
    rgba(var(--cyan-rgb), 0.45)
  );
  border-color: var(--cyan);
  box-shadow: inset 0 0 14px rgba(var(--cyan-rgb), 0.35),
    0 0 16px -4px rgba(var(--cyan-rgb), 0.7);
}

input[type="checkbox"].switch:checked::before,
input[type="checkbox"][role="switch"]:checked::before,
.switch > input[type="checkbox"][type="checkbox"]:checked::before {
  transform: translateX(24px);
  clip-path: none;
  background: linear-gradient(180deg, #ffffff, var(--cyan));
  box-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.9), 0 1px 3px rgba(0, 0, 0, 0.6);
}

input[type="checkbox"].switch:disabled,
input[type="checkbox"][role="switch"]:disabled,
.switch > input[type="checkbox"][type="checkbox"]:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Button-based switch (aria-checked), same visual language. */
button[role="switch"] {
  position: relative;
  flex: none;
  width: 54px;
  height: 30px;
  padding: 0;
  border-radius: var(--r-full);
  border: 1px solid var(--hairline-strong);
  background: rgba(var(--bg-rgb), 0.9);
  box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.7);
  cursor: pointer;
  transition: background-color var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out);
}

button[role="switch"]::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: linear-gradient(180deg, #b9c9e8, #6f86b8);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
  transition: transform var(--dur-2) var(--ease-spring);
}

button[role="switch"][aria-checked="true"] {
  background: linear-gradient(
    90deg,
    rgba(var(--cyan-rgb), 0.25),
    rgba(var(--cyan-rgb), 0.45)
  );
  border-color: var(--cyan);
}

button[role="switch"][aria-checked="true"]::before {
  transform: translateX(24px);
  background: linear-gradient(180deg, #ffffff, var(--cyan));
  box-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.9);
}

/* A label wrapper around a switch keeps the whole row tappable. */
label.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap);
  cursor: pointer;
  text-transform: none;
  letter-spacing: normal;
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  color: var(--text);
}

/* Small textual on/off mirror of the switch (colour is never the only cue). */
.switch-state {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-dim);
  min-width: 3ch;
}

/* Profile / stats blocks on the settings & profile pages. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: var(--space-2);
}


/* ==========================================================================
   21 — AUDIO TOGGLE & VOLUME POPOVER
   ========================================================================== */

.audio-control {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: none;
}

.audio-toggle[aria-pressed="true"] {
  color: var(--cyan);
}

.audio-toggle[aria-pressed="false"] {
  color: var(--text-mute);
}

/* Three animated bars inside the audio button; frozen when muted. */
.audio-bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 16px;
}

.audio-bars span {
  display: block;
  width: 3px;
  height: 40%;
  border-radius: 1px;
  background: currentColor;
  transform-origin: bottom;
  animation: pw-eq 900ms var(--ease-in-out) infinite alternate;
}

.audio-bars span:nth-child(2) {
  animation-duration: 640ms;
  height: 90%;
}

.audio-bars span:nth-child(3) {
  animation-duration: 780ms;
  height: 62%;
}

.audio-toggle[aria-pressed="false"] .audio-bars span {
  animation: none;
  height: 30%;
  opacity: 0.6;
}

.volume-popover {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: var(--z-popover);
  width: 236px;
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  border-radius: var(--r-md);
  border: 1px solid var(--hairline-strong);
  background: rgba(var(--bg-elev-rgb), 0.97);
  box-shadow: var(--shadow-3), 0 0 40px -22px rgba(var(--cyan-rgb), 0.9);
  transform-origin: top right;
  animation: pw-pop-in var(--dur-2) var(--ease-spring) both;
}

@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .volume-popover {
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    background: rgba(var(--bg-elev-rgb), 0.86);
  }
}

/* Little pointer arrow. */
.volume-popover::before {
  content: "";
  position: absolute;
  top: -5px;
  right: 14px;
  width: 9px;
  height: 9px;
  transform: rotate(45deg);
  background: inherit;
  border-left: 1px solid var(--hairline-strong);
  border-top: 1px solid var(--hairline-strong);
}

.volume-popover__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 36px;
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-dim);
}

.volume-popover input[type="range"] {
  height: 32px;
}

.volume-popover__value {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--cyan);
  min-width: 4ch;
  text-align: right;
}

/* Popovers must escape any clipping ancestor; if a template nests one inside
   a scrolling rail, this keeps it usable. */
.side-rail .volume-popover,
.topbar .volume-popover {
  position: absolute;
}


/* ==========================================================================
   22 — STATUS PILLS, BADGES, CODE CHIPS, SKELETONS
   ========================================================================== */

.status-pill,
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.2em 0.6em;
  border-radius: var(--r-full);
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text-dim);
  background: rgba(var(--text-rgb), 0.06);
  border: 1px solid var(--hairline-soft);
}

.status-pill::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.status-pill[data-status="active"],
.status-pill--active {
  color: var(--lime);
  background: rgba(var(--lime-rgb), 0.1);
  border-color: rgba(var(--lime-rgb), 0.35);
}

.status-pill[data-status="waiting"],
.status-pill--waiting {
  color: var(--amber);
  background: rgba(var(--amber-rgb), 0.1);
  border-color: rgba(var(--amber-rgb), 0.35);
}

.status-pill[data-status="finished"],
.status-pill--finished {
  color: var(--text-dim);
}

.status-pill[data-status="abandoned"],
.status-pill--abandoned {
  color: var(--danger-text);
  background: rgba(var(--danger-rgb), 0.1);
  border-color: rgba(var(--danger-rgb), 0.3);
}

.status-pill--your-turn {
  color: var(--on-neon);
  background: var(--lime);
  border-color: transparent;
  box-shadow: 0 0 14px rgba(var(--lime-rgb), 0.5);
}

.status-pill--your-turn::before {
  background: rgba(var(--bg-rgb), 0.7);
  animation: pw-blink 1.4s var(--ease-in-out) infinite;
}

/* Sharable short code. */
.short-code {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-widest);
  color: var(--cyan);
  background: rgba(var(--cyan-rgb), 0.08);
  border: 1px dashed rgba(var(--cyan-rgb), 0.45);
  box-shadow: var(--glow-cyan);
  text-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.45);
  user-select: all;
  -webkit-user-select: all;
}

/* Key/value HUD readout. */
.kv {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-1) var(--space-3);
  font-size: var(--fs-xs);
}

.kv dt {
  color: var(--text-dim);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  font-family: var(--font-mono);
}

.kv dd {
  margin: 0;
  color: var(--text);
  font-family: var(--font-mono);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Loading skeleton. */
.skel {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-sm);
  background: var(--grid-line);
  color: transparent !important;
  min-height: 1em;
}

/* Shimmer via background-position: tiny element, negligible repaint. */
.skel::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(var(--cyan-rgb), 0.14),
    transparent
  );
  background-size: 200% 100%;
  animation: pw-sweep 0.7s linear infinite;
}

.skel--line {
  height: 0.9em;
  margin-block: 0.25em;
}

.skel--block {
  height: 100%;
  width: 100%;
}

/* Small inline spinner. */
.spinner {
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(var(--text-rgb), 0.18);
  border-top-color: var(--cyan);
  animation: pw-spin 700ms linear infinite;
}

/* Progress meter (AI thinking, upload, etc.). */
.meter {
  height: 4px;
  width: 100%;
  border-radius: var(--r-full);
  background: var(--grid-line);
  overflow: hidden;
}

.meter__fill {
  height: 100%;
  width: var(--value, 0%);
  border-radius: inherit;
  background: linear-gradient(90deg, var(--cyan), var(--violet));
  box-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.7);
  transition: width var(--dur-3) var(--ease-out);
}

.meter--indeterminate .meter__fill {
  width: 38%;
  animation: pw-indeterminate 1.2s var(--ease-in-out) infinite;
}

/* "Thinking" ellipsis used while the AI computes.
   The dots are real, static text and are revealed by clipping. Animating
   `content` (the old approach) relaid out the generated text node on every
   step; clip-path is paint-only. Width is 3ch — one cell per dot in the mono
   stack — because clip-path resolves against the border box, so the box has to
   actually contain all three dots.
   The box is taken out of flow (position: absolute) instead of sized inline,
   so it can never wrap onto a second line and adding/removing this class
   never changes its host's box size: #turn-indicator permanently reserves
   the trailing space this paints into, whether or not `thinking` is present
   (see 12 — PLAYER CARDS, WALL PIPS, TURN INDICATOR). `right: 0` anchors the
   3ch-wide (~1.7em) box to that reserved padding's outer edge, so it never
   overflows past it — it only overlaps a little past the padding's own inner
   edge, into the tail of the label text, same as the old inline version did. */
.thinking::after {
  content: "...";
  display: inline-block;
  position: absolute;
  right: 0;
  top: 0;
  width: 3ch;
  font-family: var(--font-mono);
  text-align: left;
  animation: pw-ellipsis 1.2s linear infinite;
}

/* Invite / share row. */
.share-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* QR / thumbnail placeholder slot. */
.thumb-slot {
  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  border-radius: var(--r-md);
  border: 1px dashed var(--hairline-strong);
  background: rgba(var(--bg-rgb), 0.5);
  color: var(--text-mute);
  font-size: var(--fs-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}


/* ==========================================================================
   23 — NO-JS FALLBACK
   The page must be readable and navigable with JS disabled or broken.
   Nothing essential is hidden by CSS alone; anything JS-only is opt-in via
   .requires-js, which templates may mark up freely.
   ========================================================================== */

.noscript,
noscript {
  display: block;
}

.noscript-note {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-4) auto;
  max-width: var(--prose-max);
  padding: var(--space-4);
  border-radius: var(--r-lg);
  border: 1px solid rgba(var(--amber-rgb), 0.45);
  background: rgba(var(--amber-rgb), 0.08);
  color: #ffe8c4;
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
}

.noscript-note strong,
.noscript-note b {
  color: var(--amber);
  font-family: var(--font-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  font-size: var(--fs-xs);
}

/* app.js is expected to remove `no-js` from <html> on boot. Until then the
   page shows its static content and hides the interactive-only bits. */
.no-js-only {
  display: none;
}

html.no-js .no-js-only {
  display: block;
}

html.no-js .requires-js {
  display: none !important;
}

/* A board that never received a renderer still shows a legible placeholder
   rather than a black square: templates put a .board-fallback inside the
   frame and board.js removes (or hides) it once the canvas has painted. */
.board-fallback {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  padding: var(--space-5);
  text-align: center;
  gap: var(--space-3);
  color: var(--text-dim);
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  background: repeating-linear-gradient(
      45deg,
      rgba(var(--grid-rgb), 0.5) 0 8px,
      transparent 8px 16px
    ),
    var(--bg-sunken);
}


/* ==========================================================================
   24 — PRINT
   Ink on paper: the neon inverts to a legible monochrome document. Board and
   move log survive; chrome, effects and controls do not.
   ========================================================================== */

@media print {
  html,
  body {
    background: #ffffff !important;
    color: #000000 !important;
  }

  html::before,
  html::after,
  body::before,
  body::after {
    display: none !important;
  }

  .topbar,
  .nav,
  .action-bar,
  .audio-control,
  .volume-popover,
  #toasts,
  .toasts,
  .skip-link,
  .site-footer,
  .btn,
  button,
  .replay,
  .net-status {
    display: none !important;
  }

  a {
    color: #000000 !important;
    text-decoration: underline;
  }

  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 0.8em;
  }

  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    color: #000000 !important;
    text-shadow: none !important;
    page-break-after: avoid;
    break-after: avoid;
  }

  .card,
  .panel,
  .player-card,
  .game-card,
  .settings-group,
  .move-log,
  .neon-frame,
  .board-frame {
    background: #ffffff !important;
    border: 1px solid #999999 !important;
    box-shadow: none !important;
    color: #000000 !important;
  }

  .neon-frame::before,
  .neon-frame::after,
  .board-frame::before,
  .board-frame::after,
  .topbar::after {
    display: none !important;
  }

  .player-card::before,
  .move-log__seat,
  .pip {
    box-shadow: none !important;
  }

  .prose,
  .move-log,
  .kv {
    color: #000000 !important;
  }

  .move-log {
    max-height: none !important;
    overflow: visible !important;
  }

  .move-log__row,
  .move-log li {
    color: #000000 !important;
    border-color: #cccccc !important;
    break-inside: avoid;
  }

  .board-frame {
    width: 120mm !important;
    max-width: 120mm !important;
    page-break-inside: avoid;
    break-inside: avoid;
  }

  .page,
  .play-main,
  .play-shell {
    display: block !important;
    max-width: none !important;
    padding: 0 !important;
    min-height: 0 !important;
  }
}


/* ==========================================================================
   25 — REDUCED MOTION
   Every decorative animation stops. Colour, contrast and layout are untouched,
   and state that was carried by motion (turn indicator, invalid preview) is
   restated statically.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  html {
    scroll-behavior: auto;
  }

  /* Ambient FX: keep the texture, kill the drift, soften the intensity.
     will-change is released too — nothing is animating, so holding a
     full-viewport compositing layer would be pure wasted memory. */
  body::before {
    animation: none !important;
    will-change: auto;
    --fx-grid-opacity: 0.4;
  }

  html::before,
  html::after {
    --fx-grid-opacity: 0.4;
  }

  .topbar::after,
  .wordmark__text,
  .skel::after,
  .neon-frame::before,
  .board-frame::before,
  .audio-bars span,
  .meter--indeterminate .meter__fill,
  .thinking::after {
    animation: none !important;
    will-change: auto;
  }

  .wordmark__text {
    background-position: 0 0;
  }

  /* Turn indicator: a solid ring and a solid dot instead of a pulse. */
  .player-card[data-turn="true"]::after,
  .player-card.is-turn::after {
    animation: none !important;
    border-width: 2px;
    border-color: var(--seat-color);
  }

  [data-turn="true"] .turn-dot,
  .is-turn .turn-dot,
  .status-pill--your-turn::before,
  .net-status__dot {
    animation: none !important;
    opacity: 1;
  }

  /* The action bar still appears, it just does not travel. */
  .action-bar__confirm {
    transform: none;
  }

  .action-bar[data-open="true"] .action-bar__confirm,
  .action-bar.is-open .action-bar__confirm,
  .action-bar[data-preview="pending"] .action-bar__confirm {
    transform: none;
  }

  /* Invalid preview: no shake — a persistent red frame instead. */
  .board-frame.is-invalid,
  .board-canvas.is-invalid {
    animation: none !important;
    box-shadow: 0 0 0 2px var(--danger), 0 0 30px -8px rgba(var(--danger-rgb), 0.9);
  }

  .game-card:hover {
    transform: none;
  }

  .btn:active {
    transform: none;
  }

  .btn--primary::after,
  input[type="submit"]::after {
    display: none;
  }
}

/* Users who want fewer transparencies (iOS "Reduce Transparency"). */
@media (prefers-reduced-transparency: reduce) {
  .topbar,
  .action-bar,
  .toast,
  .volume-popover,
  .board-badge {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: var(--bg-elev);
  }

  body::after {
    --fx-vignette: 0.3;
  }
}

/* Forced colours / Windows high contrast. */
@media (forced-colors: active) {
  html::before,
  html::after,
  body::before,
  body::after,
  .neon-frame::before,
  .board-frame::before,
  .topbar::after {
    display: none;
  }

  .btn,
  .card,
  .panel,
  .player-card,
  .game-card,
  .toast,
  .modal {
    border: 1px solid CanvasText;
  }

  .pip {
    forced-color-adjust: none;
  }

  :focus-visible {
    outline: 2px solid Highlight;
  }
}


/* ==========================================================================
   26 — KEYFRAMES
   All prefixed `pw-` to avoid collisions with anything else on the page.
   ========================================================================== */

/* One fine-grid period (48px) on both axes, so the loop wraps invisibly.
   translate3d, not background-position: this runs entirely on the compositor. */
@keyframes pw-grid-drift {
  /* -48 -> 0 rather than 0 -> +48: same drift direction, but the layer only
     ever extends into its top/left overhang, never past the viewport's right
     or bottom edge (mobile turns that into scrollable overflow). */
  from {
    transform: translate3d(-48px, -48px, 0);
  }
  to {
    transform: translate3d(0, 0, 0);
  }
}

@keyframes pw-grid-drift-coarse {
  from {
    transform: translate3d(-240px, -240px, 0);
  }
  to {
    transform: translate3d(0, 0, 0);
  }
}

/* background-position sweep. Still used by .wordmark__text only, where the
   gradient is clipped to the glyphs and so cannot be moved with a transform. */
@keyframes pw-sweep {
  from {
    background-position: 200% 0;
  }
  to {
    background-position: -200% 0;
  }
}

@keyframes pw-orbit {
  from {
    transform: translate(-50%, -50%) rotate(0turn);
  }
  to {
    transform: translate(-50%, -50%) rotate(1turn);
  }
}

@keyframes pw-spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes pw-pulse-ring {
  0% {
    opacity: 0.9;
    transform: scale(1);
  }
  55% {
    opacity: 0.15;
    transform: scale(1.012);
  }
  100% {
    opacity: 0.9;
    transform: scale(1);
  }
}

@keyframes pw-blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.28;
  }
}

@keyframes pw-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes pw-flash-in {
  0% {
    background-color: rgba(var(--cyan-rgb), 0.28);
  }
  100% {
    background-color: transparent;
  }
}

@keyframes pw-toast-in {
  from {
    opacity: 0;
    transform: translateY(-14px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes pw-toast-out {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-10px);
  }
}

@keyframes pw-modal-in {
  from {
    opacity: 0;
    transform: translateY(10px) scale(0.97);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes pw-sheet-in {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

@keyframes pw-pop-in {
  from {
    opacity: 0;
    transform: scale(0.94) translateY(-6px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

@keyframes pw-shake {
  0%,
  100% {
    transform: translateX(0);
  }
  15% {
    transform: translateX(-7px);
  }
  30% {
    transform: translateX(6px);
  }
  45% {
    transform: translateX(-4px);
  }
  60% {
    transform: translateX(3px);
  }
  80% {
    transform: translateX(-2px);
  }
}

/* The overlay is 260% of the button wide, matching the old background-size.
   background-position 160% on a 260%-wide image offsets it by 160% x (100-260)%
   = -2.56 button-widths; -60% offsets it by +0.96. As a fraction of the
   overlay's own width those are -98.5% and +36.9%. */
@keyframes pw-specular {
  from {
    transform: translate3d(-98.5%, 0, 0);
  }
  to {
    transform: translate3d(36.9%, 0, 0);
  }
}

@keyframes pw-eq {
  from {
    transform: scaleY(0.35);
  }
  to {
    transform: scaleY(1);
  }
}

@keyframes pw-indeterminate {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(300%);
  }
}

/* Reveals a static "..." one character-cell at a time. The old version animated
   `content`, which relaid out the generated text node on every step; clipping is
   paint-only. The stops are doubled so each state is held rather than tweened,
   which is what steps(4, end) used to do. */
@keyframes pw-ellipsis {
  0%,
  24% {
    clip-path: inset(0 100% 0 0);
  }
  25%,
  49% {
    clip-path: inset(0 66.67% 0 0);
  }
  50%,
  74% {
    clip-path: inset(0 33.34% 0 0);
  }
  75%,
  100% {
    clip-path: inset(0 0 0 0);
  }
}

/* ==========================================================================
   END — static/css/pallwall.css
   ========================================================================== */


/* Auto-play chip (app.js shows it beside #turn-indicator once the race is
   decided). Same row as the turn label, so it must not be able to wrap that
   row - the board below would jump. */
#autoplay-chip {
  white-space: nowrap;
  flex: 0 0 auto;
}

.status-pill--autoplay {
  --pill-rgb: var(--violet-rgb);
  color: rgb(var(--violet-rgb));
  border-color: rgba(var(--violet-rgb), 0.55);
  background: rgba(var(--violet-rgb), 0.12);
  box-shadow: 0 0 10px rgba(var(--violet-rgb), 0.35);
}


/* --- Narrow screens: the top bar must fit ---------------------------------
   .wordmark and .topbar__tools are both flex:none, so on a phone the row is
   wider than the viewport and LOG IN / SIGN UP hang ~34px off the right edge
   (measured at 390px). Below 520px drop the wordmark text (the PW mark stays
   as the home link), compact the tool buttons, and let the scrolling nav take
   whatever is left. Nothing in the bar may ever be wider than the viewport:
   that is what turns into sideways panning on mobile. */
@media (max-width: 519.98px) {
  .topbar__inner {
    gap: var(--space-2);
    padding-inline: var(--space-2);
    min-width: 0;
  }

  .wordmark__text {
    display: none;
  }

  .nav {
    flex: 1 1 0;
    min-width: 0;
  }

  .topbar__tools {
    flex: 0 0 auto;
    gap: 2px;
  }

  .topbar__tools .btn {
    padding-inline: var(--space-2);
    font-size: var(--fs-2xs);
    letter-spacing: var(--tracking-wide);
  }
}

/* Player-card meta row (badge + PATH stat): it was a non-wrapping flex row,
   and .player-card is overflow:hidden, so on a phone the PATH value was
   silently clipped (measured 20-40px past the card edge at 390-320px). Let
   the row wrap; keep each stat on one line. */
.player-card__meta {
  flex-wrap: wrap;
  row-gap: 2px;
  min-width: 0;
}

.player-card .path-stat {
  min-width: 0;
  white-space: nowrap;
}

/* Two player cards side by side do not fit a phone: at 360px the wall pips
   and count overlapped the PATH / TO MOVE text. Stack them below 600px. */
@media (max-width: 599.98px) {
  .players--row {
    display: flex;
    flex-direction: column;
    grid-template-columns: minmax(0, 1fr);
  }

  /* The <=959px row layout gives each card flex: 1 1 0. In a column that zero
     basis collapses the container, and .player-card is overflow:hidden so its
     automatic min-height is 0 - the cards squashed to ~34px and overlapped. */
  .players--row .player-card {
    flex: 0 0 auto;
    min-width: 0;
  }
}
