/* =============================================================================
   Paofuqi shared design system — ONE stylesheet linked by every page.
   Published to /assets/paofuqi-ui.css. New pages/games/articles adopt the whole
   look by linking this file and using the classes below.

   Style: Flat (PLAN-001) — brand teal #58C9B5 from the logo, dark ink text,
   borders over shadows, no gradients. Visual baseline: design-preview/index.html.

   Contains: design tokens, brand bar, hero, toolbar, segmented control, buttons,
   cards, article prose, master-guide components (step TOC, guide meta, board
   figures), learning-path lists, footer, one page-load reveal.
   Game-specific CSS (board, stones, timers, online dialogs) stays with each game.
   ========================================================================== */

@import url("https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;500;600;700;800&family=Noto+Sans+TC:wght@400;500;700&display=swap");

:root{
  --bg:#ffffff; --surface:#ffffff; --ink:#2c2c2c; --muted:#6b7280;
  --line:#d5e5e0; --accent:#58c9b5; --accent-ink:#2a7d73; --accent-soft:#eaf8f4;
  --accent-strong:#1e8e7c; --accent-hover:#40a394; --accent-active:#2a7d73;
  --surface-tint:#f2f9f7;
  --accent-2:#58c9b5;                 /* brand teal, from the Paofuqi logo */
  --radius:8px;
  --shadow-sm:0 1px 2px rgba(44,44,44,.05);
  --shadow-md:0 1px 2px rgba(44,44,44,.05);
  --shadow-lg:0 1px 3px rgba(44,44,44,.06);

  /* ---- elevation scale (2026-08-06) -------------------------------------
     The three --shadow-* tokens above are a legacy of the Flat era: sm and md
     were byte-identical and lg differed by 1px, so nothing on the site could
     ever look like it sat above anything else. They stay untouched (52 pages
     reference them); new work uses this scale, which actually separates.
     1 = resting card · 2 = raised card / hovered · 3 = popover / floating panel
     · 4 = modal. */
  --elev-1:0 1px 2px rgba(44,44,44,.06);
  --elev-2:0 2px 6px rgba(44,44,44,.10), 0 1px 2px rgba(44,44,44,.06);
  --elev-3:0 8px 20px rgba(44,44,44,.12), 0 2px 6px rgba(44,44,44,.08);
  --elev-4:0 20px 48px rgba(44,44,44,.20), 0 6px 14px rgba(44,44,44,.10);

  /* ---- second + third colour families (2026-08-06) ----------------------
     Until now the whole system had exactly one accent family, so it could not
     express more than two levels of action ("teal or not teal"). Gold carries
     reward / league / premium, danger carries resign & timeout. Both are tuned
     against the warm neutrals, not bolted on from a generic palette. */
  --gold:#c9963c; --gold-hover:#b8862f; --gold-soft:#fdf5e4; --gold-ink:#8a6420;
  --danger:#c0392b; --danger-hover:#a93226; --danger-soft:#fdeceb; --danger-ink:#96271c;

  /* ---- board material (2026-08-06) --------------------------------------
     Shared so the four games agree on what wood looks like. The renderers own
     the geometry; this owns the colour. */
  /* Darkened 2026-08-07 to the luminance of a real board (see the note in
     shared/board-material.js — that file is the source of truth for the same
     four values; these exist so CSS can reach them). --wood-line moves with the
     base so the board/line contrast ratio stays at its measured 3.68. */
  --wood-base:#7e583b; --wood-shade:#65462f; --wood-line:#24170e; --wood-edge:#352214;
  --wood-light:#8c6242;

  /* ---- radius scale (2026-08-06) ----------------------------------------
     One `--radius:8px` was doing containers AND controls, which is why cards
     read too round and buttons too square at the same time.
     Measured on draughts.io: card 18px, primary button 12–13px, toggle 999px.
     The outer NUMBER is the bigger one — what makes a container read squarer is
     radius relative to its size (18/300 ≈ 0.06 against 12/46 ≈ 0.26). So the
     rule is not "smaller radius outside", it is "same absolute step, and let
     the size do the work".
     `--radius` itself is untouched: 52 live pages reference it. */
  --radius-card:18px;      /* panels, dialogs, surfaces */
  --radius-control:12px;   /* buttons, selects, inputs */
  --radius-board:10px;     /* the board plate itself */
  --radius-pill:999px;     /* switches, chips, segmented tracks */
  /* A control taller than ~60px needs a bigger absolute radius to keep the same
     PERCEIVED roundness. Measured on the gomoku page before this token existed:
     the 72px primary button sat at 12/72 = 0.167 while the 63px player card it
     sits under was at 12/63 = 0.191 — the control read squarer than its own
     container, which is the rule backwards. 16/72 = 0.222 puts it back on top. */
  --radius-control-lg:16px;
  /* Translucent, not an opaque grey. An opaque border is only correct on the
     one background it was picked for; this one sits on any surface.
     draughts.io: rgba(231,242,227,0.1) on a near-black page. */
  --line-soft:rgba(44,44,44,.12);

  /* ---- motion (2026-08-06) ----------------------------------------------
     ANIMATION_AND_AUDIO_PLAYBOOK §3.5 settled these durations on 2026-07-28,
     but only as a table in a markdown file — the one implementation lives in
     bigtwo_src/style.css as --bt-t-*. Same failure mode as the visual system:
     a spec that gets re-typed per page instead of a shared layer. Values are
     identical to bigtwo's so it can adopt these without changing behaviour.
       tap   120-150ms  select / toggle / press
       move  200-250ms  a piece or card travelling
       clear 150-200ms  fading something out
       enter 300-400ms  one-shot entrances (deal, dialog) */
  --t-tap:140ms; --t-move:220ms; --t-clear:180ms; --t-enter:320ms;
  /* Decelerating: things arrive, they do not overshoot. Matches --bt-ease. */
  --ease-out:cubic-bezier(.22,.61,.36,1);
  --ease-in-out:cubic-bezier(.4,0,.2,1);

  /* ---- icon tints (2026-08-06) ------------------------------------------
     One teal for every icon made all settings look equally important. Each
     function gets its own hue so the eye can find "difficulty" without
     reading. Tuned against the warm neutrals; every pair is measured against
     the 3:1 graphical-object floor in both themes. */
  --tint-teal:#1e8e7c;   --tint-teal-soft:#eaf8f4;
  --tint-gold:#a8751f;   --tint-gold-soft:#fdf3e0;
  --tint-coral:#b8442c;  --tint-coral-soft:#fdeee9;
  --tint-violet:#6b4bb0; --tint-violet-soft:#f2eefb;
  --tint-sky:#1f6f96;    --tint-sky-soft:#e8f4fa;

  /* ---- control material (2026-08-06) -------------------------------------
     The board got a material and the controls did not — and the controls are
     the part you actually touch. Every button on the site measured
     `background-image:none`, so the most important action on a page was a flat
     rectangle of accent colour lying on the surface.
     Two declarations do the whole job: a vertical sheen (light collects at the
     top of a convex surface) and a 1px inset highlight along the top edge,
     which is what reads as "this has a lip". Both are tokens so the dark theme
     can invert them rather than needing its own copy of every button rule. */
  --sheen:linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,0) 62%);
  --sheen-lip:rgba(255,255,255,.6);
  --sheen-strong:linear-gradient(180deg, rgba(255,255,255,.30), rgba(0,0,0,.07));
  --sheen-lip-strong:rgba(255,255,255,.42);

  /* ---- scrollbars (2026-08-06) -------------------------------------------
     There was not one `::-webkit-scrollbar` or `scrollbar-color` rule on the
     site, so every scrolling panel — the move list, the sidebar, the settings
     sheet — drew the OS default: a bright wide bar in a light track, sitting on
     our dark warm surface. It is the single most visible "unfinished" cue on a
     dark page, and it costs four lines to fix. */
  --sb-thumb:rgba(44,44,44,.22);
  --sb-thumb-hover:rgba(44,44,44,.38);
  --sb-track:transparent;
  --font-display:'Fredoka','Noto Sans TC',system-ui,sans-serif;
  --font-body:'Nunito','Noto Sans TC',system-ui,-apple-system,"Segoe UI",sans-serif;
  --maxw:980px;
  --page-inline-pad:20px;
  --site-nav-block-size:64px;
  --site-header-border-size:1px;
  --site-header-block-size:calc(var(--site-nav-block-size) + var(--site-header-border-size));
}

*{box-sizing:border-box}

/* Both syntaxes, deliberately. Chrome 121+ honours the standard properties and
   then IGNORES ::-webkit-scrollbar entirely, while older Chrome and every
   WebKit browser only understand the pseudo-elements — so each engine has to be
   given the same answer separately or half the users keep the OS bar.
   `scrollbar-color` inherits, so declaring it on html covers every descendant
   without a universal selector. */
/* scrollbar-color INHERITS, scrollbar-width does NOT — so the colour only has
   to be declared once on <html>, but the width has to reach every scrolling
   element or the inner panels keep the OS default gauge (measured `auto` on the
   move list while its colour was already correct). */
html{scrollbar-color:var(--sb-thumb) var(--sb-track)}
*{scrollbar-width:thin}
/* The theme class lives on <body>, but this declaration is on <html> — so it
   resolves var(--sb-thumb) against the ROOT tokens and the computed colour then
   inherits downwards. Overriding --sb-* inside .pf-dark therefore changes
   nothing: measured rgba(44,44,44,.22), the light thumb, on a fully dark page.
   Hoisting the override to <html> fixes the document scrollbar AND every inner
   one in a single declaration, because what inherits is the already-resolved
   colour. */
