/* ===========================================================================
   docs/assets/site.css — the one stylesheet for the whole site.

   WHY THIS FILE EXISTS
   Seven pages (home, learn, cards, rules, builder, play, my collection) are
   written by different hands at different times. Without a single source of
   colour, type and spacing they drift into seven products. Everything below is
   a CONTRACT:
   other pages code against these names, so renaming one is a breaking change.

   WHY THE PALETTE LOOKS LIKE THIS
   The dark values are lifted verbatim from docs/play/index.html, which was
   tuned against real card art on a dark table. The site should feel like the
   same product as the board, so the board's palette is the canonical one and
   light is the derived counterpart — not the other way round.

   THE PUBLIC API — the only names another page should rely on
   ---------------------------------------------------------------------------
   TOKENS (on :root)
     colour    --bg --panel --panel-2 --line --ink --dim --accent --good --warn --bad
     tinted    --accent-soft --accent-line --accent-ink --good-soft --warn-soft --bad-soft
     card      --c-red --c-green --c-blue --c-purple --c-black --c-yellow — the game's six
               colours (CR 2-3-3). Identity, not intent: never let one stand in for
               --good/--warn/--bad. Applied via the matching .c-red….c-yellow class.
               --cb-deepen — how much black §6b's PRINTED card back folds into one
               of those six. 0 in light, 32% in dark; read by .card-back alone.
     surface   --hover --focus --shadow-1 --shadow-2
     radius    --radius --radius-sm --radius-lg --radius-pill
     type      --font-sans --font-mono --fs-2xs..--fs-3xl
     space     --sp-1..--sp-8
     layout    --site-max --measure --card-min

   LAYOUT
     .site-nav      the shared header (rendered by site.js mountNav)
     .site-main     the page's content column; centred, capped at --site-max
     .skip-link     keyboard escape hatch to #main, visible only on focus

   BLOCKS
     .panel         a bordered surface. Padded by default; .panel.flush is not,
                    for the .panel-head / .panel-body split.
     .card-grid     responsive grid of card art, 360px to 2560px
     .card-tile     one card in that grid; art keeps 63:88 and is never squashed
     .card-back     the CSS-drawn card back — what a card-shaped box shows when
                    there is no image, coloured via .c-red….c-yellow. Takes three
                    child spans, __frame __ring __mark, in that order; §6b has the
                    markup. docs/art/ is gitignored, so THIS is the normal
                    rendering on any host that is not the one that built the
                    site, not an error state.
     .stat-grid     a row of .stat number tiles
     .notice        a boxed statement; .notice.good/.warn/.bad
     .bar           a coverage meter; .bar > .bar-fill
     .harbor        the home page's full-bleed sea band; .harbor__inner is the
                    ordinary content column inside it, and .seascape is the
                    drawn horizon behind it. §11.
     .hand          the home page's doors, as a hand of cards. One .hand-door is
                    a .card-back with a nameplate struck into it. §11.

   CONTROLS
     .btn           button or link; .btn.primary .btn.ghost .btn.sm
     .input         input, select, textarea
     .chip          a filter toggle; reads [aria-pressed="true"]
     .pill          a small status label; .pill.good .pill.warn .pill.bad

   TEXT / UTILITY
     .mono .dim .empty .eyebrow .lede .meta .kbd .tag
     .cluster .stack .scroll-x .sr-only

   RULES THIS FILE OBEYS
   1. Nothing hardcodes a colour. Every value is a token, so the light theme is
      a token swap and not a second stylesheet.
   2. Both themes, always. @media (prefers-color-scheme: dark) is the default
      signal; :root[data-theme="dark"|"light"] overrides it and WINS, because
      an explicit choice beats an inferred one. Attribute selectors outrank the
      bare :root inside the media query on specificity, so order cannot break it.
   3. No horizontal page scroll at any width. Wide things scroll inside their
      own box (.scroll-x, .panel-body, pre, table) — the page never does.
   4. Focus is visible. A keyboard user can always see where they are.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   1. TOKENS
   --------------------------------------------------------------------------- */

:root {
  /* Tells the UA to render form controls and scrollbars for the right theme.
     Overridden per explicit theme further down. */
  color-scheme: light dark;

  /* --- type ---------------------------------------------------------------
     The scale is in rem so a reader who raises their browser's base font size
     scales the WHOLE site, not just the paragraphs. 1rem = 16px by default;
     body text sits at --fs-md (14px), which is the density the builder and the
     board already use. */
  --font-sans: ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", "SFMono-Regular", Menlo, Consolas, monospace;

  /* 12px, was 11px (0.6875rem). Two page audits reached this token from
     opposite ends of the site — the card browser's pills and tile metadata, the
     landing page's `.chip .n` stacking 11px mono with `--dim` and 0.75 opacity —
     and both stopped short of changing it because it reflows every page.
     It is still the right change: this token carries the "engine gap" and
     "banned" pills, which are CAVEATS, and a warning set in the smallest type
     on the page is a warning designed to be skipped. Badges and axis labels
     lose nothing at 12px. It now equals --fs-xs; the two are kept as separate
     names because they mean different things and may diverge again. */
  --fs-2xs: 0.75rem;    /* 12px — badges, axis labels */
  --fs-xs:  0.75rem;    /* 12px — captions, table meta */
  --fs-sm:  0.8125rem;  /* 13px — secondary body */
  --fs-md:  0.875rem;   /* 14px — body */
  --fs-lg:  1rem;       /* 16px — lede, panel titles */
  --fs-xl:  1.25rem;    /* 20px — h2 */
  --fs-2xl: clamp(1.35rem, 1.1rem + 1.1vw, 1.75rem);  /* h1 on inner pages */
  --fs-3xl: clamp(1.9rem, 1.35rem + 2.4vw, 3rem);     /* the home hero only */

  --lh-tight: 1.18;
  --lh-snug:  1.35;
  --lh-body:  1.55;

  /* --- space --------------------------------------------------------------
     A 4px base, doubling with two half-steps. Every margin, gap and padding in
     this file comes from here; nothing invents a 7px. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 72px;

  /* --- shape --- */
  --radius: 8px;
  --radius-sm: 5px;
  --radius-lg: 14px;
  --radius-pill: 999px;

  /* --- layout -------------------------------------------------------------
     --site-max caps the column so a 2560px monitor gets a readable page rather
     than a 40-word line. --card-min drives .card-grid: 150px means a 300px
     thumbnail renders at 2x with nothing invented, and two cards still fit a
     360px phone. */
  --site-max: 1400px;
  --measure: 74ch;
  --card-min: 150px;
  --nav-h: 52px;

  /* --- colour: LIGHT is the base, dark is applied below -------------------- */
  --bg:      #f2f4f8;
  --panel:   #ffffff;
  --panel-2: #e9edf5;
  --line:    #ccd3e2;
  --ink:     #161922;
  --dim:     #59617a;
  --accent:  #2158c9;
  --good:    #17784a;
  --warn:    #8a5b00;
  --bad:     #c02c2c;

  /* The game's six colours (CR 2-3-3) — identity, not intent. A red card is
     not an error and a green one is not success; never substitute one of
     these for --good/--warn/--bad. Canonical lowercase names, per
     .claude/skills/optcg-design. */
  --c-red:    #c0393e;
  --c-green:  #2e8455;
  --c-blue:   #2a659f;
  --c-purple: #6f4590;
  --c-black:  #414852;
  --c-yellow: #bf9312;

  /* Derived. Kept as explicit tokens rather than colour-mix() at each use so a
     theme is one block to read and one block to change. */
  --accent-ink:  #ffffff;                 /* text drawn ON an accent fill */
  --accent-soft: rgba(33, 88, 201, 0.09); /* accent as a background wash */
  --accent-line: rgba(33, 88, 201, 0.42);
  --good-soft:   rgba(23, 120, 74, 0.10);
  --warn-soft:   rgba(138, 91, 0, 0.11);
  --bad-soft:    rgba(192, 44, 44, 0.09);
  --hover:       #dfe5f1;                 /* control background on hover */
  --focus:       #1544a8;                 /* focus ring; darker than accent so
                                             it is still visible ON an accent
                                             fill, where accent-on-accent is not */
  --shadow-1: 0 1px 2px rgba(20, 26, 40, 0.07);
  --shadow-2: 0 18px 40px -20px rgba(20, 26, 40, 0.30);
  --art-bg:   #dfe4ee;                    /* behind a card image while it loads */

  /* HOW MUCH BLACK THE PRINTED CARD BACK FOLDS INTO ITS OWN COLOUR (§6b).
     The six card colours are UI ink and lighten in the dark theme so they stay
     legible on a dark panel — correct for a badge, wrong for a card, which is a
     physical object whose printing does not change because the reader changed
     their theme. Left alone, a dark-theme back came out pastel while the light
     one was deep, and the pair did not look like the same card. This is the one
     number that reconciles them: 0 in light, and enough in dark to land the
     ramp back where the light theme already had it. It is a token rather than a
     mix inside the component so the whole correction is one value to read. */
  --cb-deepen: 0%;
}

/* The default signal: no stored choice, so follow the operating system. */
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --bg:      #12141a;
    --panel:   #1b1e27;
    --panel-2: #232734;
    --line:    #333a4d;
    --ink:     #e6e9f2;
    --dim:     #939cb4;
    --accent:  #6ea8fe;
    --good:    #6bd08a;
    --warn:    #ffb84d;
    --bad:     #ff6b6b;

    --c-red:    #e0656a;
    --c-green:  #5fbc84;
    --c-blue:   #6aa5db;
    --c-purple: #a67cc7;
    --c-black:  #8b96a3;
    --c-yellow: #dcb845;

    --accent-ink:  #08101f;
    --accent-soft: rgba(110, 168, 254, 0.14);
    --accent-line: rgba(110, 168, 254, 0.45);
    --good-soft:   rgba(107, 208, 138, 0.14);
    --warn-soft:   rgba(255, 184, 77, 0.14);
    --bad-soft:    rgba(255, 107, 107, 0.14);
    --hover:       #2b3142;
    --focus:       #9dc4ff;   /* LIGHTER than accent here — same reasoning,
                                 inverted, because the fill is now light-on-dark */
    --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.5);
    --shadow-2: 0 18px 44px -18px rgba(0, 0, 0, 0.8);
    --art-bg:   #0b0d12;
    --cb-deepen: 32%;
  }
}

