/* ==========================================================================
   Theme Gallery · base.css
   --------------------------------------------------------------------------
   1. The token contract every theme overrides (neutral defaults).
   2. Every component, written against tokens only.
   Themes live in css/themes/<id>.css as one [data-theme="<id>"] block each:
   first the tokens, then rules for whatever tokens cannot express (layout,
   casing, pseudo-element decoration). See docs/README.md.
   ========================================================================== */

/* ---------- fonts (all SIL OFL, bundled so every theme works offline) ---------- */
@font-face { font-family: "Fraunces"; src: url("../fonts/fraunces.woff2") format("woff2"); font-weight: 300 900; font-display: swap; }
@font-face { font-family: "Jost"; src: url("../fonts/jost.woff2") format("woff2"); font-weight: 300 800; font-display: swap; }
@font-face { font-family: "Space Grotesk"; src: url("../fonts/space-grotesk.woff2") format("woff2"); font-weight: 300 700; font-display: swap; }
@font-face { font-family: "IBM Plex Mono"; src: url("../fonts/plex-mono-400.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "IBM Plex Mono"; src: url("../fonts/plex-mono-600.woff2") format("woff2"); font-weight: 600; font-display: swap; }
@font-face { font-family: "Silkscreen"; src: url("../fonts/silkscreen-400.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "Silkscreen"; src: url("../fonts/silkscreen-700.woff2") format("woff2"); font-weight: 700; font-display: swap; }
@font-face { font-family: "Zen Kaku Gothic New"; src: url("../fonts/zen-kaku-400.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "Zen Kaku Gothic New"; src: url("../fonts/zen-kaku-700.woff2") format("woff2"); font-weight: 700; font-display: swap; }
@font-face { font-family: "Shippori Mincho"; src: url("../fonts/shippori-mincho-700.woff2") format("woff2"); font-weight: 400 800; font-display: swap; }
@font-face { font-family: "Bricolage Grotesque"; src: url("../fonts/bricolage-grotesque.woff2") format("woff2"); font-weight: 400 800; font-stretch: 75% 100%; font-display: swap; }

/* ---------- 1. tokens (defaults) ---------- */
:root {
  color-scheme: light;
  /* surfaces and ink */
  --bg: #f5f6fb;
  --surface: #ffffff;
  --surface-2: #eceef7;
  --text: #1b1d2e;
  --muted: #6a6f8e;
  --faint: #8d91ab;
  --border: #dfe2ef;
  --rule: var(--border);             /* structural lines; editorial themes make them ink */
  /* the one accent, and the button colours derived from it */
  --accent: #5b5bd6;
  --primary: #5b5bd6;
  --primary-strong: #3f3fc0;
  --primary-contrast: #ffffff;
  --primary-soft: #e6e6fc;
  --danger: #dc3e42;
  --danger-soft: #fde8e8;
  --danger-contrast: #1a0204;        /* text on a --danger fill (a wrong answer) */
  --success: #2b9a66;
  --success-contrast: #03140d;       /* text on a --success fill (a right answer) */
  --warn: #a15c00;                   /* amber: something is on its way, or worth a look. Text-safe on --bg and --surface */
  --warn-soft: #fdf0d5;
  /* shape: every corner in the app reads one of these */
  --radius: 16px;                    /* cards, panels, banners */
  --ctl-h: 40px;                     /* the one height of every button, icon button and segmented control */
  --r-ctl: 12px;                     /* buttons, inputs, segmented */
  --r-ctl-in: 9px;                   /* the pressed segment inside a control */
  --r-dialog: 22px;
  --r-menu: 16px;
  --r-board: 10px;
  --r-cell: 8px;
  --r-pill: 999px;                   /* badges, switches, counts */
  --bw: 1px;                         /* default border width */
  /* the reading column, a theme decision. Work pages (several panels used side by
     side) take --work-w instead, the same in every theme: usability outranks a
     theme's proportions (docs §0, §2.7). .view, the footer's inner and the veil read
     --page-w, the column the current page uses, so they always stand in one column. */
  --content-w: 1040px;
  --work-w: 1480px;
  --page-w: var(--content-w);
  --side-w: 340px;                   /* the side panel of a .work layout */
  --toolbar-h: 64px;                 /* the bottom .toolbar, without the safe-area inset */
  --frame-out: 0px;                  /* the room a frame drawn outside a box needs (Arcade: 8px) */
  /* motion: one curve and two durations, so everything that moves agrees.
     --t-fast is a colour change under the pointer, --t-slow a panel arriving.
     --fade is the property list every hover uses; a component that also moves
     writes its own transform ahead of it (see .btn). */
  --ease: cubic-bezier(.2, .7, .3, 1);
  --t-fast: .13s;
  --t-slow: .26s;
  --t-move: .18s;                    /* a game piece that moves, an answer's pop or shake (§2.7) */
  --fade: background-color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
          color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease),
          outline-color var(--t-fast) var(--ease);
  /* depth */
  --shadow: 0 1px 2px rgba(20, 22, 50, .05), 0 4px 16px rgba(20, 22, 50, .06);
  /* a panel's edge as one box-shadow value: what a small panel drawn on a game surface (a
     callout on the card table, a pay table) wears to read as the theme's own panel */
  --panel-edge: 0 0 0 1px var(--border);
  --menu-shadow: 0 16px 48px rgba(10, 12, 30, .22);
  --dialog-shadow: 0 20px 60px rgba(0, 0, 0, .3);
  --overlay: rgba(10, 12, 30, .45);
  /* scrollbars: mixed from --muted, so a theme that sets its ink gets a matching
     bar without writing a rule. Override either one to change it on purpose. */
  --scroll-thumb: color-mix(in srgb, var(--muted) 32%, transparent);
  --scroll-thumb-hover: color-mix(in srgb, var(--muted) 58%, transparent);
  /* type */
  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: var(--font);
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  /* toast & record badge */
  --toast-bg: var(--text);
  --toast-fg: var(--bg);
  --toast-action: var(--primary-soft);
  --record-bg: #fff3c4;
  --record-fg: #8a5a00;
  /* a win: the colours of the .celebration pieces, a comma list read by the script that throws them */
  --confetti: #f59e0b, #e5484d, #30a46c, #0090ff, #8e4ec6, #f76b15;
  /* sides: the players of a game, each with the ink that reads on it (4.5:1). --side-1 is
     always the player - the theme's own colour for "you" - and 2 to 6 are the rivals */
  --side-1: var(--primary); --side-1-ink: var(--primary-contrast);
  --side-2: #dc3e42; --side-2-ink: #1a0204;
  --side-3: #2b9a66; --side-3-ink: #03140d;
  --side-4: #e8b100; --side-4-ink: #1b1d2e;
  --side-5: #0b6fd4; --side-5-ink: #ffffff;
  --side-6: #f76b15; --side-6-ink: #1b1d2e;
  /* game pieces: the 2048 tile board */
  --g-board: #bbada0;
  --g-slot: rgba(238, 228, 218, .35);
  --t2: #eee4da; --t4: #ede0c8; --t8: #f2b179; --t16: #f59563; --t32: #f67c5f; --t64: #f65e3b;
  --t128: #edcf72; --t256: #edcc61; --t512: #edc850; --t1024: #edc53f; --t2048: #edc22e;
  --t-ink: #776e65; --t-paper: #f9f6f2;
  /* game pieces: the memory grid and number pad */
  --flash: var(--primary);
  --tile: var(--surface-2);
  /* game pieces: the card table and playing cards */
  --table-bg: radial-gradient(130% 100% at 50% 0%, #2a5f48, #1f4a38 50%, #143226);
  --table-frame: none;
  --slot-line: rgba(255, 255, 255, .16);
  /* text drawn straight on the table: names and scores, the figures that matter, the asides,
     and the mark of whose turn it is */
  --table-ink: rgba(255, 255, 255, .86);
  --table-ink-strong: #ffffff;
  --table-ink-quiet: rgba(255, 255, 255, .62);
  --table-turn: #ffd54a;
  --card-paper: #fbf8f1;
  --card-r: .085;                    /* corner radius as a fraction of the card width */
  --card-shadow: 0 0 0 1px rgba(20, 16, 8, .28), 0 1px 2px rgba(0, 0, 0, .28);
  --court-panel: color-mix(in srgb, #d6ad5c 16%, var(--card-paper));
  --court-rule: inset 0 0 0 1px color-mix(in srgb, currentColor 35%, transparent);
  --suit-red: #b4231c;
  --suit-black: #1a1d1b;
  --suit-blue: #1f55a6;              /* four-colour suits: diamonds ([data-four-color="on"]) */
  --suit-green: #1d7a3e;             /* four-colour suits: clubs */
  --card-back-art:
    repeating-linear-gradient(45deg, rgba(255, 255, 255, .09) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(-45deg, rgba(255, 255, 255, .09) 0 1px, transparent 1px 5px),
    radial-gradient(circle at 50% 40%, #93303f, #7b2432);
  --back-edge: var(--card-paper);
  --back-edge-w: .045;               /* multiplied by --cw in the rule, see .back */
  --back-shadow: 0 0 0 1px rgba(0, 0, 0, .35), 0 1px 2px rgba(0, 0, 0, .3);
  --font-card: var(--font-display);
}

/* ---------- 2. base ---------- */
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; scroll-padding-top: 90px; }
body {
  margin: 0;
  min-height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  -webkit-tap-highlight-color: transparent;
}
button, input, select { font: inherit; color: inherit; }
a { color: inherit; }
button:focus-visible, a:focus-visible, input:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* Hover and focus are colour changes, so they are eased rather than cut: every control
   below carries `transition: var(--fade)` in its own rule, on the same 130 ms, whatever
   a theme paints it with. Nothing there moves - the size rule (§4.11) still holds; a
   control that does move writes its own transform ahead of it, as .btn does.
   This is deliberately not one shared :is() list: the most specific argument would set
   the whole list's specificity (docs/README.md §5) and silently beat the per-component
   rules that follow. */
/* A scrollbar is chrome, not content: a thin bar in the theme's own ink, on no
   track at all. The standard properties are what Chrome and Firefox read; the
   -webkit- block below is for Safari, which still has only the old ones. */
* { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-button { display: none; }
::-webkit-scrollbar-thumb {
  /* The transparent border plus padding-box clip is what makes the thumb float
     in the middle of the track instead of filling it edge to edge. */
  border: 3px solid transparent; border-radius: var(--r-pill);
  background: var(--scroll-thumb); background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: padding-box; }

.ico { width: 22px; height: 22px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* Line symbols carry a heavier Horizon redraw next to the normal drawing; Horizon shows its own. */
:root:not([data-theme="horizon"]) .sym-hz, [data-theme="horizon"] .sym-app { display: none; }

/* ---------- top bar ---------- */
.topbar {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: 6px;
  min-height: 56px;
  padding: calc(env(safe-area-inset-top) + 8px) max(12px, env(safe-area-inset-right)) 8px max(12px, env(safe-area-inset-left));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}
.brand {
  display: flex; align-items: center; gap: 10px;
  min-width: 0; margin: 0 4px;
  color: var(--text); text-decoration: none;
  font-family: var(--font-display); font-size: 1.15rem; font-weight: 750; letter-spacing: -.01em;
  white-space: nowrap;
}
.brand:hover { color: var(--primary-strong); }
.brand-text { overflow: hidden; text-overflow: ellipsis; }
/* The brand is the page's one <h1> on every page (§4.2): it names the product, and every
   other heading on the page starts at <h2>. Reset here, so the bar looks as before. */
h1.brand-text { margin: 0; font: inherit; }
/* The inner page's name, after the brand, in a product without navigation (§4.2). It
   takes the room the bar has left and no more: a name that does not fit is cut, and
   never pushes the brand or wraps the bar. A <span>, or an <h2> when the page has no other
   heading for its name. */
.page-name {
  flex: 1 1 0; min-width: 0; margin: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--muted); font-size: .9rem; font-weight: 600;
}
.page-name::before { content: "/"; margin-right: 8px; color: var(--faint); }
/* the four-square brand mark; only the themes that draw it switch it on */
.brand-mark { display: none; flex: none; width: 30px; height: 30px; padding: 6px; place-items: center; }
.brand-mark svg { width: 100%; height: 100%; fill: currentColor; }
.icon-btn {
  flex: none;
  width: var(--ctl-h); height: var(--ctl-h);
  display: grid; place-items: center;
  padding: 0;
  border: 0; border-radius: var(--r-ctl);
  background: transparent; color: var(--text);
  cursor: pointer;
  transition: var(--fade);
}
.icon-btn:hover:not(:disabled) { background: var(--surface-2); }
.icon-btn:disabled { opacity: .4; cursor: default; }
.topbar-end { display: flex; align-items: center; gap: 6px; margin-left: auto; }
/* The way to the documentation, from every page of the style guide: a .btn, so --ctl-h tall. */
.docs-link { padding: 0 12px; font-size: .88rem; }
.docs-link .ico { width: 18px; height: 18px; }
@media (max-width: 680px) { .docs-link span { display: none; } .docs-link { padding: 0 10px; } }

/* navigation */
.main-nav { display: flex; gap: 2px; margin-left: 12px; }
.main-nav a {
  padding: 8px 12px;
  border-radius: var(--r-ctl);
  color: var(--muted); text-decoration: none;
  font-size: .9rem; font-weight: 600;
  white-space: nowrap;
  transition: var(--fade);
}
.main-nav a:hover { background: var(--surface-2); color: var(--text); }
.main-nav a[aria-current="true"] { background: var(--primary-soft); color: var(--primary-strong); }
@media (max-width: 900px) {
  .topbar { flex-wrap: wrap; row-gap: 4px; }
  .main-nav { order: 5; flex-basis: 100%; margin: 0; overflow-x: auto; scrollbar-width: none; }
  /* The row scrolls, and says so: the edge it continues past fades out over 36px, so a cut
     item reads as "more this way" instead of a mistake. js/app.js keeps data-overflow in
     step with the scroll position: "end", "start", "start end" or absent. */
  .main-nav[data-overflow~="end"] { -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 36px), transparent); mask-image: linear-gradient(90deg, #000 calc(100% - 36px), transparent); }
  .main-nav[data-overflow~="start"] { -webkit-mask-image: linear-gradient(90deg, transparent, #000 36px); mask-image: linear-gradient(90deg, transparent, #000 36px); }
  .main-nav[data-overflow="start end"] { -webkit-mask-image: linear-gradient(90deg, transparent, #000 36px, #000 calc(100% - 36px), transparent); mask-image: linear-gradient(90deg, transparent, #000 36px, #000 calc(100% - 36px), transparent); }
}

/* Stats in the top bar: on a full-screen game route (body.app-full) the game's stats stand
   in the bar, between the brand and .topbar-end, instead of in a .bar that costs the table a
   row. <div class="stats topbar-stats">, the usual .stat chips, made to fit the bar's 40px.
   Under 560px they take a row of their own under the brand. */
.topbar-stats { display: flex; flex-wrap: nowrap; gap: 6px; margin-left: auto; min-width: 0; }
.topbar-stats + .topbar-end { margin-left: 6px; }
.topbar-stats .stat { min-width: 50px; padding: 1px 8px; }
.topbar-stats .stat-label { font-size: .58rem; }
.topbar-stats .stat-value { font-size: .95rem; line-height: 1.2; }
@media (max-width: 560px) {
  .topbar-stats { order: 4; flex-basis: 100%; justify-content: center; margin: 0; }
  .topbar-stats + .topbar-end { margin-left: auto; }
}

/* language toggle = segmented control in the bar */
.topbar .lang-toggle button { padding: 0 9px; letter-spacing: .04em; }

/* theme picker */
.theme-picker { position: relative; }
.theme-btn {
  display: inline-flex; align-items: center; gap: 8px;
  height: var(--ctl-h); padding: 0 8px 0 6px;
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  background: var(--surface);
  font-size: .88rem; font-weight: 600;
  cursor: pointer;
  transition: var(--fade);
}
.theme-btn:hover { background: var(--surface-2); }
.theme-btn .theme-swatch { width: 28px; height: 28px; border-radius: 999px; }
.theme-btn .theme-swatch b { font-size: 11px; }
.theme-btn .theme-swatch i { display: none; }
.theme-btn .chev { width: 16px; height: 16px; color: var(--muted); transition: transform var(--t-fast) var(--ease); }
.theme-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
@media (max-width: 560px) { .theme-btn .theme-name { display: none; } }
/* The swatch is a small sample of its theme, painted in that theme's colours, typeface
   and signature mark whatever theme is active, so the menu reads as samples.
   Values per theme: docs/README.md §2.6. */
.theme-swatch {
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  width: 48px;
  height: 36px;
  overflow: hidden;
  border-radius: 9px;
  border: 1px solid rgba(128, 128, 128, .35);
  font-weight: 700;
  font-style: normal;
  line-height: 1;
  letter-spacing: 0;
  text-transform: none;
}
.theme-swatch b { font-size: 16px; font-weight: inherit; line-height: 1; }
.theme-swatch i { position: absolute; left: 6px; right: 6px; bottom: 5px; height: 3px; border-radius: 2px; }
.theme-swatch[data-swatch="cyberpunk"] {
  background: radial-gradient(120% 90% at 50% 110%, #3a0f47, #0a0614 70%);
  color: #fff4fb;
  border-color: #ff2e88;
  border-radius: 0;
  font-family: "Bricolage Grotesque", "Arial Narrow", sans-serif;
  font-stretch: 75%;
  font-weight: 800;
  text-shadow: -1px 0 #ff2e88, 1px 0 #2de2ff;
}
.theme-swatch[data-swatch="cyberpunk"] i { height: 2px; border-radius: 0; background: linear-gradient(90deg, #ff2e88 0 50%, #2de2ff 0); box-shadow: 0 0 5px #ff2e88; }
.theme-swatch[data-swatch="washi"] {
  background: #f1ece1;
  color: #262421;
  border-color: #d8cfbf;
  border-radius: 3px;
  font-family: "Shippori Mincho", "Hiragino Mincho ProN", Georgia, serif;
}
.theme-swatch[data-swatch="washi"] i { left: auto; right: 5px; bottom: auto; top: 5px; width: 7px; height: 7px; border-radius: 1px; background: #c6412a; transform: rotate(-3deg); }
.theme-swatch[data-swatch="broadsheet"] {
  background: #f2ede2;
  color: #161513;
  border-color: #161513;
  border-radius: 0;
  font-family: "Fraunces", Georgia, serif;
  font-weight: 900;
  letter-spacing: -.03em;
}
.theme-swatch[data-swatch="broadsheet"] i { left: 0; right: 0; bottom: 0; height: 22%; border-radius: 0; background: #b3261e; }
.theme-swatch[data-swatch="bauhaus"] {
  background: #efe8d8;
  color: #121212;
  border: 2px solid #121212;
  border-radius: 0;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-weight: 800;
  letter-spacing: -.04em;
}
.theme-swatch[data-swatch="bauhaus"] b { text-transform: lowercase; }
.theme-swatch[data-swatch="bauhaus"] i { left: 0; right: 0; bottom: 0; height: 5px; border-radius: 0; background: linear-gradient(90deg, #d7261e 0 33.3%, #f4b400 0 66.6%, #1e4fa3 0); }
.theme-swatch[data-swatch="pop"] {
  background: #ffd23f;
  color: #111;
  border: 2px solid #111;
  border-radius: 10px;
  font-family: "Bricolage Grotesque", system-ui, sans-serif;
  font-weight: 800;
}
.theme-swatch[data-swatch="pop"] i { background: #ff5c8a; height: 5px; border: 1.5px solid #111; }
.theme-swatch[data-swatch="horizon"] {
  background: linear-gradient(180deg, #fff 55%, #e9eef4);
  color: #17181c;
  border-color: #b9c3cf;
  border-radius: 4px;
  font-family: Frutiger, "Frutiger Linotype", "Segoe UI", "Noto Sans", "Helvetica Neue", Arial, sans-serif;
}
.theme-swatch[data-swatch="horizon"] i { left: 0; right: 0; bottom: 0; height: 4px; border-radius: 0; background: #1f5fb4; }
.theme-swatch[data-swatch="blueprint"] {
  background:
    linear-gradient(rgba(207, 224, 255, .2) 1px, transparent 1px),
    linear-gradient(90deg, rgba(207, 224, 255, .2) 1px, transparent 1px),
    #0d3466;
  background-size: 8px 8px;
  background-position: -1px -1px;
  color: #eaf2ff;
  border-color: #cfe0ff;
  border-radius: 2px;
  font-family: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
  letter-spacing: -.03em;
}
.theme-swatch[data-swatch="blueprint"] i { height: 2px; border-radius: 0; background: #ffd23f; }
.theme-swatch[data-swatch="arcade"] {
  background: #1d2b53;
  color: #fff1e8;
  border: 2px solid #000;
  border-radius: 0;
  font-family: "Silkscreen", monospace;
  font-weight: 400;
}
.theme-swatch[data-swatch="arcade"] b { font-size: 14px; }
.theme-swatch[data-swatch="arcade"] i { height: 4px; border-radius: 0; background: repeating-linear-gradient(to right, #ffec27 0 4px, transparent 4px 8px); }
.theme-menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 40;
  width: min(340px, calc(100vw - 24px));
  max-height: calc(100dvh - 90px); overflow: auto;
  margin: 0; padding: 6px;
  list-style: none;
  border: 1px solid var(--border); border-radius: var(--r-menu);
  background: var(--surface); color: var(--text);
  box-shadow: var(--menu-shadow);
  animation: menu-in var(--t-fast) var(--ease);
}
.theme-opt {
  display: grid; grid-template-columns: auto 1fr 18px; align-items: center; gap: 12px;
  width: 100%; padding: 8px;
  border: 0; border-radius: var(--r-ctl-in);
  background: none; text-align: left;
  cursor: pointer;
  transition: var(--fade);
}
.theme-opt:hover, .theme-opt:focus-visible { background: var(--surface-2); outline: none; }
.theme-opt:focus-visible { box-shadow: inset 0 0 0 2px var(--primary); }
.theme-opt strong { display: block; font-family: var(--opt-font); font-size: .98rem; font-weight: 700; line-height: 1.2; }
.theme-opt small { display: block; margin-top: 2px; font-size: .78rem; line-height: 1.3; color: var(--muted); }
.theme-opt .ico { width: 18px; height: 18px; color: var(--primary-strong); visibility: hidden; }
.theme-opt[aria-checked="true"] .ico { visibility: visible; }
/* The menu is the style guide's index: the rows arrive one after another, 18 ms apart,
   so it reads as a list of samples being laid out rather than a block appearing.
   --i is the row's index, set in js/app.js; the whole cascade is under 200 ms. */
.theme-menu .theme-opt { animation: opt-in var(--t-slow) var(--ease) backwards; animation-delay: calc(var(--i, 0) * 18ms); }
@keyframes menu-in { from { opacity: 0; } }
@keyframes opt-in { from { opacity: 0; transform: translateY(-5px); } }

/* ---------- buttons & controls ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: var(--ctl-h); padding: 0 16px;
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  background: var(--surface); color: var(--text);
  font-weight: 650; text-decoration: none;
  cursor: pointer; white-space: nowrap;
  transition: transform .08s var(--ease), var(--fade);
}
.btn:hover:not(:disabled) { background: var(--surface-2); }
.btn:active { transform: scale(.96); }
.btn:disabled { opacity: .45; cursor: default; transform: none; }
.btn .ico { width: 18px; height: 18px; }
.btn-primary { background: var(--primary); border-color: transparent; color: var(--primary-contrast); }
.btn-primary:hover:not(:disabled) { background: var(--primary-strong); }
.btn-danger { color: var(--danger); }
.btn-danger:hover:not(:disabled) { background: var(--danger-soft); }

/* ---------- links ----------
   A link in running text - inside .prose: a page of text, the docs reader, a guide - is
   underlined in --primary-strong. A link dressed as a button (.btn, .icon-btn) is that
   button and nothing else: the :not() keeps this rule off it entirely, so each theme's own
   button rule applies untouched, and :where() keeps the rule at (0,1,1) so a page's own
   link rules can still refine it. Never style `a` in a container without that exclusion. */
.prose a:where(:not(.btn, .icon-btn)) { color: var(--primary-strong); text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1px; }
.prose a:where(:not(.btn, .icon-btn)):hover { text-decoration-thickness: 2px; }

.segmented {
  display: inline-flex; align-items: stretch;
  height: var(--ctl-h);
  padding: 3px;
  background: var(--surface-2);
  border-radius: var(--r-ctl);
}
.segmented button {
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent;
  padding: 0 12px; border-radius: var(--r-ctl-in);
  font-size: .9rem; font-weight: 600; color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
  transition: var(--fade);
}
.segmented button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: var(--shadow); }
/* the unpressed options have to look pressable; the :not() also lifts this over a theme's resting rule */
.segmented button:hover:not([aria-pressed="true"]) { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text); }

.stats { display: flex; gap: 8px; flex-wrap: wrap; }
.stat {
  display: flex; flex-direction: column; align-items: center;
  min-width: 62px; padding: 3px 10px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-ctl);
}
.stat-label { font-size: .62rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
.stat-value { font-family: var(--font-display); font-size: 1.05rem; font-weight: 750; font-variant-numeric: tabular-nums; }

.tools { display: flex; justify-content: center; align-items: stretch; gap: 4px; }
/* Tools stretch to the tallest of the row and start at the top, so a label that wraps
   (How to play, in the mono faces) keeps every icon on one line and every pressed fill
   the same height; the label is centred and tight. */
.tool {
  position: relative;
  flex: 1; max-width: 104px;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 3px;
  padding: 7px 4px;
  text-align: center; line-height: 1.15;
  border: 0; border-radius: var(--r-ctl);
  background: transparent; color: var(--muted);
  font-size: .75rem; font-weight: 650;
  cursor: pointer;
  transition: var(--fade);
}
.tool .ico { width: 24px; height: 24px; }
.tool:hover:not(:disabled) { background: var(--surface-2); }
.tool:active:not(:disabled) { transform: scale(.95); }
.tool[aria-pressed="true"] { background: var(--primary-soft); color: var(--primary-strong); }
.tool:disabled { opacity: .4; cursor: default; }
.tool .badge {
  position: absolute; top: 2px; right: calc(50% - 22px);
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 8px; background: var(--primary); color: var(--primary-contrast);
  font-size: .65rem; line-height: 16px;
}

.record { display: inline-block; padding: 4px 10px; border-radius: var(--r-pill); background: var(--record-bg); color: var(--record-fg); font-weight: 700; font-size: .85rem; }

/* form controls */
/* .field is the one box for anything typed or picked: text, search, number, colour, a
   select and a textarea all share its border, fill, hover and focus - and every theme's
   .field rule, which is written against :is(input, select, textarea). */
.field { display: grid; gap: 6px; }
.field label { font-size: .82rem; font-weight: 650; color: var(--muted); }
.field :is(input, select, textarea) {
  height: 42px; padding: 0 12px;
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  background: var(--surface); color: var(--text);
  font: inherit;
  transition: var(--fade);
}
.field :is(input, textarea)::placeholder { color: var(--faint); }
.field :is(input, select, textarea):hover:not(:focus-visible) { border-color: var(--muted); }
.field :is(input, select, textarea):focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
/* a textarea starts at two to three lines (§4.15) and only grows by hand */
.field textarea { height: auto; min-height: calc(2.6em + 20px); padding: 10px 12px; line-height: 1.5; resize: vertical; }
/* a select draws its own chevron, in the theme's ink, so the native one does not break the face */
.select { position: relative; display: grid; }
.select select { appearance: none; -webkit-appearance: none; padding-right: 38px; cursor: pointer; }
.select::after {
  content: ""; position: absolute; right: 15px; top: calc(50% - 6px);
  width: 8px; height: 8px;
  border: solid var(--muted); border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  pointer-events: none;
}
/* search: the icon sits in the field, the field keeps the room for it */
.search { position: relative; display: grid; }
.search .ico { position: absolute; left: 12px; top: calc(50% - 9px); width: 18px; height: 18px; color: var(--faint); pointer-events: none; }
.search input { padding-left: 38px; }
.search input::-webkit-search-decoration, .search input::-webkit-search-cancel-button { -webkit-appearance: none; }
/* number stepper: a field between two icon buttons, so each theme's button and field rules dress it */
.stepper { display: inline-flex; align-items: center; gap: 4px; }
.stepper input { width: 4.5ch; min-width: 56px; padding: 0 4px; text-align: center; font-variant-numeric: tabular-nums; -moz-appearance: textfield; }
.stepper input::-webkit-outer-spin-button, .stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.stepper .icon-btn { border: 1px solid var(--border); font-size: 1.2rem; font-weight: 600; line-height: 1; }
/* colour: a swatch in a field-shaped box */
.field input[type="color"] { width: 64px; padding: 4px 6px; cursor: pointer; }
.field input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.field input[type="color"]::-webkit-color-swatch, .field input[type="color"]::-moz-color-swatch { border: 0; border-radius: calc(var(--r-ctl) - 3px); }
/* an invalid value: danger border, and a line under the field that says what is wrong.
   The line is text that carries meaning, so it is --danger, never --faint. */
.field :is(input, select, textarea)[aria-invalid="true"] { border-color: var(--danger); }
.field :is(input, select, textarea)[aria-invalid="true"]:focus-visible { outline-color: var(--danger); }
.field-error { margin: 0; font-size: .8rem; font-weight: 600; color: var(--danger); }
/* a hint under a field: repeats or qualifies the label, so it may be --muted */
.field-hint { margin: 0; font-size: .8rem; color: var(--muted); }

.switch {
  position: relative; flex: none;
  width: 46px; height: 28px; padding: 0;
  border: 0; border-radius: var(--r-pill);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
  cursor: pointer;
  transition: var(--fade);
}
.switch::before {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 22px; height: 22px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
  transition: transform var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.switch[aria-checked="true"] { background: var(--primary); box-shadow: none; }
.switch[aria-checked="true"]::before { transform: translateX(18px); }
/* hover halo for the small form controls: themes draw their tracks with box-shadow, so the
   halo uses outline, which none of them set (keyboard focus replaces it with the solid ring) */
.switch:hover:not(:disabled, :focus-visible), .check:hover input:not(:focus-visible), .range:hover:not(:focus-visible) { outline: 2px solid color-mix(in srgb, var(--primary) 45%, transparent); outline-offset: 2px; }

.check { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; font-size: .95rem; }
.check input { width: 20px; height: 20px; margin: 0; accent-color: var(--primary); }

.range { width: 100%; accent-color: var(--primary); cursor: pointer; }

/* ---------- reading text ----------
   .prose is a page of text: a rules page, help, a guide, the docs reader's body. Headings
   in the display face, generous line-height, lists, a quote, inline code and keys. The docs
   reader's own rules (docs.css) refine these at the same specificity, loading later. */
.prose { line-height: 1.6; }
.prose :is(h1, h2, h3, h4) { margin: 0 0 .5em; font-family: var(--font-display); letter-spacing: -.01em; line-height: 1.2; }
.prose * + :is(h2, h3, h4) { margin-top: 1.4em; }
.prose h1 { font-size: 1.8rem; } .prose h2 { font-size: 1.35rem; } .prose h3 { font-size: 1.1rem; } .prose h4 { font-size: 1rem; }
.prose p, .prose ul, .prose ol { margin: 0 0 .9em; }
.prose :is(ul, ol) { padding-left: 1.3em; }
.prose li + li { margin-top: .3em; }
.prose blockquote { margin: 0 0 .9em; padding: 2px 0 2px 16px; border-left: 3px solid var(--rule); color: var(--muted); }
.prose :not(pre) > code { padding: .1em .4em; border-radius: var(--r-ctl-in); background: var(--surface-2); font-family: var(--font-mono); font-size: .88em; }
.prose kbd {
  display: inline-block; padding: .05em .5em;
  border: 1px solid var(--border); border-bottom-width: 2px; border-radius: var(--r-ctl-in);
  background: var(--surface); font-family: var(--font-mono); font-size: .82em; line-height: 1.5;
}
.prose hr { margin: 1.4em 0; border: 0; border-top: 1px solid var(--rule); }
.prose > :last-child { margin-bottom: 0; }

/* ---------- tabs ----------
   One row of panes, one open: a control-height strip on a rule, the open tab marked by a
   2px --primary edge on that rule. For three choices of one value use .segmented; tabs are
   for panes of content. */
.tabs { display: flex; gap: 2px; margin: 0; padding: 0; border-bottom: 1px solid var(--rule); overflow-x: auto; scrollbar-width: none; }
.tabs button {
  flex: none;
  display: inline-flex; align-items: center; gap: 6px;
  height: var(--ctl-h); margin-bottom: -1px; padding: 0 14px;
  border: 0; border-bottom: 2px solid transparent;
  background: none; color: var(--muted);
  font-weight: 600; white-space: nowrap;
  cursor: pointer;
  transition: var(--fade);
}
.tabs button:hover:not([aria-selected="true"]) { color: var(--text); border-bottom-color: var(--border); }
.tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--primary); }
.tabs button .ico { width: 18px; height: 18px; }

/* ---------- chips ----------
   Filters and tags: a row of small pills that toggle. 32px, not a button in the --ctl-h sense. */
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chips .chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 12px;
  border: 1px solid var(--border); border-radius: var(--r-pill);
  background: var(--surface); color: var(--muted);
  font-size: .85rem; font-weight: 600; white-space: nowrap;
  cursor: pointer;
  transition: var(--fade);
}
.chips .chip:hover:not([aria-pressed="true"]) { color: var(--text); border-color: var(--muted); }
.chips .chip[aria-pressed="true"] { background: var(--primary-soft); border-color: var(--primary); color: var(--primary-strong); }
.chips .chip .ico { width: 16px; height: 16px; }

/* ---------- progress ----------
   A bar that shows how far: --value from 0 to 1 on the bar. .busy is the same bar with no
   value yet - a sweep, the system's sixth movement (§2.7), still under reduced motion. */
.progress { position: relative; height: 8px; overflow: hidden; border-radius: var(--r-pill); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--border); }
.progress > i { display: block; height: 100%; width: calc(var(--value, 0) * 100%); border-radius: inherit; background: var(--primary); transition: width var(--t-slow) var(--ease); }
.progress.busy > i { position: absolute; width: 38%; animation: progress-sweep 1.4s var(--ease) infinite; }
@keyframes progress-sweep { from { left: -38%; } to { left: 100%; } }
/* the words around a bar: what it measures, and the figure */
.progress-row { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 6px; font-size: .82rem; color: var(--muted); }
.progress-row b { color: var(--text); font-variant-numeric: tabular-nums; }

/* ---------- loading ----------
   A spinner for something short (a board being fetched), a skeleton for a list that is
   about to arrive in that shape. Both stand still under reduced motion. */
.spinner { display: inline-block; width: 22px; height: 22px; flex: none; border: 3px solid var(--surface-2); border-top-color: var(--primary); border-radius: 50%; animation: spin .8s linear infinite; }
.loading { display: inline-flex; align-items: center; gap: 10px; color: var(--muted); font-size: .9rem; }
@keyframes spin { to { transform: rotate(360deg); } }
.skeleton { display: block; height: 1em; border-radius: var(--r-ctl-in); background: var(--surface-2); animation: skeleton-pulse 1.4s ease-in-out infinite; }
.skeleton + .skeleton { margin-top: .6em; }
@keyframes skeleton-pulse { 50% { opacity: .55; } }

/* ---------- notice ----------
   A message that stays on the page, unlike the toast: a line of text with an icon and a
   coloured edge. Plain = --primary; .notice-warn = --danger; .notice-ok = --success. */
.notice {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 14px;
  border: 1px solid var(--border); border-left: 4px solid var(--primary); border-radius: var(--r-ctl);
  background: var(--surface-2);
  font-size: .9rem; line-height: 1.45;
}
.notice .ico { width: 20px; height: 20px; margin-top: 1px; color: var(--primary); }
.notice-warn { border-left-color: var(--danger); } .notice-warn .ico { color: var(--danger); }
.notice-ok { border-left-color: var(--success); } .notice-ok .ico { color: var(--success); }
.notice p { margin: 0; } .notice strong { display: block; }

/* ---------- tooltip ----------
   data-tip on any control: its text appears above it under the pointer and on keyboard
   focus, in the toast's colours. Short, never a sentence the control needs. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute; left: 50%; bottom: calc(100% + 8px); z-index: 30;
  padding: 6px 10px;
  border-radius: var(--r-ctl-in);
  background: var(--toast-bg); color: var(--toast-fg);
  font-size: .78rem; font-weight: 600; line-height: 1.3; white-space: nowrap;
  transform: translateX(-50%);
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-fast) var(--ease);
}
[data-tip]:hover::after, [data-tip]:focus-visible::after { opacity: 1; }

/* ---------- data table ----------
   Rows that are read and compared: a rule under the head, hairlines between rows, figures
   right-aligned and tabular. Wrap it in .table-scroll where it may be wider than the page. */
.table-scroll { overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.data-table th { padding: 6px 10px; border-bottom: 1px solid var(--rule); text-align: left; font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.data-table td { padding: 9px 10px; border-bottom: 1px solid var(--border); }
.data-table tr:last-child td { border-bottom: 0; }
.data-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.data-table tbody tr:hover td { background: var(--surface-2); }

/* ---------- key-value list ----------
   A statistics sheet: label left, figure right, one hairline per row. */
.kv-list { display: grid; grid-template-columns: 1fr auto; margin: 0; }
.kv-list > * { padding: 8px 0; border-bottom: 1px solid var(--border); }
.kv-list dt { color: var(--muted); }
.kv-list dd { margin: 0; text-align: right; font-weight: 650; font-variant-numeric: tabular-nums; }
.kv-list > :nth-last-child(-n+2) { border-bottom: 0; }

/* ---------- empty state ----------
   A list with nothing in it says so, and says what would fill it: an icon, a line in ink,
   a line in --muted, and the action if there is one. */
.empty-list { display: grid; justify-items: center; gap: 6px; padding: 28px 16px; border: 1px dashed var(--border); border-radius: var(--radius); text-align: center; color: var(--muted); }
.empty-list .ico { width: 30px; height: 30px; margin-bottom: 4px; color: var(--faint); }
.empty-list strong { color: var(--text); }
.empty-list .btn { margin-top: 8px; }

/* ---------- device list ----------
   The devices a tool works with - or the people, the rooms -, one row each: an icon, the
   name with a status dot under it, and a count of what is new. A row is a link that opens
   what it names; the open one is [aria-current]. .over: files dragged over it go to it. */
.device-list { display: grid; gap: 4px; }
.device-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px;
  min-height: 56px; padding: 7px 10px;
  border: 1px solid transparent; border-radius: var(--r-ctl);
  color: var(--text); text-decoration: none;
  transition: var(--fade);
}
.device-row:hover { background: var(--surface-2); border-color: var(--border); }
.device-row[aria-current="true"] { background: var(--primary-soft); border-color: var(--primary); }
.device-row.over { outline: 2px dashed var(--primary); outline-offset: -3px; }
.device-icon { display: grid; place-items: center; flex: none; width: 40px; height: 40px; border-radius: var(--r-ctl); background: var(--surface-2); color: var(--muted); }
.device-icon .ico { width: 22px; height: 22px; }
.device-row:hover .device-icon { color: var(--text); }
.device-row[aria-current="true"] .device-icon { background: var(--surface); color: var(--primary-strong); }
.device-text { display: grid; gap: 1px; min-width: 0; }
.device-text strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.device-text .status-dot { font-size: .8rem; color: var(--muted); }
.device-count {
  min-width: 22px; height: 22px; padding: 0 7px;
  border-radius: var(--r-pill); background: var(--primary); color: var(--primary-contrast);
  font-size: .75rem; font-weight: 700; line-height: 22px; text-align: center; font-variant-numeric: tabular-nums;
}

/* ---------- feed ----------
   What two sides sent each other, oldest first: text, pictures, files. The other side's
   items stand at the left on --surface-2, this side's at the right in the --primary fill;
   a .feed-day line starts each day. An item is a row - the bubble and its actions as icon
   buttons beside it, so a message stays one control high - and a meta line under it: the
   time, a state (.wait amber, .bad red) and the tick of a delivered one. The feed scrolls
   inside its panel; the panel does not. */
.feed { display: flex; flex-direction: column; gap: 8px; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 4px 2px 8px; }
.feed-day { display: flex; align-items: center; gap: 12px; margin: 10px 0 2px; color: var(--muted); font-size: .76rem; font-weight: 650; }
.feed-day::before, .feed-day::after { content: ""; flex: 1; border-top: 1px solid var(--border); }
.feed-item { display: grid; justify-items: start; gap: 3px; max-width: min(92%, 620px); }
.feed-item.mine { justify-items: end; align-self: flex-end; }
.feed-row { display: flex; align-items: flex-start; gap: 4px; max-width: 100%; min-width: 0; }
.feed-item.mine .feed-row { flex-direction: row-reverse; }
.feed-bubble {
  min-width: 0; max-width: 100%;
  padding: 9px 13px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2); color: var(--text);
}
.feed-item.mine .feed-bubble { background: var(--primary); border-color: transparent; color: var(--primary-contrast); }
.feed-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.45; }
.feed-text a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.feed-text a:hover { text-decoration-thickness: 2px; }
/* a picture or a file: the preview (if there is one) over a row of icon, name and size */
.feed-file-bubble { display: grid; gap: 8px; width: min(320px, 100%); padding: 8px; }
.feed-media { display: block; max-width: 100%; padding: 0; border: 0; border-radius: calc(var(--radius) * .6); background: none; overflow: hidden; }
button.feed-media { cursor: zoom-in; }
button.feed-media:hover img { filter: brightness(1.06); }
.feed-media img, video.feed-media { display: block; max-width: 100%; max-height: 300px; border-radius: inherit; }
.feed-bubble audio { width: 100%; }
.feed-file { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 10px; padding: 0 4px 0 2px; }
.feed-file-icon { display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--r-ctl); background: var(--surface); color: var(--primary-strong); }
.feed-file-icon .ico { width: 22px; height: 22px; }
.feed-item.mine .feed-file-icon { background: color-mix(in srgb, var(--primary-contrast) 16%, transparent); color: inherit; }
.feed-file-text { display: grid; min-width: 0; line-height: 1.3; }
.feed-file-text strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feed-file-text small { color: var(--muted); font-size: .8rem; }
.feed-item.mine .feed-file-text small { color: inherit; }
.feed-item.mine .feed-bubble .progress { background: color-mix(in srgb, var(--primary-contrast) 22%, transparent); box-shadow: none; }
.feed-item.mine .feed-bubble .progress > i { background: var(--primary-contrast); }
.feed-actions { display: flex; flex: none; }
.feed-actions .icon-btn { color: var(--muted); }
.feed-actions .icon-btn:hover:not(:disabled) { color: var(--text); }
.feed-actions .icon-btn .ico { width: 20px; height: 20px; }
.feed-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; padding: 0 6px; color: var(--muted); font-size: .74rem; }
.feed-meta.wait { color: var(--warn); }
.feed-meta.bad { color: var(--danger); }
.feed-tick { display: inline-flex; color: var(--success); }
.feed-tick .ico { width: 14px; height: 14px; stroke-width: 2.4; }

/* ---------- composer ----------
   Writing to the feed: an icon button for files, the text (one line that grows with what is
   typed, up to about eight), and the send button - all on one baseline at the bottom. */
.composer { display: flex; align-items: flex-end; gap: 8px; }
.composer .field { flex: 1; min-width: 0; }
.composer textarea { height: 42px; min-height: 42px; max-height: 200px; padding-block: 9px; resize: none; line-height: 1.45; }
.composer > .icon-btn, .composer > .btn { margin-bottom: 1px; }

/* ---------- drop target ----------
   A panel files can be dragged onto: .drop-target on the panel, a .drop-veil inside it, and
   .over on the panel while files hover over it - a dashed --primary edge and a line saying
   where they will go. The veil takes no pointer events; the page handles the drop. */
.drop-target { position: relative; }
.drop-veil {
  position: absolute; inset: 0; z-index: 5;
  display: none; place-items: center;
  border: 2px dashed var(--primary); border-radius: inherit;
  background: color-mix(in srgb, var(--surface) 96%, transparent);
  color: var(--primary-strong); font-weight: 700; text-align: center;
  pointer-events: none;
}
.drop-veil > span { display: grid; justify-items: center; gap: 10px; padding: 16px; }
.drop-veil .ico { width: 36px; height: 36px; }
.drop-target.over > .drop-veil { display: grid; }

/* ---------- code frame ----------
   A QR code to scan, or the camera picture that scans one: a square up to 280px, the
   Code Generator's embed in it (docs/progress.md §6), a line of text while it loads. */
.code-frame { position: relative; width: min(100%, 280px); aspect-ratio: 1; overflow: hidden; border: var(--bw) solid var(--border); border-radius: var(--r-board); background: var(--surface-2); }
.code-frame[hidden] { display: none; }
.code-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.code-wait { position: absolute; inset: 0; display: grid; place-items: center; padding: 16px; text-align: center; color: var(--muted); font-size: .9rem; }

/* ---------- surfaces ---------- */
/* A box that groups content: the style guide's specimen panels, the settings panel. */
.box {
  padding: 20px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.box > :is(h2, h3) { margin: 0 0 14px; font-family: var(--font-display); font-size: 1rem; letter-spacing: -.01em; }

.install-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-bottom: 14px; padding: 14px 16px;
  border-radius: var(--radius);
  background: var(--primary-soft);
}
@media (max-width: 560px) { .install-banner { flex-wrap: wrap; } }
.install-banner strong { display: block; }
.install-banner span { color: var(--muted); font-size: .88rem; }

.offline-badge { display: inline-flex; align-items: center; gap: 8px; font-size: .82rem; color: var(--muted); }
.offline-badge i { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
.offline-badge.ready i { background: var(--success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 25%, transparent); }
/* the cache is still filling: amber, and still - a status dot is not one of the places that move (§2.7) */
.offline-badge.pending i { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 25%, transparent); }
/* no service worker here (plain http): an empty ring */
.offline-badge.unsupported i { background: transparent; box-shadow: inset 0 0 0 1.5px var(--muted); }

/* ---------- dialog & toast ---------- */
.overlay {
  position: fixed; inset: 0; z-index: 50;
  display: grid; place-items: center;
  padding: 20px;
  background: var(--overlay);
  animation: fade var(--t-fast) var(--ease);
}
.overlay.closing { animation: fade var(--t-fast) var(--ease) reverse forwards; }
.dialog {
  width: min(100%, 400px);
  max-height: calc(100dvh - 40px); overflow: auto;
  padding: 22px;
  border-radius: var(--r-dialog);
  background: var(--surface); color: var(--text);
  box-shadow: var(--dialog-shadow);
  animation: dialog-in .22s cubic-bezier(.2, .9, .3, 1.2);
}
/* A wide dialog, for what would otherwise be a narrow, long column that scrolls on a PC:
   rules, scores, leaderboards, a transfer. The one other width a dialog has (§2.7). */
.dialog.wide { width: min(100%, 760px); }
/* A passive dialog leaves the board in view and in reach: no veil, at the bottom of the
   screen, and the page behind it still takes clicks (a hint that points at a cell). Its two
   actions stay in a row at every width; it never has three. */
.overlay:has(> .dialog.passive) { background: none; -webkit-backdrop-filter: none; backdrop-filter: none; place-items: end center; padding-bottom: calc(16px + env(safe-area-inset-bottom)); pointer-events: none; }
.dialog.passive { pointer-events: auto; }
.dialog.passive .dialog-actions { flex-direction: row; justify-content: flex-end; align-items: center; margin-top: 14px; }
body.has-toolbar .overlay:has(> .dialog.passive) { padding-bottom: calc(var(--toolbar-h) + 16px + env(safe-area-inset-bottom)); }
/* A dialog that only faded out would leave its box hanging; it leaves the way it came. */
.overlay.closing .dialog { animation: dialog-in var(--t-fast) var(--ease) reverse forwards; }
.dialog h2 { margin: 0 0 10px; font-family: var(--font-display); font-size: 1.3rem; letter-spacing: -.01em; }
.dialog p { margin: 0; color: var(--muted); line-height: 1.5; }
.dialog .stats { margin-top: 14px; }
.dialog .stats .stat { flex: 1; padding: 8px 10px; }
.dialog .record { margin-top: 12px; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
/* Two actions fit the 400px dialog in a row in every theme; three do not (up to 87px over
   in Cyberpunk, German). The dialog is overflow: auto, and an overflowing flex-end row
   spills to the left, where nothing can scroll to it - the first button just disappears.
   So three or more actions always stack, and on a phone every dialog's actions do: full
   width, primary on top. Anything that sets its own direction (.xfer-stack) is more specific. */
.dialog-actions:has(> :nth-child(3)) { flex-direction: column-reverse; align-items: stretch; }
@media (max-width: 560px) {
  .dialog-actions { flex-direction: column-reverse; align-items: stretch; }
}
.rules { margin: 0; padding-left: 1.2em; color: var(--muted); line-height: 1.5; }
.rules li + li { margin-top: 6px; }
@keyframes fade { from { opacity: 0; } }
@keyframes dialog-in { from { opacity: 0; transform: scale(.92); } }

#toast {
  position: fixed; left: 50%; bottom: calc(20px + env(safe-area-inset-bottom)); z-index: 70;
  display: flex; align-items: center; gap: 14px;
  max-width: calc(100% - 32px);
  padding: 11px 16px;
  border-radius: var(--r-ctl);
  background: var(--toast-bg); color: var(--toast-fg);
  box-shadow: var(--shadow);
  font-size: .92rem;
  opacity: 0; pointer-events: none;
  transform: translate(-50%, 20px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}
#toast.show { opacity: 1; pointer-events: auto; transform: translate(-50%, 0); }
/* Over a bottom .toolbar the toast stands above it, never on the tools. */
body.has-toolbar #toast { bottom: calc(var(--toolbar-h) + 12px + env(safe-area-inset-bottom)); }
/* On a full-screen game route every pixel below the top bar is the game, and the bottom is
   where its buttons are: the toast comes down over the top bar instead - over the brand and
   the stats, which nobody clicks mid-game - and lets clicks through to whatever it covers;
   only its action takes them (§3, Toast). */
body.app-full #toast { top: calc(env(safe-area-inset-top) + 7px); bottom: auto; transform: translate(-50%, -16px); pointer-events: none; }
body.app-full #toast.show { transform: translate(-50%, 0); }
body.app-full #toast.show .toast-action { pointer-events: auto; }
.toast-action { border: 0; padding: 0; background: none; color: var(--toast-action); font-weight: 800; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; cursor: pointer; transition: var(--fade); }
.toast-action:hover { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }

/* The backdrop: js/backdrop.js creates this canvas and paints each theme's scene on it
   (js/backdrops/<theme>.js). It sits behind the page and never takes a pointer event.
   --veil is how much of the scene shows through the content column (1 = all of it);
   the margins show the full scene and the mask fades to --veil over 80px at each edge
   of the column, so text stays readable while the margins stay alive. */
.backdrop {
  --veil: 1;
  --veil-mask: linear-gradient(90deg, #000 calc(50% - var(--page-w) / 2 - 40px), rgb(0 0 0 / var(--veil)) calc(50% - var(--page-w) / 2 + 40px), rgb(0 0 0 / var(--veil)) calc(50% + var(--page-w) / 2 - 40px), #000 calc(50% + var(--page-w) / 2 + 40px));
  position: fixed; inset: 0; z-index: -1; width: 100%; height: 100%; pointer-events: none;
  -webkit-mask-image: var(--veil-mask); mask-image: var(--veil-mask);
}
/* the reader is for long text: the scene stays, but well behind it */
body[data-route="docs"] .backdrop { opacity: .5; }

/* ---------- page ---------- */
.view { width: 100%; max-width: var(--page-w); margin: 0 auto; padding: 16px max(16px, env(safe-area-inset-right)) calc(28px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left)); }
.page { scroll-margin-top: 80px; }
.page + .page { margin-top: 64px; }
/* the style guide's own section heads reuse .section-head, so they follow each theme */

/* ---------- home: hero ---------- */
.home-hero { margin-bottom: 16px; }
/* the masthead title, dateline and artwork are only switched on by themes that use them */
.hero-title, .hero-meta, .hero-art, .game-num { display: none; }
.intro { margin: 4px 2px 0; color: var(--muted); line-height: 1.5; }

/* ---------- home: sections and game cards ---------- */
.home-section + .home-section { margin-top: 28px; }
.section-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin: 0 2px 12px; }
.section-title { margin: 0; font-family: var(--font-display); font-size: 1.15rem; font-weight: 750; letter-spacing: -.01em; }
.section-count { display: none; }
.section-note { margin: 0; color: var(--muted); font-size: .88rem; }
/* An untitled list hides its head, but only visually: the h2 keeps the heading outline
   (h1 > h2 > h3) intact for screen readers. */
.home-section.untitled .section-head { position: absolute; width: 1px; height: 1px; margin: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* guide only, not part of a product screen: names the layout a home section demonstrates */
.layout-tag { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; margin: 0 2px 14px; padding-bottom: 8px; border-bottom: 1px dashed var(--border); color: var(--muted); font-size: .8rem; line-height: 1.4; }
.layout-tag strong { color: var(--text); }
.layout-tag strong::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 8px; border-radius: var(--r-pill); background: var(--accent); vertical-align: .08em; }

.game-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 12px; }
.game-card {
  --c: var(--gc);
  display: flex; align-items: center; gap: 14px;
  padding: 14px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: inherit;
  box-shadow: var(--shadow);
  text-decoration: none;
  transition: transform .12s var(--ease), var(--fade);
}
.game-card:hover { transform: translateY(-2px); border-color: color-mix(in srgb, var(--c) 45%, var(--border)); }
.game-card:active { transform: scale(.98); }
.game-icon {
  flex: none;
  display: grid; place-items: center;
  width: 58px; height: 58px;
  border-radius: 16px;
  background: var(--c); color: var(--c);
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, .12);
}
.game-icon svg { width: 42px; height: 42px; }
.game-meta { min-width: 0; }
.game-meta h3 { margin: 0; font-family: var(--font-display); font-size: 1.05rem; letter-spacing: -.01em; }
.game-meta p { margin: 2px 0 0; color: var(--muted); font-size: .87rem; line-height: 1.35; }
.game-status { margin-top: 5px; font-size: .75rem; font-weight: 700; color: var(--primary-strong); }

/* Three list layouts, set by a class on .game-list (see docs/README.md §1.1):
   .kind-glyph  filled pictograms with #fff details on the game colour (the default rules above)
   .kind-line   stroke-drawn symbols in currentColor
   .kind-art    a card-art panel above the text */
.kind-line .game-icon svg, .ico-line svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.kind-line .game-icon { color: #fff; }
.kind-line .game-icon svg { width: 30px; height: 30px; }

/* art panels: the icon box shows a small card layout instead (see .tile-art-inner).
   The panel is as tall as the tallest layout (104px at --cw 40px) plus a 12px margin,
   not a ratio of the card's width: a 16/10 panel on a wide card was mostly empty table. */
.kind-art .game-card { align-items: stretch; flex-direction: column; gap: 10px; padding: 0 0 14px; overflow: hidden; }
.kind-art .game-card .game-meta { padding: 0 14px; }
.kind-art .game-icon {
  position: relative; overflow: hidden;
  width: auto; height: 128px;
  border-radius: 0;
  background: var(--table-bg);
  box-shadow: none;
  --cw: 40px;
}
.tile-art-inner { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(var(--art-scale, 1)); }

.home-footer { margin-top: 22px; display: flex; justify-content: center; }

/* ---------- page layouts: the work page, the full-screen game route ----------
   Two page widths and no third (§2.7): --content-w, or --work-w on a page that needs more of
   the screen. A narrow game caps its own board, never the page. */
body.work-page { --page-w: var(--work-w); }
/* .work: the main panel and a side panel, side by side from 900px; below that the side
   panels follow the main one and gather two to a row where they fit. */
.work { display: grid; gap: 16px; align-items: start; }
.work-main, .work-side { min-width: 0; }
.work-side { display: grid; gap: 16px; align-content: start; }
@media (min-width: 600px) and (max-width: 899px) {
  .work-side { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
}
@media (min-width: 900px) {
  .work { grid-template-columns: minmax(0, 1fr) var(--side-w); }
  /* .side-first: a list that picks what the main panel shows (devices, then one device's
     feed) reads from the left, the side panel first in the markup and on the screen. */
  .work.side-first { grid-template-columns: var(--side-w) minmax(0, 1fr); }
}
/* A game route that fills the screen (a card table): the page does not scroll, the game
   screen takes the height the bars leave, and its .board-frame the height the game screen
   leaves. The class goes on <body> on that route only; home keeps scrolling as a page. */
body.app-full { height: 100dvh; min-height: 0; overflow: hidden; }
body.app-full > .view { flex: 1; min-height: 0; display: flex; flex-direction: column; padding-bottom: 12px; }
body.app-full > .site-footer { display: none; }
body.app-full .game-screen { flex: 1; min-height: 0; display: flex; flex-direction: column; }
body.app-full .game-screen > .board-frame { flex: 1; min-height: 0; }

/* ---------- the bottom tool bar ----------
   A game's tool row as a bar along the bottom of the screen, a child of <body> after the
   view, drawn like the theme's top bar. It sticks to the bottom while the page scrolls; on a
   full-screen route it is simply the last thing on the screen. body.has-toolbar lifts the
   toast and a passive dialog above it. */
.toolbar {
  position: sticky; bottom: 0; z-index: 20;
  display: flex; justify-content: center; align-items: stretch; gap: 4px;
  min-height: calc(var(--toolbar-h) + env(safe-area-inset-bottom));
  padding: 4px max(8px, env(safe-area-inset-right)) calc(4px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-top: 1px solid var(--border);
}
.toolbar > .tool { flex: 0 1 108px; max-width: none; justify-content: center; }
/* guide only: the specimen of the bar sits in its box instead of on the screen's edge */
.toolbar.spec-toolbar { position: static; z-index: auto; min-height: var(--toolbar-h); padding: 4px 8px; margin: 0 -2px; }
/* guide only: a top bar in a specimen box (the full-screen route's, with its stats) */
.topbar.spec-topbar { position: static; z-index: auto; padding: 6px 8px; margin: 0 -2px; }

/* ---------- tool rows: a side panel's grid, a crowded row ----------
   .tools.grid fills its panel with tools at least --tool-min wide (84px): a 340px side panel
   holds three. --tool-min: 60px holds four there; --tool-cols: 4 (with --tool-min: 0px) fixes
   the count whatever the width. */
.tools.grid { display: grid; grid-template-columns: repeat(var(--tool-cols, auto-fill), minmax(var(--tool-min, 84px), 1fr)); }
.tools.grid .tool { max-width: none; }
/* Six tools or more on a phone: the row drops its gaps and the labels step down before any
   label is cut. :root lifts it over each theme's .tool rule (§5). */
@media (max-width: 560px) {
  :root .tools.dense { gap: 0; }
  :root .tools.dense .tool { padding-inline: 1px; font-size: .68rem; letter-spacing: 0; }
}

/* ---------- buttons that share a width ----------
   .btn-fill: the buttons of a panel (Upgrade / Sell) share its width - side by side while each
   gets --btn-min (its own label by default), stacked full width once they don't. */
.btn-fill { display: flex; flex-wrap: wrap; gap: 8px; }
.btn-fill > .btn { flex: 1 1 var(--btn-min, max-content); min-width: 0; }

/* ---------- segmented: wrapping, dense ----------
   .wrap: more options than the row holds go onto a second row instead of being cut; every
   button keeps the control's one height. .dense: four or five options on a 360px phone. */
.segmented.wrap { flex-wrap: wrap; height: auto; min-height: var(--ctl-h); max-width: 100%; }
.segmented.wrap button { flex: 1 0 auto; min-height: calc(var(--ctl-h) - 6px); }
.segmented.dense { max-width: 100%; }
:root .segmented.dense button { flex: 1 1 auto; min-width: 0; padding-inline: 7px; font-size: .8rem; letter-spacing: .01em; }

/* ---------- stat states ----------
   [aria-current="true"]: whose turn it is. .won: who won. .low: a value in danger (the last
   life, the last seconds) - the figure turns --danger, :root lifting it over the themes' own
   figure colours (§5). */
.stat[aria-current="true"] { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
.stat.won { border-color: var(--success); box-shadow: inset 0 0 0 1px var(--success); }
:root .stat.low .stat-value { color: var(--danger); }

/* ---------- key-value list: dense, with a delta ---------- */
.kv-list.dense { font-size: .88rem; }
.kv-list.dense > * { padding: 4px 0; }
/* what an upgrade adds: a figure after the value, the success colour pulled toward the ink
   so it reads as text in every theme */
.kv-list .delta { margin-left: 6px; color: color-mix(in srgb, var(--success) 78%, var(--text)); font-weight: 700; }

/* ---------- read-only stepper ----------
   A value picked with the two buttons only (a level, a map): an <output> instead of the field. */
.stepper.readonly output {
  display: grid; place-items: center;
  min-width: 56px; height: var(--ctl-h); padding: 0 6px;
  font-family: var(--font-display); font-size: 1.1rem; font-weight: 700; font-variant-numeric: tabular-nums;
}

/* ---------- a labelled option inside a game panel ----------
   The label left, the control right, wrapping under it when the panel is narrow. A game's
   setup box is a stack of these; the drawer's .setting is for settings, not for a game. */
.setup-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; padding: 8px 0; }
.setup-row + .setup-row { border-top: 1px solid var(--border); }
.setup-label { color: var(--muted); font-size: .85rem; font-weight: 650; }

/* ---------- tags ----------
   Read-only pills (what a wave brings, what a card is): no hover, no press. Styled only
   inside .tags, so an app's own .tag is untouched. An optional dot in --dot. */
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
.tags .tag {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 26px; padding: 0 10px;
  border: 1px solid var(--border); border-radius: var(--r-pill);
  background: var(--surface); color: var(--text);
  font-size: .8rem; font-weight: 600; white-space: nowrap;
}
.tags .tag > i { flex: none; width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--dot, var(--muted)); }

/* ---------- status dot ----------
   A state in a word, with a dot in front: ok (--success), warn (--warn), off (an empty ring).
   The word carries the meaning; the dot repeats it. */
.status-dot { display: inline-flex; align-items: center; gap: 8px; color: var(--text); font-size: .88rem; }
.status-dot::before { content: ""; flex: none; width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--muted); }
.status-dot[data-tone="ok"]::before { background: var(--success); }
.status-dot[data-tone="warn"]::before { background: var(--warn); }
.status-dot[data-tone="off"]::before { background: transparent; box-shadow: inset 0 0 0 1.5px var(--muted); }

/* ---------- checklist ----------
   Read-only goals: an empty box, ticked in --success once the goal is met (.done), the goal,
   and an optional figure at the right. Not a form: nothing here is clicked. */
.checklist { margin: 0; padding: 0; list-style: none; }
.checklist li {
  position: relative;
  display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid var(--border);
}
.checklist li:last-child { border-bottom: 0; }
.checklist li::before { content: ""; width: 18px; height: 18px; border-radius: min(var(--r-ctl-in), 4px); box-shadow: inset 0 0 0 1.5px var(--muted); }
.checklist li.done::before { background: var(--success); box-shadow: none; }
.checklist li.done::after {
  content: ""; position: absolute; left: 3px; top: calc(50% - 6px); width: 12px; height: 12px;
  background: var(--success-contrast);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
}
.checklist li > :last-child:not(:first-child) { color: var(--muted); font-size: .85rem; font-variant-numeric: tabular-nums; }

/* ---------- answer tiles ----------
   An answer to pick or a tile in a picker (a promotion piece, a building): bigger than a
   button, an optional icon or glyph over the word, an optional <small> line. Before the
   answer: rest, hover, focus, chosen (aria-pressed). After it: .ok (right), .no (wrong),
   .reveal (the right one, shown after a wrong pick), the rest :disabled. The grid takes
   --choice-cols (a number, or auto-fill) and --choice-min (the narrowest tile). Right, wrong
   and revealed carry :root, so they win over every theme's resting tile (§5); a theme that
   draws them its own way writes [data-theme] .choice-tile.ok, which loads later. */
.choice-grid { display: grid; grid-template-columns: repeat(var(--choice-cols, auto-fill), minmax(var(--choice-min, 120px), 1fr)); gap: 10px; }
.choice-tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  min-height: 56px; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  background: var(--surface); color: var(--text);
  font-size: 1rem; font-weight: 650; line-height: 1.25; text-align: center;
  cursor: pointer;
  transition: var(--fade);
}
.choice-tile small { font-size: .78rem; font-weight: 500; color: var(--muted); }
.choice-tile .glyph { font-size: 1.6rem; line-height: 1; }
.choice-tile .ico { width: 26px; height: 26px; }
.choice-tile:hover:not(:disabled, [aria-pressed="true"], .ok, .no, .reveal) { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
.choice-tile[aria-pressed="true"]:not(.ok, .no, .reveal) { border-color: var(--primary); background: var(--primary-soft); color: var(--primary-strong); box-shadow: inset 0 0 0 1px var(--primary); }
:root .choice-tile.ok { border-color: var(--success); background: var(--success); color: var(--success-contrast); box-shadow: none; animation: choice-pop var(--t-move) var(--ease); }
:root .choice-tile.no { border-color: var(--danger); background: var(--danger); color: var(--danger-contrast); box-shadow: none; animation: choice-shake calc(var(--t-move) * 2) var(--ease); }
:root .choice-tile.reveal { border-color: var(--success); box-shadow: inset 0 0 0 2px var(--success); }
:root .choice-tile:is(.ok, .no) small { color: inherit; }
.choice-tile:disabled { cursor: default; }
.choice-tile:disabled:not(.ok, .no, .reveal, [aria-pressed="true"]) { opacity: .5; }
/* .text: an answer that is a sentence. The reading face in the text's own case, whatever
   the theme sets its tiles in (pixels, capitals, lowercase), starting at the left edge. :root
   lifts it over each theme's tile rule (§5). */
:root .choice-tile.text {
  align-items: flex-start; text-align: start;
  font-family: var(--font); font-size: 1rem; font-weight: 500; font-stretch: 100%;
  text-transform: none; letter-spacing: normal; line-height: 1.4; text-shadow: none;
}
/* .choice-grid.inline: a long picker (a build menu). Each tile is one row, about 48px: the
   icon or glyph at the left, the name and its <small> line (a price) at its right. */
.choice-grid.inline { --choice-min: 160px; gap: 8px; }
:root .choice-grid.inline .choice-tile {
  display: grid; grid-template-columns: auto minmax(0, 1fr); grid-auto-flow: row; align-content: center;
  column-gap: 10px; row-gap: 1px;
  min-height: 48px; padding: 5px 10px;
  text-align: start; line-height: 1.2;
}
.choice-grid.inline .choice-tile > :is(.ico, .glyph, svg, img) { grid-row: span 2; align-self: center; }
.choice-grid.inline .choice-tile .glyph { font-size: 1.35rem; }
.choice-grid.inline .choice-tile .ico { width: 24px; height: 24px; }
.choice-grid.inline .choice-tile small { grid-column: 2; }
@keyframes choice-pop { 50% { transform: scale(1.05); } }
@keyframes choice-shake { 20%, 60% { transform: translateX(-5px); } 40%, 80% { transform: translateX(5px); } }

/* ---------- board frame and board status ----------
   .board-frame is the theme's frame around any board or canvas a game draws itself; it also
   positions what stands over the board. --frame-out is the room a frame drawn outside the
   box needs (Arcade's 8px). .board-status is a pill over the board (Paused, Your turn);
   .board-status.cover dims the whole board and is a button: a click resumes. */
.board-frame { position: relative; max-width: 100%; margin: var(--frame-out); border-radius: var(--r-board); box-shadow: 0 0 0 1px var(--border); }
.board-status, .board-status.cover > span {
  padding: 6px 14px; border-radius: var(--r-pill);
  background: var(--toast-bg); color: var(--toast-fg);
  font-size: .85rem; font-weight: 700; line-height: 1.3; white-space: nowrap;
  box-shadow: var(--shadow);
}
.board-status { position: absolute; left: 50%; top: 12px; z-index: 2; transform: translateX(-50%); pointer-events: none; }
.board-status.cover {
  inset: 0; transform: none;
  display: grid; place-items: center;
  width: 100%; padding: 0; border: 0; border-radius: inherit;
  background: var(--overlay); color: inherit; box-shadow: none;
  font: inherit;
  pointer-events: auto; cursor: pointer;
  transition: var(--fade);
}
.board-status.cover:hover > span { box-shadow: var(--shadow), 0 0 0 3px var(--primary); }
.board-status.cover:focus-visible { outline-offset: -4px; }
/* .accent: a callout that matters (a win, a hand's name) in the theme's primary fill, with
   the theme's panel edge, like the keypad's accent key. */
.board-status.accent { background: var(--primary); color: var(--primary-contrast); box-shadow: var(--panel-edge); }

/* ---------- celebration ----------
   A win (§2.7, game feedback): pieces of the theme's --confetti fall once over the screen and
   are gone in three seconds. Never under reduced motion, never in the way of a click. The
   script that throws them sets --x, --dx, --rot, --c, --delay and --dur on each <i>. */
.celebration { position: fixed; inset: 0; z-index: 60; overflow: hidden; pointer-events: none; }
.celebration i {
  position: absolute; top: -14px; left: var(--x, 50%);
  width: 8px; height: 13px; border-radius: min(2px, var(--r-cell));
  background: var(--c, var(--primary));
  animation: celebration-fall var(--dur, 2.2s) linear var(--delay, 0s) forwards;
}
@keyframes celebration-fall { to { transform: translate(var(--dx, 0), 108vh) rotate(var(--rot, 360deg)); } }

/* ---------- specimen layout ---------- */
/* align-items: start, so a short specimen is a short box: stretching them all to the
   tallest in the row leaves voids that read as a mistake rather than as rhythm. */
.spec-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); align-items: start; gap: 16px; }
.spec-grid .wide { grid-column: 1 / -1; }
/* guide only: the Sharing box - the devices and a code frame beside a feed and its composer */
.spec-share { display: grid; gap: 16px; align-items: start; }
@media (min-width: 760px) { .spec-share { grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); } }
.spec-share .code-frame { justify-self: center; }
.spec-share .drop-target { border-radius: var(--radius); }
.spec-feed { max-height: 360px; }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.row + .row { margin-top: 12px; }
.stack { display: grid; gap: 14px; }
.caption { margin: 10px 0 0; color: var(--muted); font-size: .8rem; line-height: 1.45; }

.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.swatch-item { display: grid; grid-template-columns: 34px 1fr; gap: 10px; align-items: center; font-size: .78rem; }
.swatch-item i { width: 34px; height: 34px; border-radius: min(var(--r-ctl), 8px); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 18%, transparent); background: var(--sw); }
.swatch-item b { display: block; font-family: var(--font-mono); font-weight: 600; font-size: .74rem; }
.swatch-item span { font-family: var(--font-mono); color: var(--muted); font-size: .72rem; }

.type-specimen { display: grid; gap: 12px; }
.type-specimen .t-display { margin: 0; font-family: var(--font-display); font-size: 1.8rem; font-weight: 750; line-height: 1.05; letter-spacing: -.02em; }
.type-specimen .t-body { margin: 0; line-height: 1.55; }
.type-specimen .t-label { margin: 0; font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.type-specimen .t-mono { margin: 0; font-family: var(--font-mono); font-size: .9rem; }

.icon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 10px; }
.icon-tile {
  display: grid; justify-items: center; gap: 8px;
  padding: 14px 6px 10px;
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  background: var(--surface);
  font-size: .72rem; color: var(--muted);
  transition: var(--fade);
}
.icon-tile .ico { width: 26px; height: 26px; color: var(--text); }

/* ---------- leaderboard ---------- */
/* One board: its name and period, the top five, a gap, and this device's row - six rows
   however far down the player is (§4.15). Rows are read, not clicked, so they have no
   hover. .off: global sharing is switched off - the default -, nothing is asked, and the
   board is one row: the device's own best, without a rank. */
.leaderboard { display: grid; gap: 10px; }
.lb-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
.lb-title { font-weight: 650; }
.lb-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--rule); }
.lb-row {
  display: grid; grid-template-columns: 3.4em minmax(0, 1fr) auto; align-items: center; gap: 12px;
  min-height: 36px; padding: 0 10px;
  border-bottom: 1px solid var(--border);
}
.lb-rank { text-align: right; font-family: var(--font-mono); font-size: .85rem; font-variant-numeric: tabular-nums; color: var(--muted); }
.lb-row:nth-child(-n+3) .lb-rank { color: var(--text); font-weight: 700; }
.lb-name { display: flex; align-items: baseline; gap: .4em; min-width: 0; white-space: nowrap; }
.lb-who { overflow: hidden; text-overflow: ellipsis; }
/* The tag behind the name: the row's own colour, faded, so the name leads in every theme and
   on every .you background, and it stays whole when a long name is cut. */
.lb-tag { flex: none; font-size: .85em; font-weight: 400; font-variant-numeric: tabular-nums; color: color-mix(in srgb, currentColor 60%, transparent); }
.lb-score { font-family: var(--font-display); font-weight: 750; font-variant-numeric: tabular-nums; }
/* On an assisted board, the help a score took, in front of it: the hint icon and the count,
   short so the name keeps its room, faded like the tag so the score still leads. */
.lb-help { display: inline-flex; align-items: center; gap: .15em; margin-right: .6em; font-family: var(--font); font-size: .8em; font-weight: 400; white-space: nowrap; color: color-mix(in srgb, currentColor 60%, transparent); }
.lb-help .ico { width: 1.1em; height: 1.1em; }
/* A board whose rows bring their game (docs/progress.md §9.6): a play button at each row's
   end - the row's height, narrow, in the board's padding - so the name keeps its room. */
.lb-games .lb-row { grid-template-columns: 2.4em minmax(0, 1fr) auto 26px; gap: 8px; padding-right: 0; padding-left: 4px; }
.lb-race {
  display: grid; place-items: center; width: 26px; height: 34px; padding: 0;
  border: 0; border-radius: var(--r-ctl); background: transparent; color: var(--primary); cursor: pointer;
  transition: var(--fade);
}
.lb-race:hover { background: var(--surface-2); }
.lb-race .ico { width: 15px; height: 15px; }
.lb-gap { padding: 0 10px; border-bottom: 1px solid var(--border); color: var(--faint); line-height: 1.4; text-align: center; }
.lb-row.you { background: var(--primary-soft); box-shadow: inset 3px 0 0 var(--primary); font-weight: 700; }
.lb-row.you .lb-rank { color: inherit; }
/* The board's cloud: offline (crossed out, --muted) or online. It opens the settings. */
.lb-tools { display: flex; align-items: center; gap: 8px; }
.lb-net { color: var(--primary); }
.leaderboard.off .lb-net { color: var(--muted); }
.lb-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; min-height: var(--ctl-h); color: var(--muted); font-size: .82rem; }

/* ---------- settings panel ---------- */
.settings { display: grid; }
/* Its tabs - Appearance, Gameplay, Account - share the width, so none is cut on a phone;
   each pane holds a handful of rows and fits the screen without scrolling. */
.settings-tabs { margin-bottom: 6px; }
.settings-tabs button { flex: 1 0 auto; justify-content: center; padding: 0 10px; }
/* The tabs' rule is the pane's top edge: its first row draws none, in any theme. */
.settings-pane > .setting:first-child { border-top: 0; padding-top: 10px; }
.setting { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 0; border-top: 1px solid var(--border); }
.setting:first-of-type { border-top: 0; padding-top: 4px; }
.setting-text strong { display: block; font-weight: 650; }
.setting-text small { display: block; margin-top: 2px; color: var(--muted); font-size: .82rem; line-height: 1.35; }
.setting .range { max-width: 200px; }
/* Two or more buttons on one setting row (Transfer progress: Send, Receive). */
.setting-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
/* A text field on a setting row (Player name): as wide as a name needs, never wider than the
   row. It does not shrink below 210px - the row's text wraps first - because the app's own
   generated names (Dizzy Platypus 754) must show whole in every face, the mono ones too. */
.setting-field { flex: 1 0 210px; max-width: 260px; min-width: 0; }
.setting-field input { width: 100%; height: 38px; }
.setting-text label { cursor: inherit; }
/* A row whose control needs the full width puts it under its label. */
.setting-wide { flex-direction: column; align-items: stretch; gap: 10px; }
/* The theme setting: every theme as a sample, two to a row, one click to switch.
   The rows are the menu's .theme-opt, so each theme's option rules reach them too;
   the description moves into the tooltip to keep the list four rows high. */
.theme-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; }
.theme-list .theme-opt { grid-template-columns: auto minmax(0, 1fr); gap: 10px; padding: 6px; box-shadow: inset 0 0 0 1px var(--border); }
.theme-list .theme-opt strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.theme-list .theme-opt[aria-checked="true"] { background: var(--primary-soft); box-shadow: inset 0 0 0 2px var(--primary); }
.theme-list .theme-opt:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* On a phone the two columns are narrow: the name steps down to .86rem and the row tightens
   before any name is cut - "Broadsheet" in Fraunces 900 was the first to lose its ending. */
@media (max-width: 560px) {
  .theme-list { gap: 3px; }
  .theme-list .theme-opt { gap: 8px; padding: 5px; }
  .theme-list .theme-opt strong { font-size: .86rem; letter-spacing: -.01em; }
}
.settings-title { margin: 0 0 6px; font-family: var(--font-display); font-size: 1.3rem; letter-spacing: -.01em; }

/* the same panel as a drawer from the right */
.drawer-scrim { position: fixed; inset: 0; z-index: 45; background: var(--overlay); animation: fade var(--t-fast) var(--ease); }
.drawer-scrim.closing { animation: fade var(--t-fast) var(--ease) reverse forwards; }
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 46;
  width: min(420px, 100vw); overflow: auto;
  padding: 22px;
  background: var(--surface); color: var(--text);
  box-shadow: var(--dialog-shadow);
  animation: drawer-in var(--t-slow) var(--ease);
}
/* A drawer comes in off the edge it hangs from, and goes back out the same way.
   It is re-rendered in place when the theme or language changes, so it never replays. */
.drawer.closing { animation: drawer-in var(--t-fast) var(--ease) reverse forwards; }
.drawer-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
@keyframes drawer-in { from { transform: translateX(100%); } }

/* ---------- footer ---------- */
/* body is a min-height: 100dvh flex column; the auto top margin takes the slack, so on a
   short page the footer rests on the bottom of the screen instead of floating under the
   content. On a long page there is no slack and it simply follows the content. */
.site-footer {
  margin-top: auto; flex-shrink: 0;
  border-top: 1px solid var(--border);
  color: var(--muted); font-size: .85rem;
}
/* The footer sits in the same column as the page: both read --page-w, so a theme
   that narrows .view does not leave its footer hanging wider than everything above it.
   The links take the slack in the middle; below 700px the three groups stack, left
   aligned, instead of the last one wrapping under the first. */
.site-footer .inner {
  max-width: var(--page-w); margin: 0 auto;
  padding: 22px max(16px, env(safe-area-inset-right)) 32px max(16px, env(safe-area-inset-left));
  display: flex; flex-wrap: wrap; align-items: center; gap: 14px 24px;
}
.footer-brand { color: var(--text); font-family: var(--font-display); font-weight: 750; }
.footer-links { flex: 1 1 auto; display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 18px; margin: 0; padding: 0; list-style: none; }
.footer-links a { color: inherit; text-decoration: none; transition: var(--fade); }
.footer-links a:hover { color: var(--text); text-decoration: underline; }
/* "by Arrow Lab" in the note links to the landing page with every app. */
.footer-note a { text-underline-offset: 2px; transition: var(--fade); }
.footer-note a:hover { color: var(--text); }
/* flex: 1 1 auto on both of these is what makes the wrap deliberate: where the three
   groups do not fit on one line the meta takes a line of its own and stays right
   aligned under the links, instead of landing back under the brand. */
.footer-meta { flex: 1 1 auto; display: flex; align-items: center; justify-content: flex-end; gap: 16px; }
.version { font-family: var(--font-mono); font-size: .78rem; }
/* The release tag is filled in once build-info.json answers; until then it takes no room. */
.version:empty { display: none; }
@media (max-width: 700px) {
  .site-footer .inner { flex-direction: column; align-items: flex-start; }
  .footer-links, .footer-meta { justify-content: flex-start; }
}

/* ---------- the game screen ----------
   One column: .bar (the stats and the game's options, §2.8 D4), the board, a hint line that
   keeps its height so the board never jumps when the hint changes or goes, and the tool
   row. .game-demo is the style guide's specimen of it and stays as an alias. */
.game-screen, .game-demo { display: grid; gap: 14px; }
.game-screen > .bar, .game-demo > .bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
.hint-text { margin: 0; text-align: center; color: var(--muted); font-size: .85rem; }
.game-screen > .hint-text { line-height: 1.45; min-height: calc(var(--hint-lines, 1) * 1.45em); }

/* ---------- board size on a scrolling page ----------
   A board on a page that scrolls (not body.app-full) is as big as the screen allows once the
   bars are counted: --board-max is the screen's height less --chrome-h, what stands above and
   below the board on a game screen (the top bar 64, the page's top padding 26, the .bar 40,
   the hint line 20, the tool row 62, four gaps of 14 and room to breathe: 290px). A game
   screen with more around its board raises --chrome-h; the board itself takes .board-fit
   (--board-ratio is its width over its height). From the floor of 260px up; on a phone held
   upright the width is the limit and the page may scroll a little. */
.game-screen, .game-demo { --chrome-h: 290px; --board-max: max(260px, calc(100dvh - var(--chrome-h))); }
@media (max-width: 899px) and (orientation: portrait) { .game-screen, .game-demo { --board-max: 100vw; } }
.board-fit { width: min(100%, calc(var(--board-max, 100vw) * var(--board-ratio, 1))); margin-inline: auto; }

/* the 2048 tile board: sixteen slots, and over them a layer of tiles that slide.
   <div class="g2048-board">  16 x <div class="g2048-slot"></div>
     <div class="g2048-tiles"> <div class="g2048-tile" style="--r:0;--c:3"><div class="v8">8</div></div> …
   A tile moves by changing its --r / --c: it slides over --t-move. .new pops it in after the
   slide, .merged bumps the tile two others became. The board is as wide as its container
   allows (--board-max on a game screen) and sizes its digits from its own width. The old
   markup, sixteen cells with the value classes on them, still draws a still board. */
.g2048-board {
  --gap: clamp(6px, 2.6cqw, 12px);
  position: relative;
  container-type: inline-size;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap);
  width: min(100%, var(--board-max, 100%)); aspect-ratio: 1; margin: 0 auto;
  padding: var(--gap);
  border-radius: var(--r-board);
  background: var(--g-board);
  touch-action: none;
  user-select: none; -webkit-user-select: none;
}
.g2048-board > div:not(.g2048-tiles), .g2048-tile > div {
  display: grid; place-items: center;
  border-radius: var(--r-cell);
  background: var(--tb, var(--g-slot)); color: var(--tf);
  font-family: var(--font-display); font-weight: 800; font-size: calc(100cqw / 4 * .38); line-height: 1;
}
.g2048-board div.d3 { font-size: calc(100cqw / 4 * .32); }
.g2048-board div.d4 { font-size: calc(100cqw / 4 * .26); }
.g2048-board div:is(.d5, .d6) { font-size: calc(100cqw / 4 * .2); }
.g2048-tiles { position: absolute; inset: var(--gap); pointer-events: none; }
.g2048-tile {
  position: absolute; left: 0; top: 0;
  width: calc((100% - 3 * var(--gap)) / 4); height: calc((100% - 3 * var(--gap)) / 4);
  transform: translate(calc(var(--c, 0) * (100% + var(--gap))), calc(var(--r, 0) * (100% + var(--gap))));
  transition: transform var(--t-move) var(--ease);
}
.g2048-tile > div { width: 100%; height: 100%; }
.g2048-tile.new > div { animation: tile-appear var(--t-move) var(--ease) var(--t-move) backwards; }
.g2048-tile.merged > div { animation: tile-merge var(--t-move) var(--ease); }
@keyframes tile-appear { from { transform: scale(0); } }
@keyframes tile-merge { 50% { transform: scale(1.14); } }
.v2 { --tb: var(--t2); --tf: var(--t2-fg, var(--t-ink)); }
.v4 { --tb: var(--t4); --tf: var(--t4-fg, var(--t-ink)); }
.v8 { --tb: var(--t8); --tf: var(--t8-fg, var(--t-paper)); }
.v16 { --tb: var(--t16); --tf: var(--t16-fg, var(--t-paper)); }
.v32 { --tb: var(--t32); --tf: var(--t32-fg, var(--t-paper)); }
.v64 { --tb: var(--t64); --tf: var(--t64-fg, var(--t-paper)); }
.v128 { --tb: var(--t128); --tf: var(--t128-fg, var(--t-paper)); }
.v256 { --tb: var(--t256); --tf: var(--t256-fg, var(--t-paper)); }
.v512 { --tb: var(--t512); --tf: var(--t512-fg, var(--t-paper)); }
.v1024 { --tb: var(--t1024); --tf: var(--t1024-fg, var(--t-paper)); }
.v2048 { --tb: var(--t2048); --tf: var(--t2048-fg, var(--t-paper)); }

/* the memory grid and number pad */
.mem-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; width: min(100%, 300px); aspect-ratio: 1; margin: 0 auto; }
.mem-grid i { border-radius: calc(var(--r-cell) * .9); background: var(--tile); }
.mem-grid i.lit { background: var(--flash); }
.mem-grid i.bad { background: var(--danger); }
.keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; width: min(100%, 300px); margin: 0 auto; }
.keypad button {
  display: grid; place-content: center; justify-items: center;
  height: 52px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  background: var(--surface); color: var(--text);
  font-family: var(--font-display); font-size: 1.3rem; font-weight: 650;
  cursor: pointer;
  transition: var(--fade);
}
.keypad button:hover:not(:disabled) { background: var(--surface-2); }
.keypad button:disabled { opacity: .4; cursor: default; }
/* A count under the digit (how many are left to place); the digit stays the key's face. */
.keypad button small { margin-top: 1px; font-family: var(--font); font-size: .6rem; font-weight: 600; line-height: 1; letter-spacing: 0; color: var(--muted); }
/* nine digits or more in one row (Sudoku): the pad takes the board's width, not 300px */
.keypad.wide { grid-template-columns: repeat(var(--keypad-cols, 9), minmax(0, 1fr)); gap: 6px; width: 100%; }
.keypad.wide button { height: 48px; font-size: 1.15rem; }
/* notes mode: the digits are pencilled in, so the keys say so - smaller and lighter */
.keypad[data-mode="notes"] button:not(.accent) { font-size: .95rem; font-weight: 400; font-style: italic; }
.keypad button.accent { background: var(--primary); border-color: transparent; color: var(--primary-contrast); }
.keypad button.accent:hover:not(:disabled) { background: var(--primary-strong); }

/* the card table and playing cards */
.table {
  position: relative;
  height: 230px;
  border-radius: var(--r-board);
  background: var(--table-bg);
  box-shadow: var(--table-frame);
  overflow: hidden;
  --cw: 70px; --ch: 98px;
}
.card {
  position: absolute; left: 0; top: 0;
  width: var(--cw); height: var(--ch);
  border-radius: calc(var(--cw) * var(--card-r));
}
.face, .back { position: absolute; inset: 0; border-radius: inherit; }
.face { display: none; overflow: hidden; background: var(--card-paper); box-shadow: var(--card-shadow); color: var(--suit-black); }
.card.up .face { display: block; }
.card.up .back { display: none; }
.face.red { color: var(--suit-red); }
/* Four-colour suits, a setting (data-four-color on <html>): diamonds blue, clubs green. */
[data-four-color="on"] .face.s-D { color: var(--suit-blue); }
[data-four-color="on"] .face.s-C { color: var(--suit-green); }
.pip { display: block; background: currentColor; -webkit-mask: var(--suit) center / contain no-repeat; mask: var(--suit) center / contain no-repeat; }
.s-S { --suit: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 4C55 17 94 39 94 63c0 13-10 21-21 21-9 0-15-4-19-10 1 10 5 17 12 22H34c7-5 11-12 12-22-4 6-10 10-19 10C16 84 6 76 6 63 6 39 45 17 50 4z'/%3E%3C/svg%3E"); }
.s-H { --suit: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 92C46 87 5 60 5 32 5 16 17 6 30 6c9 0 16 5 20 13 4-8 11-13 20-13 13 0 25 10 25 26 0 28-41 55-45 60z'/%3E%3C/svg%3E"); }
.s-D { --suit: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 3c10 17 23 32 38 47C73 65 60 80 50 97 40 80 27 65 12 50 27 35 40 20 50 3z'/%3E%3C/svg%3E"); }
.s-C { --suit: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='27' r='20'/%3E%3Ccircle cx='27' cy='58' r='20'/%3E%3Ccircle cx='73' cy='58' r='20'/%3E%3Ccircle cx='50' cy='52' r='12'/%3E%3Cpath d='M46 50h8l2 22c1 12 4 19 11 24H33c7-5 10-12 11-24z'/%3E%3C/svg%3E"); }
.idx { position: absolute; top: calc(var(--cw) * .025); display: flex; align-items: center; }
.idx.tl { left: calc(var(--cw) * .07); }
.idx b { font: 650 calc(var(--cw) * .3)/1 var(--font-card); letter-spacing: -0.03em; }
.ten .idx b { letter-spacing: -0.09em; margin-left: -0.04em; }
.idx.tl .pip { display: none; }
.idx.br { top: calc(var(--cw) * .075); right: calc(var(--cw) * .08); }
.idx.br b { display: none; }
.idx.br .pip { width: calc(var(--cw) * .22); height: calc(var(--cw) * .22); }
.mid { position: absolute; left: 0; right: 0; bottom: 0; height: calc(var(--ch, calc(var(--cw) * 1.4)) - var(--cw) * .38); display: grid; place-items: center; }
.mid .pip { width: calc(var(--cw) * .52); height: calc(var(--cw) * .52); margin-top: calc(var(--cw) * -.06); }
.court .mid {
  left: calc(var(--cw) * .12); right: calc(var(--cw) * .12); bottom: calc(var(--cw) * .12); top: calc(var(--cw) * .44); height: auto;
  border-radius: calc(var(--cw) * .04);
  background: var(--court-panel);
  box-shadow: var(--court-rule);
  /* the rank and its suit are stacked, not layered: one grid cell put the pip over the
     letter's tail (Q, J) */
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  gap: calc(var(--cw) * .1);
}
.court .mid b { font: 600 calc(var(--cw) * .44)/1 var(--font-card); }
.court .mid .pip { width: calc(var(--cw) * .19); height: calc(var(--cw) * .19); flex: none; }
.ace .mid .pip { width: calc(var(--cw) * .6); height: calc(var(--cw) * .6); }
.back { background: var(--card-back-art); box-shadow: inset 0 0 0 calc(var(--cw) * var(--back-edge-w)) var(--back-edge), var(--back-shadow); }
.game-icon .card { --ch: 56px; height: 56px; }
.slot {
  position: absolute;
  width: var(--cw); height: var(--ch);
  border-radius: calc(var(--cw) * var(--card-r));
  box-shadow: inset 0 0 0 1.5px var(--slot-line);
  display: grid; place-items: center;
  color: var(--slot-line); font: 600 calc(var(--cw) * .36)/1 var(--font-card);
}
.card.sel .face { box-shadow: 0 0 0 2px var(--accent), 0 0 0 5px color-mix(in srgb, var(--accent) 35%, transparent), 0 6px 14px rgba(0, 0, 0, .4); }

/* ---------- the theme switch ---------- */
/* Switching the theme repaints the whole surface at once, and that is the one change
   on this page worth animating: it is what the page is for. js/app.js wraps the
   re-render in a view transition where the browser has one, so the old and the new
   theme cross-fade instead of cutting. Everything else on the page stays still.
   The switch is skipped entirely under prefers-reduced-motion (see js/app.js). */
::view-transition-old(root), ::view-transition-new(root) {
  animation-duration: var(--t-slow);
  animation-timing-function: var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-delay: 0s !important; transition-duration: .01ms !important; transition-delay: 0s !important; }
  .celebration { display: none; }
}
/* The same for an app's own "Reduce motion" setting, which sets data-motion="reduce" on <html>. */
:root[data-motion="reduce"] *, :root[data-motion="reduce"] *::before, :root[data-motion="reduce"] *::after { animation-duration: .01ms !important; animation-delay: 0s !important; transition-duration: .01ms !important; transition-delay: 0s !important; }
:root[data-motion="reduce"] .celebration { display: none; }