html:has(> body.pf-dark){
  --sb-thumb:rgba(245,239,227,.17);
  --sb-thumb-hover:rgba(245,239,227,.30);
}
*::-webkit-scrollbar{width:10px; height:10px}
*::-webkit-scrollbar-track{background:var(--sb-track)}
*::-webkit-scrollbar-thumb{
  background:var(--sb-thumb); border-radius:999px;
  /* A transparent border clipped to the padding box is the only way to inset a
     webkit thumb; without it the bar touches both walls and reads as a slab. */
  border:2px solid transparent; background-clip:padding-box;
}
*::-webkit-scrollbar-thumb:hover{background:var(--sb-thumb-hover); background-clip:padding-box}
*::-webkit-scrollbar-corner{background:var(--sb-track)}

/* The `body` qualifier is load-bearing: without it these tie on specificity with
   later component rules that set display (e.g. `.site-footer .foot a{display:block}`)
   and lose on source order, so a link for the other language would still show. */
html[data-lang="en"] body .lang-zh{display:none}
html[data-lang="zh"] body .lang-en{display:none}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

body{
  margin:0; color:var(--ink);
  background:var(--bg);
  font-family:var(--font-body);
  /* Body stays loose and untracked on purpose — it is the half of the contrast
     that is easy to forget. ⚠️ Never put negative letter-spacing on body copy
     here: this site sets Chinese, and tracking applies BETWEEN every 漢字, so a
     value that merely tightens Latin visibly crowds CJK. */
  line-height:1.6; letter-spacing:normal; -webkit-text-size-adjust:100%;
}
/* ---- type hierarchy （安咕 D 塊, 2026-08-12） -------------------------------
   The complaint was that the display face and the body face read the same:
   Fredoka and Nunito are both rounded, so a heading only announced itself by
   being a bit bigger. Family changes are 安咕's call, so the contrast has to
   come from weight, tracking and leading instead — which is where it should
   come from anyway.

   ⚠️ The weight was 600, and 600 is not a real weight in half the stack.
   Loaded: Fredoka 400/500/600/700, Nunito 400…800, **Noto Sans TC 400/500/700**.
   CSS font matching resolves a requested 600 upward when 600 is missing, so on
   the SAME heading the English rendered at 600 and the Chinese at 700 — one
   line, two weights, depending on which glyph you were looking at. 700 is a
   real weight in all three, so this also makes the two languages agree.

   Leading 1.25 -> 1.18 and -.01em tracking are the other half: display type is
   set tighter than body text, and 1.6/1.18 is a gap you can see. The tracking
   matches the value .prose h1 has used since June, so nothing on the article
   pages shifts. */
h1,h2,h3,h4{
  font-family:var(--font-display); font-weight:700;
  line-height:1.18; letter-spacing:-.01em;
}
a{color:var(--accent-ink)}

.wrap{width:min(var(--maxw),100%); margin:0 auto; padding:28px var(--page-inline-pad) 56px}

/* ---- brand bar ---------------------------------------------------------- */
.page-nav{display:flex; align-items:center; justify-content:flex-start; gap:14px; margin-bottom:18px}
.brand-link{
  display:inline-flex; align-items:center; gap:10px; text-decoration:none;
  padding:6px 8px; border-radius:var(--radius-control); transition:background .18s;
}
.brand-link:hover{background:var(--accent-soft)}
.brand-link:focus-visible{outline:3px solid var(--accent-active); outline-offset:2px}
.brand-logo{height:34px; width:auto; display:block}
.brand-home{font-family:var(--font-display); font-size:13px; font-weight:600; color:var(--muted); letter-spacing:.01em; padding-left:2px}
.brand-link:hover .brand-home{color:var(--accent-strong)}

/* ---- hero --------------------------------------------------------------- */
.hero{text-align:center; margin-bottom:22px}
.hero .eyebrow{
  display:inline-block; font-family:var(--font-display); font-size:13px; font-weight:600;
  color:var(--accent-strong); letter-spacing:.04em;
  padding:4px 12px; border-radius:999px; background:var(--accent-soft);
  border:1px solid var(--line);
}
.hero h1{margin:12px 0 8px; font-size:clamp(2rem,5vw,2.6rem); font-weight:700; letter-spacing:-.01em}
.hero .tagline{margin:0 auto; max-width:620px; color:var(--muted); font-size:15px}

/* ---- toolbar + selects -------------------------------------------------- */
.toolbar{display:flex; justify-content:center; align-items:center; gap:10px 16px; flex-wrap:wrap; margin-bottom:20px}
.control-field{display:inline-flex; align-items:center; gap:8px; font-size:13px; font-weight:600; color:var(--muted); letter-spacing:.01em}
.control-select{
  font:inherit; font-weight:600; padding:8px 12px; border:1px solid var(--line-soft);
  /* Was --radius (8px). A select is a control, not a panel — it was the one
     control still reading from the container token, which is why it looked
     squarer than the buttons standing next to it. */
  border-radius:var(--radius-control);
  background-color:var(--surface); background-image:var(--sheen); color:var(--ink); cursor:pointer;
  box-shadow:var(--elev-1), inset 0 1px 0 var(--sheen-lip);
  transition:border-color .18s, box-shadow .18s;
}
.control-select:hover{border-color:var(--accent); box-shadow:var(--elev-2), inset 0 1px 0 var(--sheen-lip)}
.control-select:focus-visible{outline:3px solid var(--accent-active); outline-offset:1px; border-color:var(--accent)}

/* ---- segmented control (pill track) -------------------------------------- */
/* The track is a well, so it gets an INSET shadow, not a drop shadow — the
   selected pill is the thing that sits above the surface, and it cannot read
   that way unless the groove it slides in reads as recessed. */
.segmented,.difficulty-buttons{
  display:inline-flex; gap:3px; padding:3px; border-radius:var(--radius-pill);
  background:var(--surface-tint); border:1px solid var(--line-soft);
  box-shadow:inset 0 1px 2px rgba(44,44,44,.07);
}
.pf-dark .segmented,.pf-dark .difficulty-buttons{box-shadow:inset 0 1px 3px rgba(0,0,0,.35)}
.seg,.difficulty-btn{
  font:inherit; font-family:var(--font-display); font-size:13px; font-weight:600; min-height:32px; padding:6px 18px;
  border:0; border-radius:999px; background:transparent; color:var(--muted);
  cursor:pointer; white-space:nowrap; transition:color .18s, background .18s;
}
.seg:hover,.difficulty-btn:hover{color:var(--ink)}
.seg.active,.difficulty-btn.active{
  background-color:var(--accent); background-image:var(--sheen-strong); color:var(--ink);
  box-shadow:var(--elev-1), inset 0 1px 0 var(--sheen-lip-strong);
}
.seg:focus-visible,.difficulty-btn:focus-visible{outline:3px solid var(--accent-active); outline-offset:2px}

/* ---- buttons (Flat) ------------------------------------------------------ */
.btn{
  font:inherit; font-family:var(--font-display); font-size:14px; font-weight:600; padding:10px 18px;
  /* --radius-control (12px), not --radius (8px). A control and a panel sharing
     one radius is why cards read too round and buttons too square at the same
     time; the button is the small element, so it needs the larger number to
     read as rounded. Site-wide on purpose — every page's buttons move. */
  border-radius:var(--radius-control); cursor:pointer; border:1px solid var(--line-soft);
  /* background-COLOR, not the shorthand, everywhere below: the shorthand resets
     background-image, so a single `background:` in a :hover rule silently wipes
     the sheen off the button the moment the pointer arrives. */
  background-color:var(--surface); background-image:var(--sheen); color:var(--ink);
  box-shadow:var(--elev-1), inset 0 1px 0 var(--sheen-lip);
  text-decoration:none; display:inline-flex; align-items:center; gap:6px;
  transition:background-color .18s, border-color .18s, color .18s,
             box-shadow .18s, transform .06s;
}
.btn:hover{background-color:var(--surface-tint); border-color:var(--accent); box-shadow:var(--elev-2), inset 0 1px 0 var(--sheen-lip)}
/* Pressing removes the lift as well as moving the button: a control that drops
   1px but keeps its shadow reads as sliding, not as being pushed. */