/* An explicit choice, written by site.js from localStorage 'optcg.theme'.
   Specificity (0,2,0) beats the (0,1,0) of :root inside the media query, so
   these win in both directions regardless of source order. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg:      #12141a;
  --panel:   #1b1e27;
  --panel-2: #232734;
  --line:    #333a4d;
  --ink:     #e6e9f2;
  --dim:     #939cb4;
  --accent:  #6ea8fe;
  --good:    #6bd08a;
  --warn:    #ffb84d;
  --bad:     #ff6b6b;
  --c-red:    #e0656a;
  --c-green:  #5fbc84;
  --c-blue:   #6aa5db;
  --c-purple: #a67cc7;
  --c-black:  #8b96a3;
  --c-yellow: #dcb845;
  --accent-ink:  #08101f;
  --accent-soft: rgba(110, 168, 254, 0.14);
  --accent-line: rgba(110, 168, 254, 0.45);
  --good-soft:   rgba(107, 208, 138, 0.14);
  --warn-soft:   rgba(255, 184, 77, 0.14);
  --bad-soft:    rgba(255, 107, 107, 0.14);
  --hover:       #2b3142;
  --focus:       #9dc4ff;
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.5);
  --shadow-2: 0 18px 44px -18px rgba(0, 0, 0, 0.8);
  --art-bg:   #0b0d12;
  --cb-deepen: 32%;
}

:root[data-theme="light"] {
  color-scheme: light;
  --bg:      #f2f4f8;
  --panel:   #ffffff;
  --panel-2: #e9edf5;
  --line:    #ccd3e2;
  --ink:     #161922;
  --dim:     #59617a;
  --accent:  #2158c9;
  --good:    #17784a;
  --warn:    #8a5b00;
  --bad:     #c02c2c;
  --c-red:    #c0393e;
  --c-green:  #2e8455;
  --c-blue:   #2a659f;
  --c-purple: #6f4590;
  --c-black:  #414852;
  --c-yellow: #bf9312;
  --accent-ink:  #ffffff;
  --accent-soft: rgba(33, 88, 201, 0.09);
  --accent-line: rgba(33, 88, 201, 0.42);
  --good-soft:   rgba(23, 120, 74, 0.10);
  --warn-soft:   rgba(138, 91, 0, 0.11);
  --bad-soft:    rgba(192, 44, 44, 0.09);
  --hover:       #dfe5f1;
  --focus:       #1544a8;
  --shadow-1: 0 1px 2px rgba(20, 26, 40, 0.07);
  --shadow-2: 0 18px 40px -20px rgba(20, 26, 40, 0.30);
  --art-bg:   #dfe4ee;
}

/* ---------------------------------------------------------------------------
   2. RESET AND BASE
   --------------------------------------------------------------------------- */

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

/* [hidden] must win. Components below set display on .bar, .notice and others,
   which would otherwise beat the UA's [hidden] rule and leave a "hidden"
   element on screen — and on this site a stray empty meter reads as a
   measurement of zero. */
[hidden] { display: none !important; }

/* overflow-x: clip, not hidden. `hidden` on the root silently breaks
   position: sticky for every descendant, which would kill the nav. `clip` is
   the backstop for a rogue wide element; the RIGHT fix is still to wrap wide
   content in .scroll-x, and everything below does. */
html {
  overflow-x: clip;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100%;
  overflow-x: clip;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  line-height: var(--lh-tight);
  font-weight: 650;
  letter-spacing: -0.012em;
  /* Long card names and clause headings must break rather than push the page
     sideways. */
  overflow-wrap: anywhere;
}
h1 { font-size: var(--fs-2xl); }
h2 { font-size: var(--fs-xl); }
h3 { font-size: var(--fs-lg); }
h4, h5, h6 { font-size: var(--fs-md); }

p, ul, ol, dl, figure, pre, table { margin: 0; }
p { max-width: var(--measure); }

a { color: var(--accent); text-underline-offset: 2px; }
a:hover { text-decoration-thickness: 2px; }

/* Images never exceed their box, and never carry the inline-baseline gap. */
img, svg, video { max-width: 100%; display: block; }

/* Long ids, URLs and pasted decklists are the usual cause of a sideways page. */
code, kbd, samp, pre { font-family: var(--font-mono); font-size: 0.92em; }
pre { overflow-x: auto; }

hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: var(--sp-5) 0;
}

/* The nav is sticky, so a jump to #cr-8-4-1-1 would park the clause underneath
   it. Every anchorable thing keeps the bar's height clear. */
:target,
h1[id], h2[id], h3[id], h4[id],
[id].anchor {
  scroll-margin-top: calc(var(--nav-h) + var(--sp-4));
}
html { scroll-behavior: smooth; }   /* disabled under prefers-reduced-motion */

/* Numbers that change (counts, prices, timers) must not jitter their column. */
[data-count], .stat b, .meta, .mono { font-variant-numeric: tabular-nums; }

/* --- focus ---------------------------------------------------------------
   One visible ring for everything focusable, on both themes. --focus is
   deliberately NOT --accent: an accent ring on an accent-filled button is
   invisible, so the token is the contrasting sibling of the accent instead.
   The soft halo underneath keeps the ring readable against card art too. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px var(--accent-soft);
}
/* Browsers that never learned :focus-visible still get a ring rather than none. */
:focus:not(:focus-visible) { outline: none; }

.skip-link {
  position: absolute;
  left: var(--sp-3);
  top: var(--sp-3);
  z-index: 200;
  transform: translateY(-160%);
  padding: var(--sp-2) var(--sp-4);
  background: var(--accent);
  color: var(--accent-ink);
  border-radius: var(--radius-sm);
  font-weight: 650;
  text-decoration: none;
}
.skip-link:focus { transform: none; }

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

/* ---------------------------------------------------------------------------
   3. SHELL — nav and main
   --------------------------------------------------------------------------- */

/* Styled on both the class and the id: pages ship <header id="site-nav">
   empty and site.js fills it, so the bar must already have its shape before
   (and if) the script runs. */
.site-nav,
#site-nav {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}

.site-nav-inner {
  max-width: var(--site-max);
  margin: 0 auto;
  min-height: var(--nav-h);
  padding: var(--sp-2) var(--sp-4);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.site-brand {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex: none;
  color: var(--ink);
  text-decoration: none;
  font-weight: 700;
  letter-spacing: 0.01em;
}
.site-brand .mark {
  color: var(--accent);
  font-size: var(--fs-lg);
  line-height: 1;
  align-self: center;
}
.site-brand .sub {
  color: var(--dim);
  font-weight: 500;
  font-size: var(--fs-xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

/* The link row scrolls INSIDE ITSELF on a narrow phone. That is the whole
   trick behind "no horizontal page scroll": the overflow is owned by a box
   that is allowed to have it. min-width:0 is what lets a flex child shrink
   below its content width and actually scroll. */
.site-nav-links {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  margin: 0;
  padding: 0;
  list-style: none;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.site-nav-links::-webkit-scrollbar { display: none; }
.site-nav-links > li { flex: none; }

.site-nav-links a {
  display: block;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm);
  color: var(--dim);
  text-decoration: none;
  font-weight: 600;
  white-space: nowrap;
}
.site-nav-links a:hover { color: var(--ink); background: var(--panel-2); }

/* The active page. Colour alone is not enough — the underline carries the
   same information for a reader who cannot separate these two hues. */
.site-nav-links a[aria-current="page"] {
  color: var(--accent);
  background: var(--accent-soft);
  box-shadow: inset 0 -2px 0 var(--accent);
}

.site-theme { flex: none; }
.site-theme .glyph { font-size: var(--fs-lg); line-height: 1; }

/* NARROW SCREENS. The link row can scroll, but the scrollbar is hidden, so a
   nav whose last door is off the edge is a nav that hides a door. The chrome is
   therefore shed in order of how little it carries — descriptor, then the theme
   button's word, then the link padding, then the brand's word.

   That was measured at 360px when there were FIVE links. There are now seven,
   and the seventh is 'My collection' — two words, and the longest label in the
   row. The 360px floor has NOT been re-measured against it, so treat it as
   design intent rather than a verified number: the row still scrolls, which
   makes an overflow survivable rather than broken, but the last door may be off
   the edge on the narrowest phones. Re-measure before quoting a pixel figure. */
@media (max-width: 560px) {
  .site-brand .sub { display: none; }
  .site-theme .label { display: none; }
  .site-nav-inner { gap: var(--sp-2); padding-inline: var(--sp-3); }
  .site-nav-links { gap: 0; }
  .site-nav-links a { padding: var(--sp-1) var(--sp-2); font-size: var(--fs-sm); }
}
@media (max-width: 420px) {
  .site-brand > span:not(.mark) { display: none; }
}

.site-main {
  max-width: var(--site-max);
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-4) var(--sp-8);
  /* Grid children default to min-width:auto and refuse to shrink; this is the
     single most common cause of a page that scrolls sideways. */
  min-width: 0;
}
@media (max-width: 560px) {
  .site-main { padding: var(--sp-5) var(--sp-3) var(--sp-7); }
}

.site-foot {
  max-width: var(--site-max);
  margin: 0 auto;
  padding: var(--sp-5) var(--sp-4) var(--sp-7);
  border-top: 1px solid var(--line);
  color: var(--dim);
  font-size: var(--fs-xs);
}

/* ---------------------------------------------------------------------------
   4. PANEL
   --------------------------------------------------------------------------- */

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--sp-4);
  min-width: 0;   /* survivable inside a grid */
}
.panel.flush { padding: 0; }

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
  border-radius: var(--radius) var(--radius) 0 0;
}
.panel-head h2,
.panel-head h3 {
  font-size: var(--fs-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--dim);
}

.panel-body {
  padding: var(--sp-4);
  min-width: 0;
  overflow-x: auto;   /* a wide table inside a panel scrolls HERE, not the page */
}

.panel-foot {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--line);
  color: var(--dim);
  font-size: var(--fs-xs);
}

/* ---------------------------------------------------------------------------
   5. CONTROLS
   --------------------------------------------------------------------------- */

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: var(--ink);
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: 1.3;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.14s ease, background-color 0.14s ease;
}
.btn:hover:not(:disabled):not([aria-disabled="true"]) {
  border-color: var(--accent);
  background: var(--hover);
}
.btn:disabled,
.btn[aria-disabled="true"] { opacity: 0.45; cursor: default; }

.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
.btn.primary:hover:not(:disabled) { background: var(--accent); filter: brightness(1.08); }

.btn.ghost { background: transparent; }
.btn.sm { padding: var(--sp-1) var(--sp-2); font-size: var(--fs-xs); }

.input {
  width: 100%;
  max-width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  color: var(--ink);
  font-size: var(--fs-md);
}
.input::placeholder { color: var(--dim); opacity: 1; }
select.input {
  /* Native select keeps its own arrow; just stop it from being taller than a
     sibling input on Safari. */
  padding-block: calc(var(--sp-2) - 1px);
}
textarea.input { resize: vertical; min-height: 7em; font-family: var(--font-mono); font-size: var(--fs-sm); }

/* A filter toggle. State lives in aria-pressed so the visual and the assistive
   reading can never disagree. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--dim);
  font-size: var(--fs-xs);
  font-weight: 650;
  cursor: pointer;
  white-space: nowrap;
}
.chip:hover { color: var(--ink); border-color: var(--accent-line); }
.chip[aria-pressed="true"] {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}
.chip .n { font-family: var(--font-mono); font-size: var(--fs-2xs); opacity: 0.75; }

/* A status label. Never interactive — if it can be clicked it is a .chip. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 1px var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--dim);
  font-size: var(--fs-2xs);
  font-weight: 650;
  line-height: 1.6;
  white-space: nowrap;
}
.pill.good { color: var(--good); border-color: var(--good); background: var(--good-soft); }
.pill.warn { color: var(--warn); border-color: var(--warn); background: var(--warn-soft); }
.pill.bad  { color: var(--bad);  border-color: var(--bad);  background: var(--bad-soft); }

/* ---------------------------------------------------------------------------
   6. CARD GRID
   --------------------------------------------------------------------------- */

/* auto-fill + minmax is what makes one rule work from a 360px phone to a
   2560px monitor: 2 columns at the bottom, ~8 at --site-max, and the tile
   never falls below the width where its name stops being readable.
   Override --card-min on the grid, not the tile. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
  gap: var(--sp-3);
  /* Tiles stretch to the tallest in their row on purpose: a two-line card name
     would otherwise make one tile shorter and leave its status pills floating
     at a different height from its neighbours'. .card-tile pushes .flags to the
     bottom with margin-top:auto, which only works if the tile is stretched. */
  align-items: stretch;
  min-width: 0;
}
.card-grid.dense { --card-min: 110px; }
.card-grid.roomy { --card-min: 200px; }

.card-tile {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
  padding: var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: inherit;
  text-decoration: none;
  transition: border-color 0.14s ease, transform 0.14s ease;
}
a.card-tile:hover,
button.card-tile:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
}
button.card-tile { text-align: left; cursor: pointer; font: inherit; }

/* THE ART BOX. A One Piece card is 63x88mm. The ratio lives on the BOX, and
   the image fills it with object-fit: cover — so a scan that is a pixel or two
   off never stretches a face. An oversize Bandai scan (some are 868px tall
   rather than 600) is cropped by a hair, never squashed. */
.card-tile .art {
  display: block;
  width: 100%;
  aspect-ratio: 63 / 88;
  border-radius: var(--radius-sm);
  background: var(--art-bg);
  overflow: hidden;
  position: relative;
}
.card-tile .art > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ---------------------------------------------------------------------------
   6b. THE CARD BACK — one drawing, every caller

   docs/art/ is gitignored (`git ls-files docs/art` returns 0), so on any host
   that is not the machine that built the site EVERY card image is missing —
   that is the site's normal starting condition, not an error state. .card-back
   is what a card-shaped box shows there, which means it is the first thing a
   visitor sees, on most of the cards, most of the time. So it is not a
   placeholder: it is an actual trading card back, of the kind you recognise
   across a table without reading anything on it.

   WHAT IT IS MADE OF, outermost first. Six layers, because that is the
   difference between a card back and a coloured rectangle:

     the GROUND  a six-stop ramp down one hue — the sheen of a printed surface;
                 one flat fill reads as a div — plus a specular bloom at the
                 top left and a linen crosshatch, so the field has a texture to
                 catch the light instead of a gradient's flatness.
     the RAYS    21 hairline rays from behind the seal, radially masked so they
                 bloom at the centre and die before the frame. An ODD count, so
                 no two are ever collinear across the card.
     the RAIL    the bevel: a lit top edge, a dark hairline all round. One
                 layer, and most of what reads as printed cardboard.
     the FRAME   a double keyline inset from the trim, corner brackets and four
                 studs — the ornate border a printed back always has.
     the SEAL    a struck medallion: a rim, a 60-tooth guilloche ring, an inner
                 hairline and a recessed well.
     the ROSE    an eight-point compass rose, every arm split light/dark down
                 its own axis by a four-quadrant conic — which is what makes it
                 read as engraved rather than stencilled.

   THE IP LINE. Nothing here is anybody's property, and nothing is traced.
   The six colours are a game mechanic (CR 2-3-3). A compass rose, a guilloche
   ring, brass studs and a keyline frame predate this game by centuries and
   belong to everyone. There is no wordmark, no lettering, no crest, no
   character mark and no scan — docs/art/ui/back.webp, a photograph of the
   printed article, was deleted for exactly that reason (docs/cosmetics/
   manifest.json states the rule in its own header). This is a different object
   that happens to come from the same sea; the arrangement is ours.

   USAGE — three children, in this order, all decorative:

     <span class="card-back c-red" aria-hidden="true">
       <span class="card-back__frame"></span>
       <span class="card-back__ring"></span>
       <span class="card-back__mark"></span>
     </span>

   The colour class is one of .c-red .c-green .c-blue .c-purple .c-black
   .c-yellow; omit it for a neutral card (colourless, or not resolved yet). The
   box sizes itself (63:88, 100% wide), so it drops into any container without
   that container pre-establishing a height — render it IN PLACE OF the <img>,
   never behind or beside one.

   OR one of the five PRINTED colorways — .card-back--standard,
   --leader, --don, --don-op01, --don-op02 — which say "this is that physical
   back" rather than "this card is red". See THE PRINTED COLORWAYS below for
   which is which and why the DON!! three invert. The two sets are alternatives:
   a card either shows its color because its picture is missing, or it shows
   the back it was actually printed with.

   THREE TOKENS EXIST ONLY SO THEY NEED NO SELECTOR OF THEIR OWN: --cb-ray (THE
   RAYS) and --cb-rail-hi / --cb-rail-lo (THE RAIL) are read with the shipped
   values as their var() fallbacks, so a caller that names none of them gets
   exactly the drawing that was here before they existed. Every other layer
   already reached the colorway through --cb-ink, --cb-mark and --cb-field.

   ONE DRAWING, FOUR CALLERS. docs/cards.html and docs/collection.html link
   this file and get it directly. docs/play/index.html cannot link it: the board
   declares its own dark palette on a bare `:root`, and §1 above declares the
   site's on `:root[data-theme=...]` precisely so an explicit choice outranks an
   inferred one — which means linking this file there repaints the mat rather
   than merely lending it a card back. So the board carries a SCOPED COPY under
   `.cb`, in which every rule below THE INK is character for character the one
   here, child class names included. templates/builder.template.html carries a
   third copy for the same reason, under `.card-back` (and it GENERATES
   docs/builder.html through scripts/build-builder.mjs, so the template is the
   file to edit and the script has to be re-run). IF THIS SECTION CHANGES, BOTH
   COPIES CHANGE WITH IT. The split below is drawn where it is so that diff
   stays mechanical: THE PRINTED COLORWAYS and THE COLOURWAY sit ABOVE it,
   which is exactly why the two copies are free to differ there and nowhere
   else.

   TWO GEOMETRY TRAPS, both of which have already cost this project real time.

   A PERCENTAGE IS NOT A LENGTH ON A 63x88 BOX. `border-radius: 6%` takes its
   horizontal radius from the width and its vertical one from the height, so on
   a card it draws an egg — which is what this file used to do. `6% / 4.3%`
   (6 x 63/88) draws a circle. `inset` has the same asymmetry: an inset of 4.4%
   of the card's WIDTH is written `3.15% 4.4%`, because 63/88 = 0.71591 converts
   a width-percentage into the height-percentage of the same length. Every inset
   below carries that arithmetic in its comment.

   AN ELEMENT IS NEVER ITS OWN CONTAINER. `container-type: inline-size` makes
   this box a query container for its DESCENDANTS; a cqw written on the box
   itself resolves against the ANCESTOR. That is how docs/play/launch.html's
   4.76cqw corner came out at 89px on a 155px card and every card in the hand
   rendered as a pill. So NOTHING here spends cqw: every length is a percentage
   of the element's own box, which is also what makes the drawing portable onto
   a host the caller has already sized (the board's .cardimg). The
   container-type is here for one job — the size query at the very end, which
   is a rule on descendants.
   --------------------------------------------------------------------------- */

/* THE BOX. Geometry only, and one of the three rules the play board does not
   copy: its cards are sized by the mat, and .cardimg already carries the 63:88
   ratio and the trim radius. It adds `container-type` to .cb and nothing else.
   (The other two are THE COLOURWAY below, which is per seat there rather than
   per card colour, and THE RAIL, which the board keeps on .cardimg::after
   because a player's uploaded SLEEVE needs the same edge and never gets the
   drawing.) */
.card-back {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 63 / 88;
  overflow: hidden;
  border-radius: 6% / 4.3%;   /* 4.3 = 6 x 63/88 — a circle, not an egg */
  container-type: inline-size;
}

/* THE COLOURWAY — eight custom properties, and the ONLY rule the board does
   not copy. Six stops from ONE token via color-mix(), not six hand-picked hexes
   per colour per theme: --card-color already flips with the active theme, so
   the whole ramp does too, for free. --cb-ramp is named separately so a caller
   with a fixed colourway (the board's DON!! card) can replace the ramp and keep
   every other layer.

   The board's backs are per SEAT rather than per card colour — two players at a
   table have to tell their own Life stack from the other one's at a glance — so
   it declares these same eight properties with its own literal values, tuned
   against a dark mat, and takes everything below unchanged. That is the whole
   of the intended variation between the two. */
.card-back {
  --card-color: var(--dim);         /* neutral default: no colour class applied */
  /* THE PRINTED INK, which is NOT the UI ink. --c-blue lightens in the dark
     theme so a blue badge stays legible on a dark panel; a card is a physical
     object and does not reprint itself when the reader changes theme. --cb-deepen
     (§1) is 0 in light and 32% in dark, which lands the two within a shade of
     each other — checked side by side at 400px, which is the size that shows it. */
  --cb-ink: color-mix(in srgb, var(--card-color), black var(--cb-deepen));
  --cb0: color-mix(in srgb, var(--cb-ink) 34%, white);
  --cb1: color-mix(in srgb, var(--cb-ink) 78%, white);
  --cb2: var(--cb-ink);
  --cb3: color-mix(in srgb, var(--cb-ink) 66%, black);
  --cb4: color-mix(in srgb, var(--cb-ink) 36%, black);
  --cb5: color-mix(in srgb, var(--cb-ink) 54%, black);
  /* The foil. One warm metal tinted by the card's own hue, so six colourways
     still read as one product line rather than six unrelated cards. */
  --cb-mark:  color-mix(in srgb, var(--cb-ink) 20%, #f2dfb4);
  /* The medallion's field, deeper than the ground it sits on so the seal reads
     as struck INTO the card rather than printed on top of it. */
  --cb-field: color-mix(in srgb, var(--cb-ink) 38%, black);
  --cb-ramp: linear-gradient(155deg,
    var(--cb0) 0%, var(--cb1) 11%, var(--cb2) 37%,
    var(--cb3) 63%, var(--cb4) 88%, var(--cb5) 100%);
}

/* THE PRINTED COLORWAYS — the register of backs that exist as objects.

   (US spelling, per the house rule, which is why this heading does not match
   THE COLOURWAY above. That one is left alone on purpose: docs/play/index.html
   and templates/builder.template.html both cite it BY NAME as the rule they
   deliberately do not copy, so renaming it here would quietly cut the thread
   holding three copies of this drawing together. New prose is US; that heading
   is a proper noun.)

   The six .c-* classes above are IDENTITY: they say what color a card is when
   its art is missing, and that is the job on docs/cards.html and
   docs/collection.html. These five are something else — they are the actual
   printed backs, the ones you see face-down across a table, and each is one
   physical ink. They are opt-in by class and change nothing that does not ask
   for them.

   EVERY ONE OF THEM IS TOKENS ONLY. Not one rule below re-declares a layer of
   the drawing; the ground, the rays, the rail, the frame, the seal and the rose
   are the same six layers under THE INK for all five, and the whole difference
   between a leader back and a DON!! back is which custom properties are set.
   That is deliberate: the moment a colorway needs its own selector on a layer,
   there are two card backs again.

   --cb-deepen IS 0 ON ALL FIVE, and this is the point of them. §1's 32% exists
   to walk the six UI colors back down after they lighten for the dark theme,
   because a badge has to stay legible on a dark panel. A print ink has no such
   problem: it is a fixed pigment, and the same card is the same card under any
   theme. So these five name their ink outright and opt out of the correction —
   which is also what makes them render identically in light and dark, as a
   physical object should.

   THE INKS. Named here rather than in §1 because they are not UI color: no
   theme block may redefine them, and keeping them beside the sentence that
   says why is what stops someone helpfully adding a dark variant later. */
.card-back {
  --cb-print-navy:      #2a3a7e;   /* the regular back — deep navy, toward indigo */
  --cb-print-red:       #bd2630;   /* the leader back — a strong, flat red */
  --cb-print-teal:      #0f6f72;   /* DON!!, current print */
  --cb-print-teal-op01: #17786a;   /* DON!!, OP01 — the same teal run greener */
  --cb-print-teal-op02: #0d6486;   /* DON!!, OP02 — and the same run cooler */
}

/* The two dark-ground backs. One line each, because one line is all a
   colorway is: the ramp, the foil and the seal all derive from the ink. */
.card-back--standard { --card-color: var(--cb-print-navy); --cb-deepen: 0%; }
.card-back--leader   { --card-color: var(--cb-print-red);  --cb-deepen: 0%; }

/* THE DON!! BACKS INVERT, and that inversion is the whole reason the family
   works: dark ink on a near-white ground, where every other back is light ink
   on a dark ground. It is not decoration. Across a table a DON!! card is told
   from a card by VALUE — pale versus dark — at a distance where no ornament on
   either is resolvable, and inverting is the only cue that survives that far.

   So the ground becomes paper, the foil becomes the ink (the engraving reads by
   being DARKER than what it sits on, exactly as the play board's gold back
   already argues), the seal's field becomes a light tint of the same ink so it
   still sits deeper than the paper, and the rays and the rail hairline invert
   with them — all five by token, none by re-declaring a layer.

   The three prints differ in ink and in paper, which is what actually differs
   between print runs of one card. Nothing else about them differs at all. */
.card-back--don,
.card-back--don-op01,
.card-back--don-op02 {
  --cb-deepen: 0%;
  --cb-paper: #ffffff;
  --cb0: var(--cb-paper);
  --cb1: color-mix(in srgb, var(--cb-ink)  2%, var(--cb-paper));
  --cb2: color-mix(in srgb, var(--cb-ink)  5%, var(--cb-paper));
  --cb3: color-mix(in srgb, var(--cb-ink)  9%, var(--cb-paper));
  --cb4: color-mix(in srgb, var(--cb-ink) 17%, var(--cb-paper));
  --cb5: color-mix(in srgb, var(--cb-ink)  7%, var(--cb-paper));
  --cb-mark:  var(--cb-ink);
  --cb-field: color-mix(in srgb, var(--cb-ink) 20%, var(--cb-paper));
  /* White rays are invisible on paper; these are the ink, at ray strength. */
  --cb-ray:     color-mix(in srgb, var(--cb-ink) 16%, transparent);
  --cb-rail-hi: rgba(255, 255, 255, 0.55);
  --cb-rail-lo: color-mix(in srgb, var(--cb-ink) 42%, transparent);
}
.card-back--don      { --card-color: var(--cb-print-teal); }
.card-back--don-op01 { --card-color: var(--cb-print-teal-op01); --cb-paper: #fcfaf2; }
.card-back--don-op02 { --card-color: var(--cb-print-teal-op02); --cb-paper: #f5f9fb; }

/* --------------------------------- THE INK -------------------------------
   From here to the end of the section, docs/play/index.html's copy is this
   text with `.card-back` read as `.cb`, minus THE RAIL (which it keeps on
   .cardimg::after, so a sleeve gets the edge too) and nothing else changed.
   Keep it that way: the moment a value diverges there are two card backs again.
   -------------------------------------------------------------------------- */

/* THE GROUND. */
.card-back {
  background-color: var(--cb3);
  background-image:
    /* varnish: one soft specular, off-centre, so the card has a light source */
    radial-gradient(120% 86% at 17% 6%, rgba(255, 255, 255, 0.20), rgba(255, 255, 255, 0) 56%),
    /* linen: two hairline hatches at right angles. In px, not percentages —
       print texture has a fixed pitch and does not scale with the card. */
    repeating-linear-gradient(45deg,  rgba(255, 255, 255, 0.05) 0 1px, rgba(255, 255, 255, 0) 1px 3px),
    repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0.06) 0 1px, rgba(0, 0, 0, 0) 1px 3px),
    var(--cb-ramp);
}
.c-red    { --card-color: var(--c-red); }
.c-green  { --card-color: var(--c-green); }
.c-blue   { --card-color: var(--c-blue); }
.c-purple { --card-color: var(--c-purple); }
.c-black  { --card-color: var(--c-black); }
.c-yellow { --card-color: var(--c-yellow); }

/* THE RAYS. 360/17.142857 = 21 rays, an odd number so no ray is ever the
   continuation of the one opposite it — which is what stops the fan reading as
   a set of straight lines through the middle of the card. The mask is what
   keeps them ornamental: full strength under the seal, gone before the frame,
   so they never fight the keylines for the eye. */
.card-back::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background-image: repeating-conic-gradient(from 8.6deg at 50% 45%,
    var(--cb-ray, rgba(255, 255, 255, 0.13)) 0 2.2deg, rgba(255, 255, 255, 0) 2.2deg 17.142857deg);
  -webkit-mask-image: radial-gradient(closest-side circle at 50% 45%,
    #000 0 20%, rgba(0, 0, 0, 0.55) 48%, rgba(0, 0, 0, 0) 76%);
          mask-image: radial-gradient(closest-side circle at 50% 45%,
    #000 0 20%, rgba(0, 0, 0, 0.55) 48%, rgba(0, 0, 0, 0) 76%);
}

/* THE RAIL. One layer, and most of what reads as printed cardboard rather than
   a coloured div. The LIT TOP EDGE is the strongest line: a dark card on a dark
   page is separated by light, not by shadow, and the dark hairline all round
   does the same job in reverse on a pale colourway. Together they work either
   way up, which a single rim colour cannot.
   inset: 4.4% of the card's WIDTH — 4.4 x 0.71591 = 3.15 vertically.

   THE TWO HAIRLINES ARE TOKENS, and the defaults ARE the values this rule
   shipped with, so every colorway that does not name them is unchanged. They
   exist for the inverted DON!! backs: at 42% black on a near-white ground the
   dark hairline stops reading as a trim and starts reading as a pencil line
   drawn round the card, which is the same fault the play board already names
   on gold ("a black hairline reads as grime"). A colorway inverts by naming
   colors, never by re-declaring this rule. */
.card-back::after {
  content: '';
  position: absolute;
  inset: 3.15% 4.4%;
  border-radius: 4% / 2.8%;
  pointer-events: none;
  background-image: linear-gradient(180deg,
    rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.02) 20%,
    rgba(0, 0, 0, 0) 42% 76%, rgba(0, 0, 0, 0.24) 100%);
  box-shadow:
    inset 0 1px 0 var(--cb-rail-hi, rgba(255, 255, 255, 0.22)),
    inset 0 -1px 0 var(--cb-rail-lo, rgba(0, 0, 0, 0.42)),
    inset 0 0 0 1px var(--cb-rail-lo, rgba(0, 0, 0, 0.42));
}

/* THE FRAME. inset 9.4% of the card's width (9.4 x 0.71591 = 6.73 vertically),
   which leaves the rail its own margin and still gives the seal the middle of
   the card. Its own box is then 0.812W wide and 1.209W tall, and every inset
   and radius below is stated as a percentage of THAT — the arithmetic is in
   each comment because it is not guessable from the number. */
.card-back__frame {
  position: absolute;
  inset: 6.73% 9.4%;
  border: 1px solid color-mix(in srgb, var(--cb-mark) 62%, transparent);
  border-radius: 3.5% / 2.35%;    /* 3.5 x 0.812/1.209 = 2.35 */
  pointer-events: none;
  /* Four studs, one per corner. Four identical layers rather than one with
     four positions: the layer count is set by background-image, and the other
     properties cycle against it — one image would have taken one position and
     silently dropped the other three. */
  background-image:
    radial-gradient(closest-side circle, var(--cb-mark) 0 56%, rgba(0, 0, 0, 0) 60%),
    radial-gradient(closest-side circle, var(--cb-mark) 0 56%, rgba(0, 0, 0, 0) 60%),
    radial-gradient(closest-side circle, var(--cb-mark) 0 56%, rgba(0, 0, 0, 0) 60%),
    radial-gradient(closest-side circle, var(--cb-mark) 0 56%, rgba(0, 0, 0, 0) 60%);
  background-repeat: no-repeat;
  background-size: 5.6% 3.76%;    /* 5.6 x 0.812/1.209 = 3.76 — round studs */
  background-position: 0 0, 100% 0, 0 100%, 100% 100%;
}
/* The inner keyline: 4.5% of the CARD's width inside the frame. */
.card-back__frame::before {
  content: '';
  position: absolute;
  inset: 3.7% 5.5%;               /* 5.5 x 0.812 = 3.7 x 1.209 = 0.0447 card widths */
  border: 1px solid color-mix(in srgb, var(--cb-mark) 34%, transparent);
  border-radius: 2.6% / 1.75%;
}
/* THE CORNER BRACKETS. A third keyline that exists only at the four corners:
   the box is a complete border, and the mask keeps four rectangles of it. Four
   copies of the same mask layer, for the same reason the studs are four. */
.card-back__frame::after {
  content: '';
  position: absolute;
  inset: 1.85% 2.75%;             /* 2.75 x 0.812 = 1.85 x 1.209 = 0.0223 card widths */
  border: 1px solid var(--cb-mark);
  border-radius: 3% / 2%;
  opacity: 0.72;
  -webkit-mask-image: linear-gradient(#000, #000), linear-gradient(#000, #000),
                      linear-gradient(#000, #000), linear-gradient(#000, #000);
          mask-image: linear-gradient(#000, #000), linear-gradient(#000, #000),
                      linear-gradient(#000, #000), linear-gradient(#000, #000);
  -webkit-mask-size: 26% 15%;
          mask-size: 26% 15%;
  -webkit-mask-position: 0 0, 100% 0, 0 100%, 100% 100%;
          mask-position: 0 0, 100% 0, 0 100%, 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* THE SEAL. A CIRCLE, and it has to be said in width on both axes: `inset: 10%`
   on a 63x88 box draws an ellipse, because a percentage inset resolves against
   the width on one axis and the height on the other. width + aspect-ratio: 1
   is immune to that. It sits at 45% rather than 50% because optical centre on a
   portrait card is above the geometric one.

   The teeth are ONE repeating-conic-gradient under an opaque field with an
   annulus punched out of it — 60 teeth show in the ring and nowhere else, with
   no second element and no mask. */
.card-back__ring {
  position: absolute;
  left: 50%;
  top: 45%;
  transform: translate(-50%, -50%);
  width: 54%;
  aspect-ratio: 1;
  border-radius: 50%;
  pointer-events: none;
  background-image:
    radial-gradient(74% 68% at 32% 22%, rgba(255, 255, 255, 0.20), rgba(255, 255, 255, 0) 62%),
    radial-gradient(closest-side circle,
      var(--cb-field) 0 63%, rgba(0, 0, 0, 0) 63% 78%, var(--cb-field) 78% 100%),
    repeating-conic-gradient(from 3deg, var(--cb-mark) 0 1.6deg, rgba(0, 0, 0, 0) 1.6deg 6deg);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--cb-mark) 70%, transparent),
    inset 0 2px 5px rgba(0, 0, 0, 0.35),
    0 1px 3px rgba(0, 0, 0, 0.40);
}
/* The hairline inside the teeth. */
.card-back__ring::before {
  content: '';
  position: absolute;
  inset: 24%;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--cb-mark) 55%, transparent);
}
/* The well the rose sits in — a dish, not a disc: the shading is what gives the
   seal depth at a glance, before any of the hairlines are resolvable. */
.card-back__ring::after {
  content: '';
  position: absolute;
  inset: 26.5%;
  border-radius: 50%;
  background-image: radial-gradient(closest-side circle,
    rgba(0, 0, 0, 0) 0 52%, rgba(0, 0, 0, 0.26) 100%);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.40);
}

/* THE COMPASS ROSE. The element is a positioning box only; the two stars are
   its pseudo-elements, because a clip-path on the box would also clip them and
   the second star would survive only where it overlapped the first.

   WHAT MAKES IT A ROSE AND NOT A PLUS SIGN, and both had to be tuned against a
   card at 44px, not at 400:
     the WAIST — 43%/57%. The arms are a seventh as wide as they are long at the
     shoulder, so they taper instead of meeting at a square crossing.
     the SHADING — a four-quadrant conic whose boundaries lie ALONG the four
     cardinal arms, so every arm is light on one side of its own axis and dark
     on the other. That is the engraving; without it a compass rose is a
     silhouette, and a silhouette of a star is a star. */
.card-back__mark {
  position: absolute;
  left: 50%;
  top: 45%;
  transform: translate(-50%, -50%);
  width: 30%;
  aspect-ratio: 1;
  pointer-events: none;
}
.card-back__mark::before,
.card-back__mark::after {
  content: '';
  position: absolute;
  inset: 0;
  clip-path: polygon(50% 0%, 57% 43%, 100% 50%, 57% 57%,
                     50% 100%, 43% 57%, 0% 50%, 43% 43%);
}
/* The intercardinal star, declared FIRST so the cardinal one lies over it —
   the two together are the eight points. Shorter and dimmer, which is what
   keeps the four cardinal arms dominant. */
.card-back__mark::before {
  inset: 15%;
  transform: rotate(45deg);
  background-image: conic-gradient(from 45deg,
    color-mix(in srgb, var(--cb-mark) 72%, black) 0 90deg,
    color-mix(in srgb, var(--cb-mark) 40%, black) 90deg 180deg,
    color-mix(in srgb, var(--cb-mark) 72%, black) 180deg 270deg,
    color-mix(in srgb, var(--cb-mark) 40%, black) 270deg 360deg);
}
/* The cardinal star, plus its pierced hub as a background layer — no third
   element needed, and the hub lands over the intercardinal star's centre where
   it would otherwise be a solid brass lump. */
.card-back__mark::after {
  background-image:
    radial-gradient(closest-side circle, var(--cb-field) 0 62%, var(--cb-mark) 68% 100%),
    conic-gradient(from 0deg,
      var(--cb-mark) 0 90deg,
      color-mix(in srgb, var(--cb-mark) 46%, black) 90deg 180deg,
      var(--cb-mark) 180deg 270deg,
      color-mix(in srgb, var(--cb-mark) 46%, black) 270deg 360deg);
  background-repeat: no-repeat;
  background-size: 21% 21%, 100% 100%;
  background-position: 50% 50%, 0 0;
}

/* WHERE THE DETAIL IS SHED, AND WHY THE LINE IS AT 54.

   It was first drawn at 92px, on the assumption that hairlines landing that
   close together would be mud. They are not: photographed side by side at 1x as
   well as 2x, the full drawing still resolves at 72px and only begins to speckle
   around 56. What 92 DID do was cost the play board the drawing altogether. The
   sizes there, measured rather than guessed, on a 1500px window:

     46px  a Life card                     the only one below the line
     62px  deck, trash, the DON!! deck
     64px  a face-up DON!! (its own burst; none of this applies)
     72px  a card in the opponent's hand

   So 54 splits the mat exactly where the drawing itself gives out. Everything at
   62 and up carries the frame, the guilloche and the eight-point rose; the Life
   stack, which is a third smaller than anything beside it and is READ AS A COUNT
   rather than looked at, keeps the seal and drops the fine work. That difference
   tracks a real difference in size, which is what stops it reading as a bug.

   Below 54 the trade genuinely reverses: three keylines land inside three pixels
   of each other, and 60 teeth on an 11px ring average well under half a pixel
   apiece — a grey smear that reads as a blurred photograph, which is worse than
   no ring at all. What survives — ground, rays, rail, one keyline, the seal and
   a four-point star — still reads as a card back at the 40px thumbnail
   docs/collection.html puts in every row, the smallest this component is ever
   asked to be.

   This is the ONLY reason .card-back declares container-type, and it is a query
   on DESCENDANTS, which is the only kind that resolves against this box. */
@container (max-width: 54px) {
  .card-back__frame::before,
  .card-back__frame::after,
  .card-back__ring::before,
  .card-back__ring::after,
  .card-back__mark::before { display: none; }
  .card-back__frame { background-image: none; }
  .card-back__ring {
    background-color: var(--cb-field);
    background-image:
      radial-gradient(74% 68% at 32% 22%, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 62%);
  }
}

.card-tile .name {
  font-weight: 650;
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  /* Two lines, then ellipsis — so a long name cannot make one tile taller than
     its row-mates and shred the grid. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.card-tile .meta {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.card-tile .flags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  margin-top: auto;
  padding-top: var(--sp-1);
}

/* ---------------------------------------------------------------------------
   7. NUMBERS AND STATEMENTS
   --------------------------------------------------------------------------- */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--sp-2);
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  min-width: 0;
}
.stat b {
  font-family: var(--font-mono);
  font-size: var(--fs-xl);
  font-weight: 650;
  line-height: 1.1;
}
.stat span {
  font-size: var(--fs-2xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 650;
  color: var(--dim);
}
.stat.good b { color: var(--good); }
.stat.warn b { color: var(--warn); }
.stat.bad  b { color: var(--bad); }

/* A boxed statement. The site uses these to say what it CANNOT do, so they are
   a first-class component and not an afterthought. */
.notice {
  border: 1px solid var(--line);
  border-left-width: 3px;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--panel-2);
  font-size: var(--fs-sm);
}
.notice > * + * { margin-top: var(--sp-2); }
.notice p { max-width: none; }
.notice.good { border-left-color: var(--good); background: var(--good-soft); }
.notice.warn { border-left-color: var(--warn); background: var(--warn-soft); }
.notice.bad  { border-left-color: var(--bad);  background: var(--bad-soft); }
.notice h3 { font-size: var(--fs-md); margin-bottom: var(--sp-1); }

/* A coverage meter. Width is set inline from real data; the label beside it
   always carries the same number in words, because a bar alone is not a fact. */
.bar {
  display: block;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--panel-2);
  border: 1px solid var(--line);
  overflow: hidden;
}
.bar > .bar-fill {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: inherit;
}
.bar > .bar-fill.good { background: var(--good); }
.bar > .bar-fill.warn { background: var(--warn); }
.bar > .bar-fill.bad  { background: var(--bad); }