.btn:active{transform:translateY(1px); box-shadow:var(--elev-1)}
.btn:focus-visible{outline:3px solid var(--accent-active); outline-offset:2px}
.btn.primary{
  background-color:var(--accent); background-image:var(--sheen-strong);
  border-color:var(--accent); color:var(--ink);
  /* The primary action is the one thing on the page allowed to float. */
  box-shadow:var(--elev-2), inset 0 1px 0 var(--sheen-lip-strong);
}
.btn.primary:hover{background-color:var(--accent-hover); border-color:var(--accent-hover); box-shadow:var(--elev-3), inset 0 1px 0 var(--sheen-lip-strong)}
.btn.primary:active{background-color:var(--accent-active); border-color:var(--accent-active); color:#fff; box-shadow:var(--elev-1)}
.btn.strong{background-color:var(--accent-strong); background-image:var(--sheen-strong); border-color:var(--accent-strong); color:#fff}
.btn.strong:hover{background-color:var(--accent-active); border-color:var(--accent-active)}
.btn.small{padding:6px 12px; font-size:13px; min-height:34px}
/* Relative curvature: a control this tall needs the larger absolute radius to
   stay rounder than the card it sits in. See --radius-control-lg. */
.btn.lg,.btn.tall{border-radius:var(--radius-control-lg)}
/* An icon button is square and centres its icon; the text padding above leaves
   a glyph jammed against the edge. */
.btn.icon-only{padding:0; min-inline-size:38px; min-block-size:38px; justify-content:center; gap:0}
.btn.icon-only svg{inline-size:16px; block-size:16px; display:block}
.btn:disabled{opacity:.45; cursor:default}
/* Same trap as the .pf-* components below: `.btn{display:inline-flex}` is a
   class selector and beats the UA's type-less `[hidden]{display:none}`, so every
   `button.hidden = true` in the game pages was a no-op. That is why the opening
   roll button stayed on screen after the roll was decided, and why Rematch /
   Resign / Share sat in the room card before there was anything to rematch. */
.btn[hidden]{display:none}

/* ---- generic surface card (pages may extend; games override) ------------ */
/* Containers below read --radius-card, controls read --radius-control. Before
   this pass a single --radius:8px served both, so a 300px panel and a 38px
   button were given the same absolute corner — which the eye reads as the panel
   being too round AND the button too square at the same time. The absolute step
   goes UP for the container (8 → 18) and the size does the rest: 18/300 = 0.06
   against 12/38 = 0.32. --radius itself is left alone; it is still referenced by
   pages this system has not reached. */
.surface-card{background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-card); padding:16px 18px}

/* ---- article prose ------------------------------------------------------ */
.prose{
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-card);
  padding:clamp(24px,5vw,48px); line-height:1.72;
}
.prose h1{font-size:clamp(2rem,5vw,3rem); line-height:1.14; margin:8px 0 14px; letter-spacing:-.01em; font-weight:700}
.prose h2{margin:2.1rem 0 .7rem; line-height:1.25; font-size:1.5rem}
.prose h3{margin:1.4rem 0 .4rem; font-size:1.18rem; font-weight:600; letter-spacing:normal}
.prose p, .prose li{font-size:1.02rem}
.prose li + li{margin-top:.45rem}
.prose .dek{color:var(--muted); font-size:1.13rem; margin:0 0 18px}
.prose .category{color:var(--accent-strong); font-size:.78rem; font-weight:800; text-transform:uppercase; letter-spacing:.04em}
.prose .byline{color:var(--muted); border-top:1px solid var(--line); border-bottom:1px solid var(--line); padding:12px 0; font-size:.92rem}
.prose .note{margin:20px 0; padding:16px 18px; background:#fff5e6; border-left:4px solid #c98a3a; border-radius:0 var(--radius-control) var(--radius-control) 0}
.prose .table-wrap{overflow-x:auto; margin:18px 0}
.prose table{width:100%; border-collapse:collapse; min-width:560px}
.prose th, .prose td{border:1px solid var(--line); padding:10px 12px; text-align:left; vertical-align:top}
.prose th{background:var(--accent-soft); font-family:var(--font-display); font-weight:600}
.prose .cta{margin-top:32px; padding:20px; background:var(--accent-soft); border:1px solid var(--line); border-radius:var(--radius-card); text-align:center}
.prose .cta a{display:inline-block; margin-top:6px; padding:10px 16px; background-color:var(--accent-strong); background-image:var(--sheen-strong); color:#fff; text-decoration:none; font-family:var(--font-display); font-weight:600; border-radius:var(--radius-control); box-shadow:var(--elev-2), inset 0 1px 0 var(--sheen-lip-strong); transition:background-color .18s, box-shadow .18s}
.prose .cta a:hover{background-color:var(--accent-active); box-shadow:var(--elev-3), inset 0 1px 0 var(--sheen-lip-strong)}
.prose .sources{margin-top:42px; padding-top:22px; border-top:1px solid var(--line)}
.prose .related{margin-top:24px; display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:12px}
.prose .related-card{display:grid; gap:4px; padding:16px; border:1px solid var(--line); border-radius:var(--radius-card); text-decoration:none; color:var(--ink); background:var(--surface-tint); transition:border-color .18s, transform .12s}
.prose .related-card:hover{border-color:var(--accent); transform:translateY(-2px)}
.prose .related-card span{color:var(--muted); font-size:.9rem}
.crumbs{font-size:.9rem; margin-bottom:20px; color:var(--muted)}
.crumbs a{text-decoration:none}

/* ---- master guide: meta row, difficulty badge, step TOC, board figures --- */
.guide-meta{display:flex; align-items:center; flex-wrap:wrap; gap:10px 14px;
  color:var(--muted); font-size:.92rem;
  border-top:1px solid var(--line); border-bottom:1px solid var(--line); padding:12px 0; margin:0 0 6px}
.guide-meta .avatar{width:30px; height:30px; border-radius:999px; background:var(--accent-soft);
  border:1px solid var(--line); display:inline-flex; align-items:center; justify-content:center; overflow:hidden}
.guide-meta .avatar img{width:22px; height:22px; object-fit:contain; display:block}
.badge-diff{display:inline-block; background:var(--accent-soft); color:var(--accent-strong);
  font-family:var(--font-display); font-weight:600; font-size:.8rem; padding:.25em .9em; border-radius:999px}
.step-toc{background:var(--surface-tint); border:1px solid var(--line); border-radius:var(--radius-card);
  padding:18px 22px; margin:22px 0}
.step-toc > strong{font-family:var(--font-display); font-weight:600; font-size:1.02rem}
.step-toc ol{margin:10px 0 0; padding-left:1.3em}
.step-toc li{padding:3px 0}
.step-toc a{color:var(--accent-ink); font-weight:700; text-decoration:none}
.step-toc a:hover{text-decoration:underline}
.figure-board{margin:22px auto; text-align:center; max-width:560px}
.figure-board svg{max-width:100%; height:auto; display:block; margin:0 auto;
  border:1px solid var(--line); border-radius:var(--radius-board); background:#fff}
.figure-board figcaption{color:var(--muted); font-size:.9rem; margin-top:8px; text-align:center}

/* card grid used by home + guides landing */
.card-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:14px}

/* narrow reading column (articles, contact) */
.wrap.narrow{width:min(840px,100%)}
.lede{max-width:720px; color:var(--muted); font-size:1.08rem}

/* simple link tiles (guides landing, contact reasons) + guide sections */
.tile-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:14px}
.tile{display:block; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-card);
  padding:18px; text-decoration:none; color:var(--ink);
  transition:border-color .18s, transform .12s}
a.tile:hover{border-color:var(--accent); transform:translateY(-2px)}
.tile > span{color:var(--accent-strong); font-size:.75rem; font-weight:800; text-transform:uppercase; letter-spacing:.04em}
.tile strong{display:block; font-size:1.08rem; margin:5px 0}
.tile p{color:var(--muted); margin:0; font-size:.92rem}
.tile h3{margin:0 0 7px; font-size:1.02rem}
.guide section{margin:44px 0}
.guide section > h2{font-size:1.6rem; margin:0 0 6px}
.guide h3{font-size:1.05rem; color:var(--muted); font-weight:800; text-transform:uppercase; letter-spacing:.04em; margin:26px 0 12px}

/* ---- learning path (guides hub) ------------------------------------------ */
.path{background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-card);
  padding:18px 22px; margin:14px 0}
.path > h3{margin:0 0 4px}
.path > p{color:var(--muted); margin:0 0 8px; font-size:.95rem}
.path ol{list-style:none; counter-reset:pathstep; margin:0; padding:0}
.path li{counter-increment:pathstep; display:flex; align-items:baseline; gap:12px; padding:10px 0; border-top:1px dashed var(--line)}
.path li:first-child{border-top:0}
.path li::before{content:counter(pathstep); flex:0 0 26px; height:26px; border-radius:999px;
  background:var(--accent); color:var(--ink); font-family:var(--font-display); font-weight:600; font-size:.85rem;
  display:inline-flex; align-items:center; justify-content:center; transform:translateY(4px)}
.path a{font-weight:700; color:var(--ink); text-decoration:none}
.path a:hover{color:var(--accent-strong); text-decoration:underline}
.path li > div > span{display:block; color:var(--muted); font-size:.9rem}

/* deferred in-content ad slots (hidden until a real unit is enabled) */
.future-ad-zone{display:none}
.future-ad-zone[data-ad-enabled="true"]{width:100%; min-height:250px; margin:40px 0; display:grid; place-items:center}

/* ---- footer ------------------------------------------------------------- */
footer{text-align:center; margin-top:40px; color:var(--muted); font-size:14px}
footer a{color:var(--muted)}

/* ---- one orchestrated page-load reveal (staggered, not scattered) ------- */
@media (prefers-reduced-motion:no-preference){
  /* Two animations, not one, and only the fade fills. An element with a
     *filling* transform animation is a containing block for position:fixed
     descendants FOREVER — even after it ends on `transform:none`, because the
     fill keeps the animation applied. That made .game-stage the containing
     block for every fixed overlay inside it: measured, a fixed inset:0 element
     in the stage came out 350x667 @20 on a 390x760 phone instead of 390x760 @0,
     so the online connect dialog was 40px narrower than the dice and result
     dialogs beside it. The fade has to fill (the base state is opacity:0); the
     rise does not, so once it finishes the trap disappears. Same motion, same
     .5s, same stagger — a single animation-delay value applies to both. */
  @keyframes pfRise{from{opacity:0} to{opacity:1}}
  @keyframes pfRiseMove{from{transform:translateY(12px)} to{transform:none}}
  .pf-reveal > *{opacity:0;
    animation:pfRise .5s cubic-bezier(.22,.61,.36,1) forwards,
              pfRiseMove .5s cubic-bezier(.22,.61,.36,1)}
  .pf-reveal > *:nth-child(1){animation-delay:.02s}
  .pf-reveal > *:nth-child(2){animation-delay:.09s}
  .pf-reveal > *:nth-child(3){animation-delay:.16s}
  .pf-reveal > *:nth-child(4){animation-delay:.23s}
  .pf-reveal > *:nth-child(n+5){animation-delay:.30s}
}
@media (prefers-reduced-motion:reduce){
  *{transition:none!important}
}