/* ---------------------------------------------------------------------------
   8. TABLES AND LISTS
   --------------------------------------------------------------------------- */

/* Any table wide enough to overflow must be wrapped in .scroll-x. */
.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.table th,
.table td {
  padding: var(--sp-2) var(--sp-3);
  text-align: left;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.table th {
  font-size: var(--fs-2xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  font-weight: 700;
  white-space: nowrap;
}
.table tbody tr:hover { background: var(--panel-2); }
.table td.num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------
   9. TEXT AND LAYOUT UTILITIES
   --------------------------------------------------------------------------- */

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

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 650;
  color: var(--accent);
}

.lede {
  font-size: var(--fs-lg);
  line-height: var(--lh-snug);
  color: var(--dim);
  max-width: var(--measure);
}

.meta {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--dim);
}

.kbd {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  padding: 1px 5px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--panel-2);
  color: var(--ink);
  white-space: nowrap;
}

.tag {
  display: inline-block;
  padding: 1px var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: var(--fs-2xs);
  color: var(--dim);
  white-space: nowrap;
}

/* The honest end of a list: nothing here, said plainly, at the size of the
   thing that would have been here. */
.empty {
  padding: var(--sp-6) var(--sp-4);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--dim);
  text-align: center;
  font-size: var(--fs-sm);
}
.empty strong { display: block; color: var(--ink); margin-bottom: var(--sp-1); }

/* Horizontal group that wraps. */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}
/* Vertical rhythm without margin collapsing surprises. */
.stack { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.stack.tight { gap: var(--sp-2); }
.stack.loose { gap: var(--sp-6); }

/* THE overflow escape hatch. Anything too wide goes in here and scrolls
   itself. tabindex="0" on the element makes it keyboard-scrollable too. */
.scroll-x {
  overflow-x: auto;
  max-width: 100%;
  min-width: 0;
  overscroll-behavior-x: contain;
}

/* ---------------------------------------------------------------------------
   10. MOTION
   --------------------------------------------------------------------------- */

/* Everything above uses short functional transitions only, so removing them
   costs nothing. A reader who asked for less motion gets none. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  a.card-tile:hover,
  button.card-tile:hover { transform: none; }
}

/* The :root[data-theme] half is not redundant: a bare :root here is (0,1,0)
   and would lose to the explicit-theme blocks above, so a reader who both
   picked a theme AND asked for more contrast would silently get neither. */
@media (prefers-contrast: more) {
  :root,
  :root[data-theme] {
    --dim: var(--ink);
    --line: var(--ink);
  }
  .panel, .card-tile, .btn, .input { border-width: 2px; }
}

/* ---------------------------------------------------------------------------
   11. THE FRONT DOOR — a hand of cards, and the sea behind it

   docs/index.html is the page that decides whether a stranger stays, and the
   front door of a CARD GAME should be a hand of cards rather than a list of
   links. Each door is one card: §6b's drawing, in one of the five PRINTED
   colorways, with a nameplate struck into it and a line of prose under it.
   docs/play/launch.html has made that argument for a while; this is the same
   argument in the site's own tokens rather than in the launcher's private
   sea-and-sky palette.

   NOTHING HERE CHANGES §6b, AND THAT IS THE POINT. §6b's drawing is copied
   character for character into docs/play/index.html and
   templates/builder.template.html, and its header says both copies move when it
   does. Every rule below is a NEW class on a card-shaped host, so neither copy
   is touched: .hand-door__plate is a fourth child of .card-back which reads the
   colorway's own custom properties (--cb-field, --cb-mark, --cb-paper) and
   nothing else. Adding a door, or restyling one, never reaches the board.

   THE ONE DEVIATION FROM §6b's DOCUMENTED USAGE, written down because it is a
   deviation: §6b puts aria-hidden="true" on the .card-back itself, since the
   drawing is decoration. Here the card carries the door's NAME, so the
   aria-hidden moves down onto the three drawing spans and the plate stays in
   the accessibility tree. Without that the links would have no accessible name
   but the one the note supplies, which is a description, not a name.

   WHY THE NAME IS ON THE CARD AND NEVER ON HOVER. A phone has no hover, so a
   door whose name appears only on hover is a door half the visitors cannot
   read. The plate is painted at rest, in the card's own foil and field, and the
   note under it says where the door goes in one line. Nothing about which door
   is which is left to the reader to guess.

   WHY THE FAN IS A FORMULA. Angle and lift are computed from --i (the card's
   index, set inline) and --n (how many cards the hand holds). docs/play/ is
   never published — scripts/publish-site.ts excludes it — so on the public site
   the Play door is removed and the hand holds five. Hard-coded angles would
   leave the fan centred on a card that is not there; the formula re-centres
   itself and index.html only has to say how many are left.
   --------------------------------------------------------------------------- */

/* THE HARBOR. A full-bleed band, which is why it is a child of <main> rather
   than of .site-main: it spans the viewport by being an ordinary block in the
   normal flow, with no 100vw anywhere. 100vw counts the scrollbar, so the usual
   `margin-inline: calc(50% - 50vw)` bleed pushes the page sideways by the
   scrollbar's width on every desktop that still draws one — rule 3 at the top
   of this file forbids exactly that.

   `isolation` keeps the sea's stacking to this section, so .seascape can sit at
   z-index 0 under content at 1 without any of it reaching the sticky nav. */
.harbor {
  position: relative;
  isolation: isolate;
  /* The fan is placed by rotation and so reaches outside .harbor__inner on
     purpose — that overhang is what stops it looking like a component in a
     column. `clip` and not `hidden`: clip does not create a scroll container,
     so nothing here can hand the page the sideways scroll that rule 3 at the
     top of this file forbids, and the vertical axis stays visible so the lifted
     card and its caption are never cut off. */
  overflow-x: clip;
  overflow-y: visible;
}
.harbor__inner {
  position: relative;
  z-index: 1;
  max-width: var(--site-max);
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-4) var(--sp-7);
  min-width: 0;
}
@media (max-width: 560px) {
  .harbor__inner { padding: var(--sp-5) var(--sp-3) var(--sp-6); }
}