@media (max-width:680px){
  .hero h1{font-size:1.9rem}
  .toolbar{gap:8px 12px}
  .prose{padding:22px 16px}
}

/* =============================================================================
   Reusable button variants (additive — existing .btn / .btn.primary / .btn.small
   unchanged). Adopted by the landing skeleton and available site-wide.
   ========================================================================== */
/* The transparent ranks stay flat. Material describes a solid object catching
   light; a sheen and a drop shadow on something you can see through is the
   cheap-looking version of exactly what this change is trying to fix. The
   `background:` shorthands below already clear the sheen — the shadow has to be
   cleared by hand. */
.btn.outline{background:transparent; border-color:var(--accent); color:var(--accent-strong); box-shadow:none}
.btn.outline:hover{background:var(--accent-soft); border-color:var(--accent); box-shadow:none}
.btn.ghost{background:transparent; border-color:transparent; color:var(--accent-strong); box-shadow:none}
.btn.ghost:hover{background:var(--accent-soft); box-shadow:none}
.btn.lg{font-size:1.05rem; padding:12px 24px}

/* =============================================================================
   Landing skeleton — ported verbatim from design-preview/index.html and scoped
   under .pf-landing so pages still using the centered .hero / bare section /
   centered footer are unaffected. The homepage is rebuilt on this; future pages
   (U5/U6) adopt the whole skeleton by adding class="pf-landing" to <body>.

   Full-bleed sections (header/hero/cta) span the viewport; an inner .pf-wrap
   holds the max-width column. Colors map design-preview vars -> shared tokens:
   --surface(tint)->--surface-tint, --accent-bg->--accent-soft, --fg->--ink,
   --border->--line.
   ========================================================================== */
/* ---- dark theme: warm, one surface, derived from our own boards -----------
   Carried by a body class (.pf-landing = homepage, .pf-doc = the article/guide/
   utility shell), NOT :root — so a page opts in and the game pages (their own
   inlined CSS) and anything not yet converted stay on the light default. It
   works as a token override because every shared component (.btn, .tile, .prose,
   .control-select, .site-header, .site-footer) already paints from these vars.

   The palette is derived from OUR boards, not a generic dark mode: the base is
   the gomoku board's darkest line colour (#3d2a14) taken down to near-black, and
   the text is the xiangqi board's paper (#f4e4c1) taken up to near-white. So the
   page is the same material as the boards on it, and the wood/paper glows instead
   of fighting a cool grey. Teal stays the one accent, spent on a single filled
   element per screen (chess.com's discipline, our colour); "-strong" lifts to
   #7ad8c6 because #1e8e7c is unreadable on dark, and accent fills therefore take
   dark ink (--accent-onink) rather than white. */
/* .pf-dark is the theme class every dark body carries (homepage .pf-landing,
   the .pf-doc reading shell, and the .*-page game pages). It holds only the
   token override + the shared-component white/light-accent fixes, so a page opts
   in by adding the class and any page not yet converted keeps the light default. */
.pf-dark{
  --bg:#1e1b17; --surface:#2b2722; --surface-tint:#221f1a;
  --ink:#f5efe3; --muted:#a49b8f; --line:#423b33;
  --accent:#58c9b5; --accent-hover:#6ed2c0; --accent-active:#45b3a0;
  --accent-strong:#7ad8c6; --accent-ink:#7ad8c6; --accent-soft:#26302c;
  --accent-onink:#12211e;                 /* readable ink for text on a teal fill */
  --shadow-sm:0 1px 2px rgba(0,0,0,.3);
  --shadow-md:0 2px 6px rgba(0,0,0,.35);
  --shadow-lg:0 6px 20px rgba(0,0,0,.4);
  /* Dark needs a wider spread to read as depth: on a dark surface a shadow is
     only visible where it is large and soft, so the steps are further apart
     than in light. */
  --elev-1:0 1px 2px rgba(0,0,0,.40);
  --elev-2:0 3px 8px rgba(0,0,0,.45), 0 1px 2px rgba(0,0,0,.35);
  --elev-3:0 10px 26px rgba(0,0,0,.50), 0 3px 8px rgba(0,0,0,.40);
  --elev-4:0 24px 60px rgba(0,0,0,.60), 0 8px 18px rgba(0,0,0,.45);
  /* Gold and danger lift for legibility on #1e1b17, same as accent does. */
  --gold:#e0b25c; --gold-hover:#eec177; --gold-soft:#332a1c; --gold-ink:#f0c877;
  --danger:#e0574a; --danger-hover:#ea6d61; --danger-soft:#3a2420; --danger-ink:#f0917f;
  /* Icon tints lift the same way; the soft backgrounds become warm-dark chips
     rather than pale washes, so they read as recessed wells on the surface. */
  --tint-teal:#7ad8c6;   --tint-teal-soft:#26302c;
  --tint-gold:#f0c877;   --tint-gold-soft:#332a1c;
  --tint-coral:#f0917f;  --tint-coral-soft:#3a2420;
  --tint-violet:#c3aeea; --tint-violet-soft:#2b2635;
  --tint-sky:#8fc9e6;    --tint-sky-soft:#1f2b33;
  --line-soft:rgba(245,239,227,.10);
  /* Dark inverts the material rather than dimming it. On a dark control the
     light still collects at the top, but it is a warm 8% lift instead of a 55%
     white wash, and the bottom is allowed to go darker — which is where the
     sense of a curved face actually comes from here. */
  --sheen:linear-gradient(180deg, rgba(245,239,227,.085), rgba(0,0,0,.12));
  --sheen-lip:rgba(245,239,227,.14);
  --sheen-strong:linear-gradient(180deg, rgba(255,255,255,.22), rgba(0,0,0,.14));
  --sheen-lip-strong:rgba(255,255,255,.30);
  /* Warm and low-contrast: a scrollbar is a control, not content. The dark
     --sb-* values are NOT here — they live on `html:has(> body.pf-dark)` near
     the top of this file, because scrollbar-color is resolved on <html> and a
     token set on <body> arrives too late. See the note there. */
  color:var(--ink);
  /* ---- depth, ours (2026-08-06) --------------------------------------------
     The reference site builds depth from FOUR independent layers, not one
     gradient: near-black base · a 64px graph-paper grid at 2.4% · a large soft
     radial light · a tighter local glow aimed at the primary button.
     The STRUCTURE is worth taking; their motifs are not — copying a green brand
     glow and graph paper is how a site stops being its own.

     So the same four layers, built from things that are ours:
       · the texture is a GOBAN LATTICE, not graph paper. Intersections, at the
         board's own 30-unit pitch, dotted rather than ruled. This site is board
         games; the wallpaper should be a board.
       · the light is WARM, from --wood-base. They light the room with their
         brand colour; ours is lit by the material on the table. Teal is our
         accent and it is already doing work on the buttons — spending it on the
         wallpaper too would flatten the hierarchy.
     `fixed` on both, so the light belongs to the window and the lattice does
     not slide around behind scrolling content. */
  background-color:var(--bg);
  background-image:
    radial-gradient(circle at 50% -10%, rgba(201,160,106,.13), rgba(0,0,0,0) 620px),
    radial-gradient(rgba(245,239,227,.045) 1px, rgba(0,0,0,0) 1px);
  background-size:auto, 30px 30px;
  background-position:center top, center top;
  background-attachment:fixed, fixed;
  background-repeat:no-repeat, repeat;
}
/* Layer 4: the local spotlight. Not decoration — it is aimed at whatever the
   page most wants you to press, so it goes on that block, not on the page.
   Opt-in with class="pf-lit" and set --lit-x/--lit-y to point it. */
.pf-lit{
  position:relative;
  background-image:radial-gradient(
    circle at var(--lit-x,50%) var(--lit-y,0%),
    rgba(88,201,181,.13), rgba(0,0,0,0) var(--lit-r,300px));
}
.pf-landing{--maxw:1120px}
/* The handful of places the shared sheet hardcodes white or assumes a light accent. */
.pf-dark .site-header{background:rgba(30,27,23,.9)}
.pf-dark .site-footer{background:#1a1714; color:var(--muted)}
.pf-dark .btn.primary{color:var(--accent-onink)}
.pf-dark .btn.primary:active{color:var(--accent-onink)}
/* Secondary buttons: a translucent lift on the one surface, no border colour.
   background-COLOR + background-image, not the shorthand: this rule is (0,3,0)
   and would otherwise beat `.btn`'s sheen with an implied `none`, leaving every
   secondary button on the dark site flat while the light site had material. */
.pf-dark .btn:not(.primary){background-color:rgba(245,239,227,.07); background-image:var(--sheen); border-color:rgba(245,239,227,.12)}
.pf-dark .btn:not(.primary):hover{background-color:rgba(245,239,227,.12); border-color:var(--accent)}
.pf-dark .btn.outline{background:rgba(245,239,227,.07); border-color:rgba(245,239,227,.12); color:var(--ink)}
.pf-dark .btn.outline:hover{background:rgba(245,239,227,.12); border-color:var(--accent); color:var(--ink)}

/* ---- dark shell: the article / guide / utility reading pages --------------
   Everything above the fold is token-driven and flips for free; these rules cover
   the few shell pieces that hardcoded a light-only colour, plus the board figures,
   which are white-background SVGs (byte-identical to the light site). Rather than
   regenerate ~25 diagrams, each figure is framed as a deliberate light inset on
   the dark page — the standard "diagram on a page" treatment — with a warm mat so
   the white board is bounded, not a hole. Repainting them cream via the palette
   system board-diagram.mjs already has is a clean follow-up if the white reads hot. */
.pf-doc{min-height:100vh}
/* --elev-*, not the legacy --shadow-* trio: sm and md are byte-identical, so
   anything painted with them is flat no matter which one it picks. */
.pf-doc .prose{box-shadow:var(--elev-2)}
/* Inline prose links already turn teal via the global a{color:--accent-ink}
   (that token is #7ad8c6 on dark); a .prose a rule here would also repaint the
   related-card and tile links, so it is deliberately omitted. */
.pf-doc .prose .note{background:#2f2822; border-left-color:var(--accent)}
.pf-doc .prose .cta a{color:var(--accent-onink)}
.pf-doc .path li::before{color:var(--accent-onink)}
.pf-doc .figure-board{background:var(--surface-tint); border:1px solid var(--line);
  border-radius:var(--radius-card); padding:14px 14px 4px; box-shadow:var(--elev-1)}
.pf-doc .figure-board svg{border-color:#d5e5e0; box-shadow:0 1px 3px rgba(0,0,0,.35)}
.pf-wrap{max-width:1120px; margin:0 auto; padding:0 24px}
.pf-landing .eyebrow{font-family:var(--font-display); font-weight:600; color:var(--accent-strong);
  letter-spacing:.06em; text-transform:uppercase; font-size:.8rem; background:none; border:0; padding:0; border-radius:0}

/* ---- sticky nav (page-agnostic: works on landing AND game pages) --------- */
/* Unique class names, unscoped from .pf-landing so any page can adopt it.
   Self-centers to var(--maxw) — 1120 inside .pf-landing, 980 on standard pages —
   so the nav column lines up with each page's content column. */
.site-header{position:sticky; top:0; z-index:20; background:rgba(255,255,255,.92);
  backdrop-filter:saturate(1.2) blur(6px); border-bottom:var(--site-header-border-size) solid var(--line)}
.site-nav{width:min(var(--maxw),100%); margin:0 auto; padding:0 var(--page-inline-pad);
  display:flex; align-items:center; justify-content:space-between; gap:14px; height:var(--site-nav-block-size)}
.site-brand{display:flex; align-items:center; gap:10px; font-family:var(--font-display);
  font-weight:700; font-size:1.25rem; text-decoration:none; color:var(--ink)}
.site-brand img{height:32px; width:auto; display:block}
.site-nav-links{display:flex; align-items:center; gap:24px; font-weight:600}
/* white-space:nowrap because "Big Two" is the first nav label with a space in
   it: the flex row is nowrap, but the TEXT inside an anchor still wrapped, and
   it broke into "Big / Two" stacked in a 31px-wide link (measured at 780px). */
.site-nav-links a{color:var(--ink); text-decoration:none; transition:color .18s; white-space:nowrap}
.site-nav-links a:hover{color:var(--accent-strong)}
.site-nav-links a.current{color:var(--accent-strong)}
.site-nav-cta{display:flex; align-items:center; gap:12px}
/* language switch = plain link to the other-language URL (separate-URL i18n) */
.lang-switch{
  font:inherit; font-weight:600; padding:8px 12px; border:1px solid var(--line-soft);
  border-radius:var(--radius-control); color:var(--ink); text-decoration:none;
  transition:border-color .18s,color .18s; white-space:nowrap;
}
.lang-switch:hover{border-color:var(--accent); color:var(--accent-strong)}
.lang-switch:focus-visible{outline:3px solid var(--accent-active); outline-offset:1px; border-color:var(--accent)}
@media(max-width:760px){.site-nav-links{display:none}}
/* Seven links since Big Two joined (2026-08-05), and at the bottom of the range
   where the nav is still shown they no longer fit: measured 17px of horizontal
   overflow at 780px. Tighten the gap instead of dropping a link — 8px off each
   of the six gaps buys back 48px, which clears the whole band. */
@media(max-width:900px){.site-nav-links{gap:16px}}
/* The landing page is the only one whose nav carries a Play button on top of the
   language select; between the links breakpoint and ~810px that overflowed the
   row and gave the page a horizontal scrollbar. The hero has three play buttons
   right below, so dropping this one in that band costs nothing. */
@media(max-width:860px){.pf-landing .site-nav-cta .btn.primary{display:none}}

/* ---- hero (headline + a real position from the puzzle bank) --------------
   No band colour and no divider: the hero sits on the same surface as the rest
   of the page, and separation comes from the zig-zag of art and copy plus the
   section pitch. The only lit objects above the fold are the board and one teal
   button. */
.pf-landing .hero{text-align:left; margin:0; padding:56px 0 64px}
.pf-landing .hero-inner{display:grid; gap:40px; align-items:center;
  grid-template-columns:minmax(0,1fr)}
/* Type contrast comes from the body being small and quiet, not from a bigger
   headline (chess.com: h2 36/800 against 14px body at 72% opacity). */
.pf-landing .hero h1{margin:0; font-size:clamp(2.1rem,5vw,3.3rem); font-weight:700; max-width:17ch;
  letter-spacing:-.02em; line-height:1.1}
.pf-landing .hero p{font-size:1rem; color:var(--muted); max-width:44ch; margin:16px 0 26px}
.pf-landing .hero-actions{display:flex; gap:10px; flex-wrap:wrap; align-items:center}
.pf-landing .hero-actions .btn.primary{font-size:1.05rem; padding:15px 26px}
.pf-landing .pill{display:inline-block; background:var(--accent-soft); color:var(--accent-strong);
  font-weight:700; font-family:var(--font-display); font-size:.8rem; padding:.35em .9em; border-radius:999px; margin-bottom:18px}
/* Two columns only once the copy column can still hold the three play buttons
   on one line (checked at 1040px: ~500px of copy against ~450px of buttons). */
@media(min-width:1040px){
  .pf-landing .hero{padding:72px 0 80px}
  .pf-landing .hero-inner{grid-template-columns:minmax(0,1fr) minmax(0,440px); gap:56px}
  .pf-landing .hero-plate{justify-self:end}
}
@media(min-width:1200px){
  .pf-landing .hero-inner{grid-template-columns:minmax(0,1fr) minmax(0,460px)}
}

/* ---- hero puzzle plate ---------------------------------------------------
   Two real positions (one per bank), switched by radio inputs so the tabs work
   with the keyboard and need no JavaScript. The inputs are sr-only (absolutely
   positioned), so they add no grid tracks; every state selector below reads
   them with ~, which is why they must stay the first children of the plate. */
.pf-landing .hero-plate{background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-card);
  box-shadow:var(--elev-3); padding:16px 16px 18px; display:grid; gap:14px;
  width:100%; max-width:460px; justify-self:center}
.pf-landing .plate-head{display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap}
.pf-landing .plate-label{font-family:var(--font-display); font-weight:600; font-size:.72rem;
  letter-spacing:.08em; text-transform:uppercase; color:var(--accent-strong)}
.pf-landing .plate-tabs{display:inline-flex; gap:4px; padding:3px; background:var(--surface-tint);
  border:1px solid var(--line); border-radius:999px}
.pf-landing .plate-tabs label{font-family:var(--font-display); font-weight:600; font-size:.85rem;
  padding:5px 13px; border-radius:999px; color:var(--muted); cursor:pointer; white-space:nowrap;
  transition:background .18s, color .18s}
.pf-landing .plate-tabs label:hover{color:var(--ink)}
.pf-landing .plate-tabs label b{font-weight:700; font-variant-numeric:tabular-nums}
#pf-board-gomoku:checked ~ .plate-head label[for="pf-board-gomoku"],
#pf-board-xiangqi:checked ~ .plate-head label[for="pf-board-xiangqi"]{background:var(--accent); color:var(--ink)}
#pf-board-gomoku:focus-visible ~ .plate-head label[for="pf-board-gomoku"],
#pf-board-xiangqi:focus-visible ~ .plate-head label[for="pf-board-xiangqi"]{outline:3px solid var(--accent-active); outline-offset:2px}
.pf-landing .plate-board{display:none; margin:0}
#pf-board-gomoku:checked ~ .plate-board[data-bank="gomoku"],
#pf-board-xiangqi:checked ~ .plate-board[data-bank="xiangqi"]{display:grid; gap:12px}
/* Fixed art height so switching banks never reflows the page: the two boards
   have different aspect ratios (15x15 vs 9x10) and are centred, not stretched. */
/* The board is a link as well as a picture: the whole position is the click
   target, with the ring standing in for a hover cursor hint. It carries
   aria-hidden + tabindex="-1" in the markup — the prompt below states the same
   position in prose and the "solve" link is the keyboard path, so this stays a
   mouse shortcut instead of a duplicate tab stop. */
.pf-landing .plate-art{display:flex; align-items:center; justify-content:center;
  height:clamp(272px,33vw,400px); border-radius:10px; transition:box-shadow .18s}
.pf-landing a.plate-art:hover{box-shadow:0 0 0 3px var(--accent)}
.pf-landing .plate-art svg{height:100%; width:auto; max-width:100%; display:block}
.pf-landing .plate-board figcaption{display:grid; gap:6px}
.pf-landing .plate-prompt{margin:0; font-family:var(--font-display); font-weight:600;
  font-size:1.02rem; line-height:1.4; color:var(--ink)}
.pf-landing .plate-go{font-family:var(--font-display); font-weight:700; font-size:.95rem;
  color:var(--accent-strong); text-decoration:none}
.pf-landing .plate-go:hover{text-decoration:underline}

/* ---- sections ----------------------------------------------------------- */
.pf-landing section{padding:56px 0}
.pf-landing .sec-head{max-width:640px; margin-bottom:36px}
.pf-landing .sec-head h2{font-size:clamp(1.6rem,3.5vw,2.2rem); font-weight:700; margin-top:8px;
  line-height:1.14; letter-spacing:-.015em}
.pf-landing .sec-head p{color:var(--muted); margin-top:10px; font-size:1rem}

/* ---- one section per game, art and copy alternating sides ----------------
   Replaces the three-up card grid: each game gets its own headline, one
   sentence, one honest capability line and exactly one button — so the page
   reads as three offers in sequence instead of three cards competing at once.
   .flip swaps the art to the right on wide screens; the source order stays
   art-then-copy so a narrow screen always shows the board before the words. */
.pf-landing .game-rows{display:grid; gap:56px}
.pf-landing .game-row{display:grid; gap:26px; align-items:center; grid-template-columns:minmax(0,1fr)}
.pf-landing .game-row-art{display:block; border:1px solid var(--line); border-radius:14px;
  overflow:hidden; background:var(--surface-tint); line-height:0;
  transition:border-color .18s, transform .18s}
.pf-landing .game-row-art:hover{border-color:var(--accent); transform:translateY(-3px)}
.pf-landing .game-row-art img{width:100%; height:auto; display:block}
.pf-landing .game-row h2{font-size:clamp(1.7rem,3.6vw,2.3rem); font-weight:700; margin:0;
  line-height:1.12; letter-spacing:-.015em}
.pf-landing .game-row p{color:var(--muted); font-size:1rem; margin:12px 0 0; max-width:46ch}
.pf-landing .game-row .modes{display:block; margin:14px 0 22px; font-size:.85rem; color:var(--muted);
  font-family:var(--font-display); font-weight:600; letter-spacing:.01em}
.pf-landing .game-row .btn.primary{font-size:1rem; padding:13px 24px}
@media(min-width:860px){
  .pf-landing .game-rows{gap:72px}
  .pf-landing .game-row{grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:56px}
  .pf-landing .game-row.flip .game-row-art{order:2}
}

/* ---- puzzle banks ------------------------------------------------------- */
/* Every number here is generated from the banks at build time, so the panels
   cannot drift from what the pages actually hold. */
.pf-landing .banks{display:grid; grid-template-columns:repeat(2,1fr); gap:20px}
@media(max-width:720px){.pf-landing .banks{grid-template-columns:1fr}}
.pf-landing .bank{display:grid; gap:6px; padding:24px 24px 22px; background:var(--surface);
  border:1px solid var(--line); border-radius:14px; text-decoration:none; color:inherit;
  transition:border-color .18s, transform .18s}
.pf-landing .bank:hover{border-color:var(--accent); transform:translateY(-3px)}
.pf-landing .bank-count{display:flex; align-items:baseline; gap:8px}
.pf-landing .bank-count b{font-family:var(--font-display); font-weight:700; font-size:2.4rem;
  line-height:1; color:var(--ink); font-variant-numeric:tabular-nums}
.pf-landing .bank-count span{font-family:var(--font-display); font-weight:600; font-size:.78rem;
  letter-spacing:.08em; text-transform:uppercase; color:var(--accent-strong)}
.pf-landing .bank h3{font-size:1.18rem; font-weight:600; margin-top:4px}
.pf-landing .bank p{color:var(--muted); font-size:.94rem; margin:0}
.pf-landing .bank .go{font-family:var(--font-display); font-weight:700; font-size:.95rem;
  color:var(--accent-strong); margin-top:8px}

/* The four icon+label "why us" cards are gone (2026-07-26): abstract feature
   icons were the one thing on this page that showed no board. The same facts now
   live where they are specific — per game in .modes, and in the closing band. */

/* ---- learn tiles on the dark surface ------------------------------------- */
.pf-landing .tile{background:var(--surface); border-color:var(--line)}
.pf-landing .tile:hover{border-color:var(--accent)}
.pf-landing .tile strong{color:var(--ink)}

/* ---- closing band --------------------------------------------------------
   Was a full-bleed teal panel; on the dark page that would have been a second
   loud surface competing with the boards. It is now the same surface as
   everything else, and the teal is spent only on the button — the page's accent
   means "this is the action", nowhere else. */
/* The band carries its own padding, so the section only needs to close the gap. */
.pf-landing #start{padding:24px 0 56px}
.pf-landing .cta{background:var(--surface); border:1px solid var(--line); color:var(--ink);
  border-radius:20px; text-align:center; padding:44px 24px; margin:0}
.pf-landing .cta h2{font-size:clamp(1.7rem,4vw,2.3rem); font-weight:700; line-height:1.14; letter-spacing:-.015em}
.pf-landing .cta p{color:var(--muted); margin:12px auto 26px; max-width:46ch; font-size:1rem}
.pf-landing .cta .btn.primary{font-size:1.05rem; padding:15px 30px}
.pf-landing .cta-diffs{display:flex; gap:22px; justify-content:center; flex-wrap:wrap; margin-top:24px;
  font-weight:600; font-family:var(--font-display); font-size:.9rem; color:var(--muted)}
.pf-landing .cta-diffs > span{display:inline-flex; align-items:center; gap:7px}
.pf-landing .cta-diffs > span::before{content:"✓"; color:var(--accent-strong); font-weight:700}

/* ---- multi-column footer (page-agnostic via .site-footer) ---------------- */
/* Opt-in with <footer class="site-footer">; keyed off .site-footer + .foot so it
   never touches the plain centered <footer> on article/guide pages. Inner columns
   self-center to var(--maxw) to match each page's content column. */
.site-footer{background:#fff; border-top:1px solid var(--line); padding:48px 0 28px; margin-top:44px; text-align:left; color:var(--muted)}
.site-footer .foot{width:min(var(--maxw),100%); margin:0 auto; padding:0 20px;
  display:grid; grid-template-columns:2fr 1fr 1fr 1fr; gap:32px}
@media(max-width:720px){.site-footer .foot{grid-template-columns:1fr 1fr}}
.site-footer .foot h4{font-family:var(--font-display); font-size:.95rem; margin-bottom:14px; color:var(--ink)}
.site-footer .foot a{display:block; color:var(--muted); padding:5px 0; font-size:.92rem; text-decoration:none; transition:color .18s}
.site-footer .foot a:hover{color:var(--accent-strong)}
.site-footer .foot-brand p{max-width:32ch; font-size:.92rem; margin-top:12px}
.site-footer .foot-bottom{width:min(var(--maxw),100%); margin:36px auto 0; padding:20px 20px 0;
  border-top:1px solid var(--line); color:var(--muted); font-size:.85rem;
  display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px}

/* ---- Quick-start entry (game pages) ----------------------------------------
   One-line pill linking first-time players to the matching how-to-play guide.
   Additive component: only pages that carry the markup are affected. Lives in
   the game-side/hero title block so it never enters the board size formula. */
.quick-start{
  display:inline-flex; align-items:center; gap:6px;
  margin-top:12px; padding:7px 14px;
  background:var(--accent-soft); border:1px solid var(--line-soft); border-radius:var(--radius-pill);
  font-size:14px; font-weight:700; color:var(--accent-strong);
  text-decoration:none; line-height:1.2;
}
.quick-start:hover{border-color:var(--accent); color:var(--accent-ink); text-decoration:none}
.quick-start:focus-visible{outline:3px solid var(--accent-active); outline-offset:2px}

/* The homepage puzzle showcase that used to live here was replaced by the hero
   plate (2026-07-26): the same real position now opens the page instead of
   sitting two screens down. Styles for it are in the .pf-landing block above. */

/* =============================================================================
   MATERIAL LAYER (2026-08-06) — the primitives the site was missing.

   Why this block exists: the game pages were assembled from whatever control
   was nearest to hand, so one sidebar could hold a <select>, a pill group,
   another <select>, a full-width button and three bordered cards — five shapes
   for five controls, with nothing saying which one mattered. That is what reads
   as "thin and messy", not a shortage of pixels.

   Everything here is ADDITIVE: new class names only, so no existing page moves
   until it opts in. Uses --elev-* and the gold/danger families, never the old
   flat --shadow-* tokens.
   ========================================================================== */

/* Every component below sets `display`, and a class selector beats the UA's
   `[hidden]{display:none}`. Without this line any of them carrying the hidden
   attribute stays on screen — which is how the settings dialog shipped open and
   undismissable. Declared once, before the components, so a new component in
   this block is covered by default. */
.pf-row[hidden],.pf-actionbar[hidden],.pf-action[hidden],.pf-player[hidden],
.pf-tile[hidden],.pf-sheet[hidden],.pf-stats[hidden],.pf-stat[hidden],
.pf-switch[hidden]{display:none}

/* ---- pf-row: the one settings shape ---------------------------------------
   icon · title (+ optional description) · control pinned right. Every option on
   a page is this row, whether it holds a select, a switch or a link. */
.pf-row{
  display:flex; align-items:center; gap:14px;
  padding:14px 16px; border-radius:var(--radius-control);
  background:var(--surface); border:1px solid var(--line-soft);
  box-shadow:var(--elev-1);
}
.pf-row + .pf-row{margin-top:8px}
.pf-row-icon{
  flex:0 0 34px; block-size:34px; display:grid; place-items:center;
  border-radius:calc(var(--radius-control) - 4px); background:var(--accent-soft); color:var(--accent-strong);
}
.pf-row-icon svg{inline-size:19px; block-size:19px}
.pf-row-body{flex:1 1 auto; min-inline-size:0}
/* display:block is load-bearing — these are usually <span>s so that the whole
   row can be a <button>, and a span child of a flex item is NOT blockified by
   the parent. Without it the title and description run together on one line. */
.pf-row-title{
  display:block; margin:0; font-family:var(--font-display); font-weight:600; font-size:15px;
  color:var(--ink); line-height:1.3;
}
.pf-row-desc{display:block; margin:3px 0 0; font-size:13px; color:var(--muted); line-height:1.45}
.pf-row-control{flex:0 0 auto; display:inline-flex; align-items:center; gap:8px}
.pf-row.is-button{
  cursor:pointer; text-align:start; inline-size:100%; font:inherit;
  transition:border-color var(--t-tap) var(--ease-out), box-shadow var(--t-tap) var(--ease-out);
}
.pf-row.is-button:hover{border-color:var(--accent); box-shadow:var(--elev-2)}
.pf-row.is-button:focus-visible{outline:3px solid var(--accent-active); outline-offset:2px}

/* ---- pf-switch: the one on/off shape -------------------------------------- */
.pf-switch{
  inline-size:44px; block-size:26px; border-radius:999px; border:0; padding:0;
  background:var(--line); cursor:pointer; position:relative;
  transition:background var(--t-tap) linear;
}
.pf-switch::after{
  content:""; position:absolute; inset-block-start:3px; inset-inline-start:3px;
  inline-size:20px; block-size:20px; border-radius:50%;
  background:var(--surface); box-shadow:var(--elev-1);
  transition:transform var(--t-tap) var(--ease-out);
}
.pf-switch[aria-checked="true"]{background:var(--accent)}
.pf-switch[aria-checked="true"]::after{transform:translateX(18px)}
.pf-switch:focus-visible{outline:3px solid var(--accent-active); outline-offset:2px}

/* ---- pf-actionbar: the one in-game control shape --------------------------
   Icon above label, equal columns, one bar. Replaces the habit of scattering
   game verbs across cards at different sizes. */
.pf-actionbar{
  display:grid; grid-auto-flow:column; grid-auto-columns:1fr; gap:2px;
  padding:6px; border-radius:var(--radius-card);
  background:var(--surface); border:1px solid var(--line-soft); box-shadow:var(--elev-2);
}
.pf-action{
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:5px;
  min-block-size:56px; padding:8px 4px; border:0; border-radius:calc(var(--radius-card) - 6px);
  background:transparent; color:var(--ink); cursor:pointer;
  font:inherit; font-family:var(--font-display); font-size:12px; font-weight:600;
  transition:background var(--t-tap) linear, color var(--t-tap) linear;
}
.pf-action svg{inline-size:20px; block-size:20px}
/* --accent-ink, not --accent-strong: on white the strong step measures 4.03:1,
   under the 4.5 AA floor for text. The strong step is fine for the icons (3:1
   graphical-object threshold, measured 3.69) but not for the labels. */
.pf-action:hover:not(:disabled){background:var(--surface-tint); color:var(--accent-ink)}
.pf-action:disabled{opacity:.35; cursor:default}
.pf-action:focus-visible{outline:3px solid var(--accent-active); outline-offset:-2px}
.pf-action.danger:hover:not(:disabled){background:var(--danger-soft); color:var(--danger-ink)}

/* ---- pf-player: identity beside the board ---------------------------------
   The single biggest source of "the page is empty": our board floats with
   nothing on either side of it. */
.pf-player{
  display:flex; align-items:center; gap:11px;
  padding:11px 14px; border-radius:var(--radius-control);
  background:var(--surface); border:1px solid var(--line-soft); box-shadow:var(--elev-1);
  /* The turn handoff is a state change you must not miss, so it gets --t-move,
     not --t-tap: slow enough to catch the eye when it happens off to the side. */
  transition:border-color var(--t-move) var(--ease-out), box-shadow var(--t-move) var(--ease-out);
}
.pf-player.is-turn{border-color:var(--accent); box-shadow:var(--elev-2)}
.pf-player-avatar{
  flex:0 0 38px; block-size:38px; border-radius:10px; display:grid; place-items:center;
  background:var(--surface-tint); border:1px solid var(--line); font-size:19px;
}
.pf-player-body{flex:1 1 auto; min-inline-size:0}
.pf-player-name{
  margin:0; font-family:var(--font-display); font-weight:600; font-size:14px;
  color:var(--ink); line-height:1.25; display:flex; align-items:center; gap:6px;
}
.pf-player-meta{display:block; margin:2px 0 0; font-size:12px; color:var(--muted); font-variant-numeric:tabular-nums}
.pf-player-count{
  flex:0 0 auto; font-family:var(--font-display); font-weight:700; font-size:20px;
  color:var(--muted); font-variant-numeric:tabular-nums;
}
.pf-player.is-turn .pf-player-count{color:var(--accent-ink)}

/* ---- pf-tile: icon card for modes / variants ------------------------------ */
.pf-tile{
  display:flex; flex-direction:column; align-items:center; gap:9px;
  padding:16px 12px; border-radius:var(--radius-card); text-align:center;
  background:var(--surface); border:1px solid var(--line-soft); box-shadow:var(--elev-1);
  color:var(--ink); text-decoration:none; cursor:pointer; font:inherit;
  transition:border-color var(--t-tap) var(--ease-out), box-shadow var(--t-tap) var(--ease-out),
             transform var(--t-tap) var(--ease-out);
}
.pf-tile:hover{border-color:var(--accent); box-shadow:var(--elev-3); transform:translateY(-2px)}
.pf-tile.is-active{border-color:var(--accent); background:var(--accent-soft)}
.pf-tile:focus-visible{outline:3px solid var(--accent-active); outline-offset:2px}
.pf-tile-icon{color:var(--accent-strong)}
.pf-tile-icon svg{inline-size:30px; block-size:30px}
.pf-tile-name{font-family:var(--font-display); font-weight:600; font-size:14px; line-height:1.25}
.pf-tile-note{font-size:12px; color:var(--muted); line-height:1.35}
.pf-tile-live{font-size:12px; color:var(--accent-ink); font-weight:700; font-variant-numeric:tabular-nums}

/* ---- pf-sheet: result / setup dialog with a real action hierarchy ---------
   Four ranks, and the markup says which is which: accent → outline → gold →
   quiet. The old end-of-game state was one line of text and one button, so
   there was nowhere to put "change difficulty" or "back to games". */
.pf-scrim{
  position:fixed; inset:0; z-index:1000; padding:20px;
  display:grid; place-items:center;
  background:rgba(20,17,14,.62); backdrop-filter:blur(3px);
}
/* Load-bearing. The UA's `[hidden]{display:none}` is a type-less rule and loses
   to the class selector above, so a scrim with the hidden attribute set stays
   fully visible — the dialog opens on page load and nothing can dismiss it.
   Every display-setting component in this file needs its own [hidden] rule. */
.pf-scrim[hidden]{display:none}
.pf-sheet{
  inline-size:min(100%,400px); padding:24px; border-radius:var(--radius-card); text-align:center;
  background:var(--surface); border:1px solid var(--line-soft); box-shadow:var(--elev-4);
  /* One-shot entrance, so @keyframes rather than transition (playbook 決策 I).
     Rises a little instead of scaling: the sheet arrives, it does not pop. */
  animation:pf-sheet-in var(--t-enter) var(--ease-out) both;
}
@keyframes pf-sheet-in{from{opacity:0; transform:translateY(14px)} to{opacity:1; transform:none}}
.pf-scrim{animation:pf-fade-in var(--t-clear) linear both}
@keyframes pf-fade-in{from{opacity:0} to{opacity:1}}
.pf-sheet-icon{
  inline-size:52px; block-size:52px; margin:0 auto 12px; border-radius:var(--radius-control);
  display:grid; place-items:center; background:var(--accent-soft); color:var(--accent-strong);
}
.pf-sheet-icon svg{inline-size:27px; block-size:27px}
.pf-sheet h2{margin:0 0 4px; font-size:21px}
.pf-sheet-sub{margin:0 0 18px; font-size:14px; color:var(--muted)}
.pf-sheet-actions{display:flex; flex-direction:column; gap:9px}
/* The mirror of .local-only (each game page defines that one). A result sheet
   has to offer different things depending on who you are playing: locally
   "play again / change opponent", online "rematch / close" — because online a
   rematch is an OFFER to another person, not a button that restarts anything.
   One sheet, two action sets, so there is never a second sheet stacked behind
   the first (安咕 2026-08-08 回報：線上結束會出現兩張一模一樣的卡). */
.online-only{display:none}
body.online-mode .online-only{display:flex}
.pf-sheet-actions .btn{inline-size:100%; justify-content:center; min-block-size:46px; font-size:16px;
  border-radius:var(--radius-control)}
.btn.gold{background-color:var(--gold); background-image:var(--sheen-strong); border-color:var(--gold); color:#2c2c2c}
.btn.gold:hover{background-color:var(--gold-hover); border-color:var(--gold-hover)}
.btn.quiet{background:transparent; border-color:transparent; color:var(--muted); min-block-size:40px; box-shadow:none}
.btn.quiet:hover{background:var(--surface-tint); color:var(--ink); box-shadow:none}
/* The dark theme paints every non-primary button with a translucent lift via
   `.pf-dark .btn:not(.primary)` (0,3,0). These two variants are not "secondary"
   — gold is its own rank and quiet must stay invisible — so they need to match
   that specificity to win. */
.pf-dark .btn.gold:not(.primary){background-color:var(--gold); background-image:var(--sheen-strong); border-color:var(--gold); color:#231a09}
.pf-dark .btn.gold:not(.primary):hover{background-color:var(--gold-hover); border-color:var(--gold-hover)}
.pf-dark .btn.quiet:not(.primary){background:transparent; border-color:transparent; color:var(--muted); box-shadow:none}
.pf-dark .btn.quiet:not(.primary):hover{background:rgba(245,239,227,.07); border-color:transparent; color:var(--ink); box-shadow:none}

/* ---- pf-stat: post-game numbers ------------------------------------------- */
.pf-stats{display:grid; grid-template-columns:repeat(auto-fit,minmax(94px,1fr)); gap:8px; margin:16px 0 4px}
.pf-stat{padding:11px 8px; border-radius:calc(var(--radius-control) - 2px); background:var(--surface-tint); border:1px solid var(--line-soft)}
.pf-stat-val{
  margin:0; font-family:var(--font-display); font-weight:700; font-size:21px;
  color:var(--ink); font-variant-numeric:tabular-nums; line-height:1.15;
}
.pf-stat-lbl{margin:2px 0 0; font-size:11px; color:var(--muted); letter-spacing:.02em}

/* ---- focus mode / fullscreen (paired with shared/board-focus.js) ----------
   安咕 2026-08-08: every game page gets the button the card table already had,
   in the board's top-right corner. Extracted from bigtwo_src, which is where
   the traps below were originally paid for. */
.pf-focus-btn{
  /* The board's top-right corner. Every .board-wrap / .table-wrap on the four
     pages is a positioning context, so one rule places it on all of them. */
  position:absolute; inset-block-start:10px; inset-inline-end:10px; z-index:7;
  inline-size:38px; block-size:38px; padding:0; border-radius:50%;
  display:grid; place-items:center; cursor:pointer;
  /* Dark translucent on cream-lit wood: the one pairing that stays legible on
     every board skin without the button having to know which skin it is on. */
  background:rgba(20,17,14,.34); border:1px solid rgba(245,239,227,.24);
  color:#f5efe3; box-shadow:var(--elev-1);
  transition:background var(--t-tap) linear, border-color var(--t-tap) linear;
}
.pf-focus-btn svg{inline-size:19px; block-size:19px}
.pf-focus-btn svg[hidden]{display:none}
.pf-focus-btn:focus-visible{outline:3px solid var(--accent-active); outline-offset:2px}
@media (hover:hover) and (pointer:fine){
  .pf-focus-btn:hover{background:rgba(20,17,14,.58); border-color:rgba(245,239,227,.4)}
}

/* Focus mode: the site chrome steps aside so the board gets the screen. Used on
   its own where the Fullscreen API is unavailable (iPhone Safari), and alongside
   it everywhere else — inside a fullscreened .game-stage the header is gone
   anyway, so its height must stop being subtracted from the board's size. */
body.pf-focus .site-header,
body.pf-focus .site-footer,
body.pf-focus .info-panels,
body.pf-focus .game-guide{display:none}
body.pf-focus{--site-header-block-size:0px}
/* ⚠️ Below the breakpoint the board is NOT the first thing on the page on every
   game, and focus mode also locks scrolling — hiding only the site chrome once
   left the table pushed below the fold with no way to reach it (安咕 2026-08-05,
   "被鎖死在不是正確的位置"). Focus mode means the board and nothing else, so
   everything around it goes too and the stage centres what is left. Above the
   breakpoint the board already sits at the top of its own column. */
@media (max-width:883px){
  body.pf-focus .game-title,
  body.pf-focus .side,
  body.pf-focus .settings-card,
  body.pf-focus .match-bar,
  body.pf-focus .board-hint{display:none}
  body.pf-focus .game-stage{
    min-block-size:100dvh;
    display:flex; align-items:center; justify-content:center;
    padding-block:0;
  }
  /* ⚠️ Zeroing the stage's own padding is not enough: .wrap keeps its
     padding-block, so a stage that is exactly 100dvh tall STARTS 28px down and
     therefore ends 28px past the bottom — and everything centred inside it goes
     with it. Measured on a 390x640 phone: the board fitted, the action bar's
     bottom edge sat 8px off screen, and focus mode had already locked scrolling.
     The page chrome is hidden in this mode, so its spacing has no job left. */
  body.pf-focus .wrap{padding-block:0}
  body.pf-focus .game-layout{inline-size:100%}
  /* ⚠️ Focus mode locks scrolling, so anything the board pushes below the fold
     is not "below the fold", it is GONE. The board is sized from WIDTH, which
     is fine while the page scrolls and fatal once it does not: measured on a
     390x640 phone, Banqi's portrait board came out 350x660 — 124px past the
     bottom of a viewport you can no longer scroll (安咕 2026-08-08, 圖1).
     So inside focus mode the width is additionally capped by the height that is
     actually left: the viewport, minus the action bar we deliberately keep (a
     board game needs Undo and Rotate in reach) and the gaps around it, times
     the board's own aspect. Each page declares its aspect below; the default of
     1 is the square case and is never wrong by more than the page's own shape. */
  body.pf-focus .board-wrap{
    max-inline-size:min(
      var(--game-single-column-max, 560px),
      calc((100dvh - 128px) * var(--pf-focus-board-aspect, 1))
    );
  }
}
/* Nothing left to scroll to, so stop the page scrolling at all. On iOS a stray
   swipe is what drags Safari's bars back into view and undoes the whole point of
   the button, and the rubber-band bounce does it even when the page already
   fits. overscroll-behavior alone stopped working on iOS years ago, hence the
   overflow lock here AND the touchmove guard in board-focus.js. */
html.pf-locked, body.pf-focus{
  overflow:hidden; overscroll-behavior:none;
  block-size:100%; touch-action:manipulation;
}
/* A notched phone held sideways puts the notch beside the board, not above it,
   so the insets have to be honoured once viewport-fit=cover is on. */
body.pf-focus .game-stage{
  padding-inline:env(safe-area-inset-left) env(safe-area-inset-right);
}
.game-stage:fullscreen{
  background:var(--bg); display:flex; align-items:center; justify-content:center;
  inline-size:100%;
}

/* ---- function tints -------------------------------------------------------
   The icon well is the only place colour is spent on a row, a tile or a sheet,
   so the hue carries the meaning on its own: warm/red for how hard, blue for
   time, violet for sound, gold for anything social or rewarding. Applies to
   .pf-row-icon, .pf-tile-icon and .pf-sheet-icon alike.

   LAST in the component block on purpose. These are single-class selectors, the
   same weight as the three icon wells they modify, so order is the only thing
   deciding the winner. Sitting up beside .pf-row-icon they only ever reached
   the settings rows — .pf-tile-icon and .pf-sheet-icon are declared further
   down and quietly took every tinted icon back to accent teal. Measured: all
   four tints on .pf-sheet-icon resolved to the same rgb(122,216,198), which is
   why win / lose / draw shared one colour on the result sheet. */
.tint-teal{background:var(--tint-teal-soft); color:var(--tint-teal)}
.tint-gold{background:var(--tint-gold-soft); color:var(--tint-gold)}
.tint-coral{background:var(--tint-coral-soft); color:var(--tint-coral)}
.tint-violet{background:var(--tint-violet-soft); color:var(--tint-violet)}
.tint-sky{background:var(--tint-sky-soft); color:var(--tint-sky)}
.pf-tile-icon.tint-teal,.pf-tile-icon.tint-gold,.pf-tile-icon.tint-coral,
.pf-tile-icon.tint-violet,.pf-tile-icon.tint-sky{
  /* Tiles show the icon on the card itself, not in a well — keep the hue,
     drop the chip so the card face stays one surface. */
  background:transparent;
}

/* ---- board motion (paired with shared/board-motion.js) ---------------------
   The transition lives on the OUTER group, which only ever holds translate;
   the inner group is left free for form changes so the two never fight
   (playbook 決策 C). */
.pf-pc{transition:transform var(--t-move) var(--ease-out)}
.pf-pc-in{transform-origin:center; transform-box:fill-box}
/* One-shots. Both are removed by board-motion.js once they finish — never
   fill-mode:both, which would pin the transform and kill later hovers. */
.pf-pc-enter{animation:pf-pc-enter var(--t-move) var(--ease-out)}
@keyframes pf-pc-enter{from{transform:scale(.55); opacity:0} to{transform:scale(1); opacity:1}}
.pf-pc-leave{animation:pf-pc-leave var(--t-clear) linear forwards}
@keyframes pf-pc-leave{from{transform:scale(1); opacity:1} to{transform:scale(.7); opacity:0}}
.pf-winline{transition:stroke-dashoffset var(--t-enter) var(--ease-out)}

/* ---- reduced motion (playbook 決策 E) --------------------------------------
   Collapsing the tokens to 0 turns off every motion in the material layer and
   in any game that adopts them, from one place. State still changes — colours,
   borders and positions all land instantly — nothing becomes unusable.
   Scoped to :root so it also reaches the games' own stylesheets once they
   switch from their local --bt-t-* to these. */
@media (prefers-reduced-motion: reduce){
  :root{--t-tap:0ms; --t-move:0ms; --t-clear:0ms; --t-enter:0ms}
  .pf-sheet,.pf-scrim{animation:none}
  .pf-tile:hover{transform:none}
}