/* THE SEA. Ships on a horizon, drawn, behind the hand.

   IT IS A CHART, NOT A PHOTOGRAPH, and that is what keeps it inside the token
   system instead of forking a sixth palette the way every other surface in this
   repo did. Hairlines in --line, hulls in --dim, one wash of --accent-soft: the
   sea reads as engraved on the page it is printed on, and it flips with the
   theme for free because every one of those three already does. No new hue is
   invented here, so nothing on this page can be mistaken for an intent colour.

   THE IP LINE. Generic maritime vocabulary only — a hull, a stern castle, three
   masts, sails, rigging, a bowsprit and a BLANK pennant. docs/play/launch.html
   sets the rule for the galleon it draws ("It must never acquire a figurehead,
   a grass deck, a device on the flag or a paddle-wheel stern") and this obeys
   it. A skull and crossbones would be public domain and safe, but at the six
   pixels a masthead pennant gets here it would be a smudge, not a joke. */
.seascape {
  /* WHERE THE HORIZON GOES. High enough that the line crosses BEHIND the hand
     rather than under it — a horizon hidden by the thing in front of it is not
     a horizon, it is a change of colour — and low enough that there is still
     open water under the cards for the near hulls to sit in. At 44% of the
     band it lands around the top third of the fan. */
  --sea-water: 44%;
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;      /* the ships drift; nothing may leave this box */
  pointer-events: none;
  /* It dissolves into the page rather than ending on a line. */
  -webkit-mask-image: linear-gradient(180deg, #000 0 86%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 0 86%, transparent 100%);
}
/* THE SKY, and the one line in it that matters. The third gradient is a HAZE
   band sitting just above the waterline: air over water is lighter at the
   horizon than it is overhead, and without that band the sky and the sea meet
   as two washes that happen to be adjacent. It is a wash of --panel, not a new
   hue — the page's own paper, thinned — so it reads as light rather than as a
   fourth colour, and it flips with the theme like everything else here. */
.seascape__sky {
  position: absolute;
  inset: 0 0 var(--sea-water) 0;
  background:
    linear-gradient(180deg, var(--accent-soft) 0%, transparent 52%),
    linear-gradient(180deg, transparent 40%, var(--accent-soft) 100%),
    linear-gradient(180deg, transparent 62%,
      color-mix(in srgb, var(--panel) 70%, transparent) 100%);
}
/* preserveAspectRatio on the SVG is xMidYMin slice, so the horizon is pinned to
   the top of this box at every width and the swell keeps its proportions
   instead of being squashed into chop on a phone. */
.seascape__water {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--sea-water);
  color: var(--line);
  background: linear-gradient(180deg, var(--accent-soft), transparent 62%);
}
.seascape__water svg { width: 100%; height: 100%; }

/* THE SHIPS, AND WHY THEY DO NOT SHARE A WATERLINE.
   Distance on an open sea is read almost entirely from height in the frame:
   the far ship sits ON the horizon, and every nearer one sits further down the
   water. Three hulls at one --sea-water offset (which is what this was) are
   three hulls at the same distance in three different sizes, which the eye
   reads as an inconsistency rather than as depth. --sink is that offset, in the
   same percentage units as --sea-water, so the whole scene rescales together.

   FOUR HULLS, NOT THREE. The fourth is the smallest and faintest, at the far
   left of the horizon, and it is what turns "some ships" into a sea with things
   happening on it. Opacity is graded with distance too: haze eats the far ones.

   TWO NESTED ELEMENTS, one animation each. The outer .seascape__ship drifts
   along the horizon over a minute-plus; the inner .seascape__hull bobs in
   seconds. One element cannot carry both, because both are `transform` and the
   second would silently win — which is why the drift alone used to have to
   stand in for floating. Placement stays on left/bottom so reduced motion can
   switch the animations off and land every hull exactly where it was authored,
   rather than wherever a keyframe happened to stop. */
.seascape__ship {
  position: absolute;
  bottom: calc(var(--sea-water) - var(--sink));
  width: var(--w);
  color: var(--dim);
  opacity: var(--o);
}
.seascape__hull {
  display: block;
  width: 100%;
  height: auto;
  transform-origin: 50% 92%;   /* it rolls about its own waterline, not its middle */
}
/* The four positions are picked to sit CLEAR OF THE FAN at desktop width, where
   the cards occupy roughly the middle 60% of the band: a hull drawn entirely
   behind an opaque card is a hull nobody will ever see, and four of those is an
   empty sea with a performance cost. Distance is carried by three things at
   once — smaller, fainter, and nearer the horizon — because any one of them
   alone reads as an inconsistency rather than as depth. */
.seascape__ship--far   { --w: clamp(28px, 3.2vw, 46px); --o: 0.14; --sink: 0.2%; left:  8%; animation: seascape-sail 113s ease-in-out  -31s infinite alternate; }
.seascape__ship--mid   { --w: clamp(38px, 4.4vw, 62px); --o: 0.18; --sink: 2.6%; left: 84%; animation: seascape-sail  97s ease-in-out infinite alternate-reverse; }
.seascape__ship--near  { --w: clamp(50px, 6.0vw, 84px); --o: 0.22; --sink: 9.0%; left: 15%; animation: seascape-sail  71s ease-in-out  -22s infinite alternate; }
.seascape__ship--close { --w: clamp(66px, 8.0vw,112px); --o: 0.22; --sink:  20%; left: 72%; animation: seascape-sail  58s ease-in-out   -9s infinite alternate; }

.seascape__ship--far   .seascape__hull { animation: seascape-bob 7.9s ease-in-out infinite; }
.seascape__ship--mid   .seascape__hull { animation: seascape-bob 6.7s ease-in-out -2.1s infinite; }
.seascape__ship--near  .seascape__hull { animation: seascape-bob 5.4s ease-in-out -3.4s infinite; }
.seascape__ship--close .seascape__hull { animation: seascape-bob 4.6s ease-in-out -1.3s infinite; }

@keyframes seascape-sail {
  0%   { transform: translateX(-3%); }
  100% { transform: translateX(3%); }
}

/* The bob is a ROLL plus a rise, not a rise alone: a hull that only goes up and
   down is a lift, and a hull that only leans is a metronome. The two together,
   out of phase with each other, is a boat. Amplitudes are deliberately under
   two pixels — this is a backdrop, and a backdrop that catches the eye has
   already lost its argument with the hand in front of it. */
@keyframes seascape-bob {
  0%   { transform: translateY(0)      rotate(-0.9deg); }
  25%  { transform: translateY(-1.6px) rotate(0.2deg); }
  50%  { transform: translateY(-0.4px) rotate(0.9deg); }
  75%  { transform: translateY(-1.9px) rotate(0.1deg); }
  100% { transform: translateY(0)      rotate(-0.9deg); }
}

/* THE HAND.

   Not a fan at every width. Six overlapping rotated cards is a hand on a
   monitor and an unreadable pile on a phone, so below 1040px this is an
   ordinary grid of upright cards — two columns on a phone, three on a tablet,
   both of which divide six exactly — and the fan is what the extra width buys.
   The card, the plate and the note are identical in all three; only the
   arrangement changes. */
.hand {
  --n: 6;
  /* HALF THE HAND, IN CARDS — the index of its middle, and the only place
     (--n - 1) / 2 is written down. --d and --arc-max both measure from it, so
     when index.html's module sets --n to 5 on the published build there is one
     number to change and the fan re-centres itself.

     A NOTE FOR THE NEXT PERSON WHO MEASURES THIS IN A BACKGROUND TAB, because
     it cost an hour here: Chrome freezes CSS transitions in a hidden tab, and
     `getComputedStyle(...).transform` then keeps returning the value the
     transition STARTED from. Change --n from the console in a tab that is not
     on screen and every door reports a correctly recomputed --d beside a
     transform still rotated for the old count — which looks exactly like a
     custom-property invalidation bug and is not one. Check `document.hidden`,
     or turn .hand-door__card's transition off, before believing it. */
  --dmax: calc((var(--n) - 1) / 2);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-5) var(--sp-3);
  align-items: end;
  justify-items: center;
  min-width: 0;
}
@media (min-width: 620px) {
  .hand { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-5) var(--sp-4); }
}

.hand-door {
  --i: 0;
  position: relative;
  display: grid;
  gap: var(--sp-2);
  width: 100%;
  max-width: 208px;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

/* The card itself is §6b's .card-back; this adds the shadow that lifts it off
   the water and the hover ring. The ring is on the card rather than the whole
   door so it traces the trim, not a rectangle around the caption. */
.hand-door__card {
  box-shadow: var(--shadow-2);
  transition: box-shadow 0.14s ease;
}
.hand-door:hover .hand-door__card,
.hand-door:focus-visible .hand-door__card {
  box-shadow: var(--shadow-2), 0 0 0 2px var(--accent);
}

/* ============================ THE FAN ====================================

   WHAT A FAN ACTUALLY IS, and why the previous version was a row.

   It used to lay the six cards out as flex items and then rotate each one a
   little in place. That gives six cards standing side by side, tilted — which
   is what a row of tilted cards looks like, because the SPACING and the ANGLE
   were two unrelated numbers. In a real hand they are the same number: every
   card is pinned at one point below the hand and swung out from it, so a card
   is far from its neighbour BECAUSE it is turned further, and the two can never
   disagree.

   So that is how this is built. One pivot, --fan-radius below the cards'
   shared centre, and card --i is rotated --fan-angle x --d about it. Everything
   else falls out of that single rotation for free:

     the SPREAD   is --fan-radius x --fan-angle per card, in radians — which is
                  why a card overlaps its neighbour by a constant fraction of
                  its own width at every screen size, with nothing to keep in
                  sync,
     the ARC      is --fan-radius x (1 - cos angle), so the middle cards ride
                  HIGH and the ends drop away. Nothing computes it; a point
                  swung about a pivot below simply travels on a circle, and this
                  is that circle. The old parabola had the sign the other way
                  round and lifted the ENDS, which is a hand nobody holds.

   ALL SIX CARDS SHARE ONE GRID CELL. They have to: the fan's geometry places
   them, so any layout that also placed them would be fighting it. The grid is a
   1x1 with `grid-area: 1 / 1` on every door, and --i decides where each one
   ends up.

   THE HIT AREA FOLLOWS THE CARD, NOT THE LINK. Because the six <a> boxes are
   stacked in that one cell, five of them would be buried under the sixth and
   unclickable. The transform is therefore on the CARD, and the <a> takes
   `pointer-events: none` while the card takes `auto` — hit-testing follows a
   transform, so each card is clickable exactly where it is drawn. :hover still
   reaches the <a>, because a hovered child hovers its ancestors regardless of
   who is accepting the pointer.

   A FAN NEEDS A MOUSE. Everything below is gated on `hover: hover` AND
   `pointer: fine` as well as width, so a touch screen never gets it — not
   because a fan cannot be drawn on a tablet, but because the fan's caption is
   revealed by hovering, and a device that cannot hover would be left with six
   cards and no descriptions. Touch keeps the grid, where every note is simply
   printed under its card and nothing is hidden at all.
   ========================================================================= */
@media (min-width: 1040px) and (hover: hover) and (pointer: fine) {
  .hand {
    /* The card width is the only length the fan is measured in. Everything
       below is a multiple of it, so the whole hand scales as one object and no
       breakpoint has to restate an angle or an offset. */
    --card-w: clamp(150px, 12.4vw, 208px);
    --fan-angle: 7deg;
    /* WHY THE RADIUS IS 7.6 AND NOT SOMETHING ROUNDER. It is not the look of
       the fan that fixes it, it is the NAMEPLATE. Cards swung about a pivot
       below converge downwards — the hand opens at the top and closes at the
       bottom — and the plate is at the bottom of the card, which is the one
       place the spacing is tightest. At the plate's height the step is about
       0.92 of the step at the card's centre, and the plate itself is 0.72 of a
       card wide, so anything under ~0.85 card widths per step buries the name
       of every door but the last. 7.6 x 7deg gives 0.93, which clears it with a
       hairline to spare. The first attempt used 5.9, looked like a hand, and
       hid four of the six names. */
    --fan-radius: calc(var(--card-w) * 7.6);   /* -> 0.93 card widths per step */
    --fan-yield: calc(var(--card-w) * 0.055);  /* how far a neighbour steps aside */
    --fan-lift: calc(var(--card-w) * -0.2);    /* how far the read card rises */
    /* THE CURVE. Not `ease`, and the difference is the whole of ask #2: this
       leaves fast and arrives slowly with no overshoot, which is the motion of
       something being drawn out of a hand rather than something snapping to a
       new position. 520ms is long for this site (§10's doctrine is ~140ms and
       functional) and is deliberate — the hand is the one place on the site
       where the motion IS the content. Named once, used by every moving part
       below so they arrive together. */
    --fan-ease: cubic-bezier(0.22, 0.86, 0.26, 1);

    position: relative;
    display: grid;
    grid-template-columns: var(--card-w);
    justify-content: center;
    gap: 0;
    /* Top: room for the lifted card. Bottom: the arc drops the outer cards by
       0.35 of a card width, their rotated bottom corners by another 0.12, and
       the caption lives under all of it. Both are reserved rather than
       overlapped — .hand-foot is real prose and must not be sat on. */
    padding: var(--sp-7) 0 calc(var(--card-w) * 0.474 + var(--sp-7) + var(--sp-4));
  }

  .hand-door {
    grid-area: 1 / 1;
    position: static;   /* so the caption's containing block is .hand, not this */
    display: block;
    width: var(--card-w);
    max-width: none;
    /* --d is how far this card is from the middle of the hand, in cards:
       -2.5 .. +2.5 for six. Declared here rather than on the card because the
       caption reads it too.

       --arc is how far the fan has already dropped THIS card, which the caption
       has to add back so it hangs the same distance under every card instead of
       lying in a straight line under a curved hand. The true drop is
       radius x (1 - cos d.angle) and CSS has no cosine, so this is its
       small-angle form, radius x (d.angle)^2 / 2, in card widths:
       0.5 x 7.6 x (7deg in radians)^2 = 0.0567. At the widest card in the fan
       that approximation is out by a third of a pixel. */
    --d: calc(var(--i) - var(--dmax));
    /* --step is how far along the hand one card is from the next, DERIVED from
       the pivot rather than typed as a second number. That matters: the first
       version of the caption had 0.72 card widths hard-coded into it, which was
       right for a --fan-radius of 5.9 and silently wrong the moment the radius
       moved to 7.6 — the panel drifted 92px away from the outermost card while
       still looking plausible under the middle ones. Anything that has to know
       where a card IS now reads this. 0.1205 is sin(7deg) averaged over the
       half-hand: the true offset is radius x sin(d.angle) and CSS has no sine,
       so a linear step is used, and this coefficient is the one that keeps the
       error inside a pixel across all six cards rather than only near d = 0. */
    --step: calc(var(--fan-radius) * 0.1205);
    --arc: calc(var(--card-w) * 0.0567 * var(--d) * var(--d));
    /* The same expression at the OUTERMOST card, written in --dmax rather than
       as the 0.354 that six cards happen to produce: the published site drops
       the Play door and the hand holds five (see the §11 header), and a
       constant here would leave every caption in a five-card hand hanging at
       the depth a six-card hand needed. */
    --arc-max: calc(var(--card-w) * 0.0567 * var(--dmax) * var(--dmax));
    z-index: calc(var(--i) + 1);   /* shingled left over right, like a dealt hand */
    pointer-events: none;
  }
  /* THE <a>'S OWN RING IS SUPPRESSED, and the specificity is the point: §2's
     `a:focus-visible` is (0,1,1) and sets an outline AND a halo, so a plain
     `.hand-door { outline: none }` at (0,1,0) loses to it — which is exactly
     what happened, and drew a tall blue rectangle through the middle of the
     hand around a link box that the fan had long since moved away from. Two
     classes beat one class and one element; both properties have to be named
     because both are set there. The ring reappears on the card below, which is
     the shape the reader can actually see. */
  .hand-door:focus-visible {
    outline: none;
    box-shadow: none;
  }

  .hand-door__card {
    pointer-events: auto;
    /* THE PIVOT IS SPELLED OUT RATHER THAN SET AS AN ORIGIN. `transform-origin`
       would put the pivot below the card for every function in the list, and
       then `scale()` would shove the card away from it instead of growing it in
       place. Translating down to the pivot, rotating, and translating back is
       the same rotation with the origin left at the card's own centre, which
       leaves --pop and --straighten free to act on the card itself. */
    transform-origin: 50% 50%;
    transform:
      translate(calc(var(--yield, 0) * var(--fan-yield)), var(--lift, 0px))
      translateY(var(--fan-radius))
      rotate(calc(var(--d) * var(--fan-angle)))
      translateY(calc(var(--fan-radius) * -1))
      rotate(var(--straighten, 0deg))
      scale(var(--pop, 1));
    transition:
      transform 520ms var(--fan-ease),
      box-shadow 320ms var(--fan-ease);
  }

  /* READING A CARD. It rises, grows a little, and STANDS HALF UP — --straighten
     is a counter-rotation about the card's own centre, so the card levels off
     without sliding along the fan. Going all the way to vertical would slide
     it: in this construction the card's position IS its angle, so straightening
     it fully would drag the outer cards a third of the way to the middle. Half
     reads as "lifted to look at" and moves nothing sideways. */
  .hand-door:hover,
  .hand-door:focus-visible {
    z-index: 40;
    --lift: var(--fan-lift);
    --pop: 1.05;
    --straighten: calc(var(--d) * var(--fan-angle) * -0.55);
  }
  /* Doubling --shadow-2 rather than inventing a deeper one: elevation here is
     the same ambient shadow, twice, which is a token used twice and not a sixth
     bespoke rgba() in a file that has spent a lot of words avoiding those. */
  .hand-door:hover .hand-door__card {
    box-shadow: var(--shadow-2), var(--shadow-2), 0 0 0 2px var(--accent);
  }
  /* Keyboard gets the same lift and the site's own focus ring, on the card,
     because in the fan the <a>'s own box is a stack of six identical rectangles
     in the middle of the hand and an outline round it would point at nothing. */
  .hand-door:focus-visible .hand-door__card {
    box-shadow:
      var(--shadow-2), var(--shadow-2),
      0 0 0 3px var(--focus), 0 0 0 6px var(--accent-soft);
  }

  /* THE NEIGHBOURS YIELD. The two cards either side of the one being read step
     away from it, and the rest of the hand follows by a smaller amount — the
     hand opens around the card instead of the card tearing out of a wall.
     Selector order matters and is not incidental: the `~` rules and the `+`
     rules are both (0,3,0), so the immediate-neighbour pair must come second to
     win on source order. :has() is what reaches BACKWARDS to the cards before
     the hovered one; where it is unsupported those simply do not move, which is
     a smaller version of the same effect rather than a broken one. */
  .hand-door:is(:hover, :focus-visible) ~ .hand-door { --yield: 1; }
  .hand-door:has(~ .hand-door:is(:hover, :focus-visible)) { --yield: -1; }
  .hand-door:is(:hover, :focus-visible) + .hand-door { --yield: 1.75; }
  .hand-door:has(+ .hand-door:is(:hover, :focus-visible)) { --yield: -1.75; }

  /* THE CAPTION — ask #2's "present its description properly".

     In the grid this is a line of prose under its own card. In the fan it
     cannot be: the cards overlap, so six captions would overlap too. It becomes
     a panel instead, below the hand, and only the card being read shows one.

     IT TRACKS ITS OWN CARD. The horizontal offset is --d x 0.72 card widths —
     the same expression the fan uses to place the card — so the panel sits
     directly beneath the card it belongs to and its caret points at it, rather
     than appearing in a fixed slot the reader has to connect up themselves.

     WHY IT IS POSITIONED AGAINST .hand. Its containing block is .hand (the door
     is `position: static` above and, crucially, carries no transform of its
     own — a transformed ancestor would have become the containing block and
     dragged the panel into the fan's rotation). One consequence worth stating:
     `bottom: 0` is .hand's padding edge, which is exactly the space reserved
     for it.

     visibility, not just opacity: a caption faded to zero is still in the
     accessibility tree and still a tab stop's worth of text read aloud for five
     cards nobody is pointing at. The delayed `visibility 0s linear 200ms` on
     the way out lets the fade finish before it leaves. */
  .hand-door__note {
    position: absolute;
    left: 50%;
    /* It rides the arc: --arc-max puts it under the lowest card in the hand,
       and subtracting this card's own --arc lifts it back to the same gap under
       whichever card is being read. A caption pinned at one height would sit a
       card's width away from the middle doors and touching the outer ones. */
    bottom: calc(var(--sp-4) + var(--arc-max) - var(--arc));
    z-index: 50;
    width: min(380px, 64vw);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    box-shadow: var(--shadow-2);
    color: var(--ink);
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    opacity: 0;
    visibility: hidden;
    transform: translate(calc(-50% + var(--d) * var(--step)), var(--sp-2));
    transition:
      opacity 200ms var(--fan-ease),
      transform 460ms var(--fan-ease),
      visibility 0s linear 200ms;
  }
  /* The caret. A square on its corner, half of it behind the panel, which is
     the cheapest honest arrow there is. */
  .hand-door__note::before {
    content: '';
    position: absolute;
    top: calc(var(--sp-1) * -1);
    left: 50%;
    width: var(--sp-2);
    height: var(--sp-2);
    transform: translateX(-50%) rotate(45deg);
    background: var(--panel);
    border-left: 1px solid var(--line);
    border-top: 1px solid var(--line);
  }
  .hand-door:hover .hand-door__note,
  .hand-door:focus-visible .hand-door__note {
    opacity: 1;
    visibility: visible;
    transform: translate(calc(-50% + var(--d) * var(--step)), 0);
    transition:
      opacity 200ms var(--fan-ease),
      transform 460ms var(--fan-ease),
      visibility 0s;
  }
}

/* THE NAMEPLATE. Struck into the card in the card's own foil, so a red leader
   back and a pale DON!! back each get a plate that belongs to them rather than
   a grey label dropped on top.

   THE BAND IS ALWAYS --cb-mark, and that is not a stylistic preference: the
   first version filled it with --cb-field, which on the dark-ground backs is
   38% ink on black — within a shade of --cb4, the ramp stop the bottom of the
   card is already painted in. The plate vanished into the card and only its
   hairline showed. --cb-mark is the one layer colour §6b guarantees CONTRASTS
   with the ground on every colorway, because it is what the compass rose and
   the keylines are drawn in.

   ONLY THE INK CHANGES, and only for the three DON!! backs. --cb-field reads
   near-black on the four dark-ground backs, which is about 9:1 on the foil. The
   DON!! backs invert (§6b): there --cb-mark IS the ink, so the band is dark and
   --cb-field is a 20% tint that would sit around 1.4:1 on it. They take
   --cb-paper instead, which measures 5.2:1 (op01) to 6.2:1 (op02). One token,
   one exception, and no colour named twice.

   It sits at bottom 11.5%, inset past the frame's inner keyline, which is the
   empty half of the card: §6b's seal is centred at 45% and ends around 64% of
   the height, so nothing the plate covers is part of the drawing. */
.hand-door__plate {
  position: absolute;
  left: 14%;
  right: 14%;
  bottom: 11.5%;
  z-index: 2;
  display: grid;
  place-items: center;
  padding: 3.5% 5%;
  border-radius: 3px;
  background: var(--cb-mark);
  color: var(--plate-ink, var(--cb-field));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--plate-ink, var(--cb-field)) 30%, transparent);
  font-size: max(10px, 6.4cqw);
  font-weight: 700;
  line-height: 1.16;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  text-align: center;
  /* cqw is legal HERE — .card-back is this element's ancestor container. The
     trap §6b names is a cqw written on the card itself, which resolves against
     the card's own ancestor and turned every launcher card into a pill. */
}
.card-back--don .hand-door__plate,
.card-back--don-op01 .hand-door__plate,
.card-back--don-op02 .hand-door__plate {
  --plate-ink: var(--cb-paper);
}

.hand-door__note {
  font-size: var(--fs-xs);
  line-height: var(--lh-snug);
  color: var(--dim);
  text-align: center;
  max-width: none;
}
.hand-door:hover .hand-door__note { color: var(--ink); }

/* REDUCED MOTION. §10 above blanket-shortens every animation to 0.001ms with a
   single iteration, which for a drift means the ship TELEPORTS to the far end
   of its keyframe and parks there — the reader asked for no motion and got a
   different picture instead. Switching the animation off entirely is the only
   version of "off" that leaves the scene as drawn.

   THE FAN IS SWITCHED OFF, NOT SLOWED. Dropping the transitions alone would
   leave the lift, the pop and the straighten as instant jumps — still motion,
   just motion nobody can follow. So the four properties that DRIVE the hover
   are pinned at their resting values with !important, which beats the :hover
   rules whatever their specificity, and the read card changes by ring and
   caption alone. The caption still appears, instantly: reduced motion is a
   request about movement, never about information. */
@media (prefers-reduced-motion: reduce) {
  .seascape__ship,
  .seascape__hull {
    animation: none !important;
    transform: none !important;
  }
  .hand-door {
    --lift: 0px !important;
    --pop: 1 !important;
    --straighten: 0deg !important;
    --yield: 0 !important;
  }
  .hand-door,
  .hand-door__card,
  .hand-door__note { transition: none !important; }
}

/* HIGH CONTRAST. --line and --dim both become --ink, so the hairline sea would
   be drawn at full ink strength directly behind the hero. A reader who asked
   for more contrast is asking for less of exactly this. */
@media (prefers-contrast: more) {
  .seascape { display: none; }
}

/* ---------------------------------------------------------------------------
   11b. THE SEA MUSIC CONTROL

   docs/index.html generates a calm 6/8 in D dorian with the Web Audio API —
   oscillators and envelopes, no file. This is the switch, and it is a switch
   and not an ornament: it is OFF on arrival, always, and its label says which
   way it is pointing. The page never makes a sound the reader did not ask for,
   which is also the only behaviour a browser's autoplay policy will allow.

   Hidden until the script that drives it has wired it up: a music button on a
   page whose script did not run (file://, an old browser, a blocked module) is
   a control that lies. index.html ships it with the `hidden` attribute and the
   script removes it.
   --------------------------------------------------------------------------- */
.sea-audio {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2) var(--sp-3);
  margin-top: var(--sp-5);
}
.sea-audio__btn .glyph {
  font-size: var(--fs-lg);
  line-height: 1;
  color: var(--accent);
}
/* Playing is a state, so it is drawn as one — aria-pressed is the source of
   truth for both the reader and the stylesheet, and there is no second flag to
   fall out of step with it. */
.sea-audio__btn[aria-pressed="true"] {
  border-color: var(--accent-line);
  background: var(--accent-soft);
}
.sea-audio__note {
  font-size: var(--fs-xs);
  color: var(--dim);
  max-width: 46ch;
}

/* ---------------------------------------------------------------------------
   DEV MODE — the depth, revealed
   ---------------------------------------------------------------------------
   The switch half of this shipped without the lock. docs/assets/site.js already
   resolves dev mode (`?dev`, `optcg.dev`, or the purple card's
   `optcg.launch.purple`) and stamps `data-dev` on <html>, and its header even
   quotes the two rules below as the CSS a page needs — but they were never
   written into this file, and no page marked anything `.dev-only`.

   That is the FAIL-OPEN direction: with the switch wired and the lock missing,
   the first `class="dev-only"` anyone adds is visible to everybody, forever,
   and it looks like it works. Writing the rules makes the mechanism real before
   there is markup depending on it.

   `display: revert`, not `block` — `.dev-only` goes on <tr>, <li> and <span>
   as readily as on a <div>, and forcing `block` would flatten a table row.

   WHAT THIS MUST NEVER HIDE (site.js says the same thing at greater length):
   a sample size beside a rate, a "this price is inherited from another
   printing" note, or a "the engine cannot execute this ability" badge. Dev mode
   gates the EXPLANATION, never the CAVEAT. A reader who has not found the
   purple card is still owed every reason to distrust a number; what they are
   spared is the paragraph on why it is computed that way.
   --------------------------------------------------------------------------- */

.dev-only {
  display: none;
}

:root[data-dev] .dev-only {
  display: revert;
}

/* ---------------------------------------------------------------------------
   TOUCH AND WRAPPING — what a phone needs that a mouse does not
   ---------------------------------------------------------------------------
   Both of these came out of a per-page audit that reached the same two
   conclusions from opposite ends of the site, so they are fixed once here
   rather than seven times in seven page-scoped blocks.

   WRAPPING. Only h1-h6 carried `overflow-wrap` (above). Body copy on these
   pages interpolates card ids, clause ids like `7-1-4-1-1-2`, file paths and
   error messages, and one unbreakable token sets a min-content width wider than
   a 375px phone. `html`/`body` are `overflow-x: clip`, so that overflow does not
   become a scrollbar the reader can chase — it is silently CUT OFF, which is the
   worst of the three options. `anywhere` on code/samp because a path has no
   spaces to break at; `break-word` on prose so ordinary words still wrap
   normally.

   TOUCH TARGETS. `.btn` computes to roughly 36px tall and `.input` about the
   same — fine for a cursor, under the ~44px that a finger wants. Gated on
   `pointer: coarse` rather than a width breakpoint, so a narrow desktop window
   keeps its density and a large tablet still gets the bigger targets: the thing
   that changed is the input device, not the viewport.

   `.btn.sm` and `.chip` are deliberately NOT raised. They exist in dense
   toolbars — the card browser and the play board each render dozens at once —
   and forcing 44px there would re-lay-out those pages rather than improve them.
   Both still clear the 24px WCAG 2.5.8 AA minimum.
   --------------------------------------------------------------------------- */

p, li, dd, dt, td, th, figcaption, blockquote {
  overflow-wrap: break-word;
}

code, samp, .mono {
  overflow-wrap: anywhere;
}

@media (pointer: coarse) {
  .btn:not(.sm) {
    min-height: 44px;
  }

  .input:not(textarea) {
    min-height: 44px;
  }

  /* THE NAV IS THE MOST-TAPPED THING ON THE SITE and was the smallest: the bar
     is about 52px tall but each link only claimed 28px of it, so two thirds of
     what looks like a target is padding belonging to the <li>. Measured on the
     live home page at 375px - all seven came back 28px.
     Flex rather than padding, so the label stays vertically centred in the row
     it now fills. */
  .site-nav-links a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
}
