/*
 * OFC Board — the Chinese-poker scoreboard.
 * A graphite card-room canvas, oxidised-gold accents, monospace numbers; or,
 * in the light palette, warm paper and ink with the gold darkened to bronze.
 * Ported from trietment.com, where the scoreboard started; the chinezen-*
 * class names are kept as they were (the game is still called Chinezen).
 *
 * Page-specific styles live in their own files next to this one (landing.css,
 * groups.css, billing.css, admin.css, preferences.css); `stylesheet_link_tag
 * :app` loads them all.
 */

/* ── The palettes ──────────────────────────────────────────────────────────
 *
 * Every colour on the site is one of these tokens, in this file and in every
 * other stylesheet; test/stylesheets/palettes_test.rb fails on a colour
 * written anywhere else (it lists the few that are the same in both
 * palettes) and checks the contrast of the main pairs below.
 *
 * Dark is the default. Light comes from <html data-theme="light">, the
 * visitor's choice, or, when the visitor has not chosen (no data-theme at
 * all), from a device that prefers light. Plain CSS cannot point two
 * selectors at one block, so the light palette is written twice, once for
 * each way in; the test fails as soon as the two copies differ.
 * An explicit data-theme="dark" needs no block of its own: it is the default,
 * and it keeps the media query's copy away.
 *
 * DisplayPreferencesHelper::THEME_COLORS (the browser's own chrome) is each
 * palette's --c-bg; the test holds them together.
 *
 * Contrast (WCAG AA): text 4.5:1 on every surface it sits on, small text
 * included, and the --c-control edge of a field 3:1 against what is around
 * it; the test measures the main pairs in both palettes.
 *
 *   --c-bg              the page
 *   --c-bg-deep         the footer, and the foot of the page's backdrop
 *   --c-bg-2            a surface: cards, panels, the dialog, the
 *                       scoreboard's table
 *   --c-bg-3            inside a surface: table heads and totals, rows in
 *                       a panel
 *   --c-field           inputs and selects, edged in --c-control
 *   --c-header          the frosted header
 *   --c-text(-dim, -faint)   body text, secondary text, the least of it
 *   --c-accent          gold: links, labels, buttons; --c-accent-2 is its
 *                       hover, --c-on-accent the words on a gold button
 *   --c-border(-strong) hairlines
 *
 * In dark every layer is a step lighter than the one under it. In light
 * the surfaces are a step lighter than the paper and --c-bg-3 a step darker
 * than they are, so a table's head and totals still read as bands.
 * The -wash/-tint/-line tokens are the accent, danger and success colours at
 * a fixed strength (a marked row, a selected tab, a coloured border); light
 * needs a little more of each to show on paper.
 */

:root {
  color-scheme: dark;

  --c-bg: #1a1d23;
  --c-bg-deep: #16191e;
  --c-bg-2: #21252c;
  --c-bg-3: #282d35;
  --c-field: #2b3039;
  --c-header: rgba(33, 37, 44, 0.88);
  --c-backdrop: rgba(10, 12, 15, 0.72);
  --c-border: #30353f;
  --c-border-strong: #3d4450;
  --c-control: #787f8c;
  --c-text: #dfe1e6;
  --c-text-dim: #aab0bb;
  --c-text-faint: #959ba6;
  --c-accent: #c9a45c;
  --c-accent-2: #e9d8a6;
  --c-accent-cool: #7aa2a8;
  --c-on-accent: #15181d;
  --c-danger: #dc7b6f;
  --c-success: #7fad8b;

  --c-accent-hover: rgba(201, 164, 92, 0.05);
  --c-accent-wash: rgba(201, 164, 92, 0.08);
  --c-accent-tint: rgba(201, 164, 92, 0.12);
  --c-accent-line: rgba(201, 164, 92, 0.45);
  --c-accent-glow: rgba(201, 164, 92, 0.3);
  --c-danger-wash: rgba(220, 123, 111, 0.08);
  --c-danger-line: rgba(220, 123, 111, 0.45);
  --c-success-wash: rgba(127, 173, 139, 0.08);
  --c-success-line: rgba(127, 173, 139, 0.45);
  --c-cool-line: rgba(122, 162, 168, 0.45);

  --c-glow-warm: rgba(201, 164, 92, 0.06);
  --c-glow-cool: rgba(122, 162, 168, 0.05);
  --c-grid-line: rgba(201, 164, 92, 0.035);
  --noise-opacity: 0.35;
  --shadow-soft: 0 1px 0 rgba(201, 164, 92, 0.04), 0 20px 60px rgba(0, 0, 0, 0.4);

  /* One per player in the trend charts, in seating order; the scoreboard's
     are ChinezenChartHelper's, which these repaint per palette. Any two stay
     apart in lightness or hue for colour-blind players too, and each keeps
     3:1 on the card (test/stylesheets/chart_series_test.rb). */
  --c-series-1: #c9a45c;
  --c-series-2: #7aa2a8;
  --c-series-3: #b6574e;
  --c-series-4: #6f9b7a;
  --c-series-5: #e6acaa;
  --c-series-6: #9476c4;
}

:root[data-theme="light"] {
  color-scheme: light;

  --c-bg: #f4f1ea;
  --c-bg-deep: #efebe3;
  --c-bg-2: #fbf9f5;
  --c-bg-3: #ede8dd;
  --c-field: #fffefc;
  --c-header: rgba(251, 249, 245, 0.88);
  --c-backdrop: rgba(46, 40, 28, 0.38);
  --c-border: #dcd4c3;
  --c-border-strong: #c8bea8;
  --c-control: #8e846e;
  --c-text: #211e17;
  --c-text-dim: #514b3e;
  --c-text-faint: #6a6354;
  --c-accent: #806219;
  --c-accent-2: #654c12;
  --c-accent-cool: #3d6a71;
  --c-on-accent: #fffdf8;
  --c-danger: #9e4339;
  --c-success: #3f6d4b;

  --c-accent-hover: rgba(128, 98, 25, 0.06);
  --c-accent-wash: rgba(128, 98, 25, 0.1);
  --c-accent-tint: rgba(128, 98, 25, 0.14);
  --c-accent-line: rgba(128, 98, 25, 0.42);
  --c-accent-glow: rgba(128, 98, 25, 0.22);
  --c-danger-wash: rgba(158, 67, 57, 0.07);
  --c-danger-line: rgba(158, 67, 57, 0.4);
  --c-success-wash: rgba(63, 109, 75, 0.08);
  --c-success-line: rgba(63, 109, 75, 0.4);
  --c-cool-line: rgba(61, 106, 113, 0.4);

  --c-glow-warm: rgba(128, 98, 25, 0.07);
  --c-glow-cool: rgba(61, 106, 113, 0.06);
  --c-grid-line: rgba(128, 98, 25, 0.05);
  --noise-opacity: 0.06;
  --shadow-soft: 0 1px 0 rgba(128, 98, 25, 0.05), 0 18px 48px rgba(66, 52, 24, 0.12);

  --c-series-1: #806219;
  --c-series-2: #3f6f76;
  --c-series-3: #90364e;
  --c-series-4: #009c78;
  --c-series-5: #ba6c4e;
  --c-series-6: #6c4fa3;
}

/* The same light palette, for a device that prefers light when the visitor
   has not chosen. Keep it identical to the block above. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    color-scheme: light;

    --c-bg: #f4f1ea;
    --c-bg-deep: #efebe3;
    --c-bg-2: #fbf9f5;
    --c-bg-3: #ede8dd;
    --c-field: #fffefc;
    --c-header: rgba(251, 249, 245, 0.88);
    --c-backdrop: rgba(46, 40, 28, 0.38);
    --c-border: #dcd4c3;
    --c-border-strong: #c8bea8;
    --c-control: #8e846e;
    --c-text: #211e17;
    --c-text-dim: #514b3e;
    --c-text-faint: #6a6354;
    --c-accent: #806219;
    --c-accent-2: #654c12;
    --c-accent-cool: #3d6a71;
    --c-on-accent: #fffdf8;
    --c-danger: #9e4339;
    --c-success: #3f6d4b;

    --c-accent-hover: rgba(128, 98, 25, 0.06);
    --c-accent-wash: rgba(128, 98, 25, 0.1);
    --c-accent-tint: rgba(128, 98, 25, 0.14);
    --c-accent-line: rgba(128, 98, 25, 0.42);
    --c-accent-glow: rgba(128, 98, 25, 0.22);
    --c-danger-wash: rgba(158, 67, 57, 0.07);
    --c-danger-line: rgba(158, 67, 57, 0.4);
    --c-success-wash: rgba(63, 109, 75, 0.08);
    --c-success-line: rgba(63, 109, 75, 0.4);
    --c-cool-line: rgba(61, 106, 113, 0.4);

    --c-glow-warm: rgba(128, 98, 25, 0.07);
    --c-glow-cool: rgba(61, 106, 113, 0.06);
    --c-grid-line: rgba(128, 98, 25, 0.05);
    --noise-opacity: 0.06;
    --shadow-soft: 0 1px 0 rgba(128, 98, 25, 0.05), 0 18px 48px rgba(66, 52, 24, 0.12);

    --c-series-1: #806219;
    --c-series-2: #3f6f76;
    --c-series-3: #90364e;
    --c-series-4: #009c78;
    --c-series-5: #ba6c4e;
    --c-series-6: #6c4fa3;
  }
}

/* Type, radii and widths: the same in both palettes. */
:root {
  --f-serif: "Cormorant Garamond", "Times New Roman", serif;
  --f-sans: "Inter", system-ui, -apple-system, sans-serif;
  --f-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --r-sm: 2px;
  --r-md: 4px;

  --w-container: 1180px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--c-bg);
  color: var(--c-text);
  /* Checkboxes and radio buttons in the house gold, in either palette. */
  accent-color: var(--c-accent);
  font-family: var(--f-sans);
  font-weight: 300;
  line-height: 1.65;
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100%;
}

body {
  background: var(--c-bg);
  overflow-x: hidden;
}

/* Fixed gradient backdrop on its own compositor layer.
   Replaces `background-attachment: fixed` on <body>, which forced the
   browser to re-rasterise the whole background on every scroll frame —
   the classic cause of the page visibly shaking/tearing while scrolling,
   worst on mobile. A position:fixed pseudo-element gives the identical
   "background stays put" look but is composited once and just stays
   parked, so scrolling no longer repaints it. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(1200px 600px at 85% -10%, var(--c-glow-warm), transparent 60%),
    radial-gradient(900px 500px at -10% 20%, var(--c-glow-cool), transparent 55%),
    linear-gradient(180deg, var(--c-bg) 0%, var(--c-bg-deep) 100%);
}

/* ambient textures */
/* The grain is white specks in both palettes, blended with overlay: they
   lift a dark page and all but vanish on paper, so light turns the layer
   nearly off (--noise-opacity) rather than painting it another colour. */
.noise {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  opacity: var(--noise-opacity);
  mix-blend-mode: overlay;
  background-image:
    radial-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
    radial-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px);
  background-size: 3px 3px, 7px 7px;
  background-position: 0 0, 1px 1px;
}

.grid-overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background-image:
    linear-gradient(var(--c-grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--c-grid-line) 1px, transparent 1px);
  background-size: 80px 80px;
  mask-image: radial-gradient(ellipse at 50% 20%, black 40%, transparent 80%);
}

main, .site-header, .site-footer { position: relative; z-index: 1; }

/* typography */
h1, h2, h3, h4 {
  font-family: var(--f-serif);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--c-text);
  margin: 0 0 0.6em;
  line-height: 1.15;
}
h1 { font-size: clamp(2.4rem, 4.8vw, 4rem); letter-spacing: -0.01em; }
h2 { font-size: clamp(1.9rem, 3vw, 2.6rem); }
h3 { font-size: 1.5rem; }
h4 { font-size: 1rem; font-family: var(--f-sans); font-weight: 600; text-transform: uppercase; letter-spacing: 0.14em; color: var(--c-accent); }

p { margin: 0 0 1em; color: var(--c-text); }
.muted { color: var(--c-text-dim); }
.small { font-size: 0.85rem; }
.mono  { font-family: var(--f-mono); letter-spacing: 0.02em; }
em     { color: var(--c-accent-2); font-style: italic; }
strong { color: var(--c-text); font-weight: 500; }

a {
  color: var(--c-accent);
  text-decoration: none;
  transition: color 150ms ease, border-color 150ms ease;
  border-bottom: 1px solid transparent;
}
a:hover {
  color: var(--c-accent-2);
  border-bottom-color: var(--c-accent-line);
}

hr {
  border: 0;
  border-top: 1px solid var(--c-border);
  margin: 3rem 0;
}

/* layout */
.container {
  width: 100%;
  max-width: var(--w-container);
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* Vertical rhythm only, with padding-block: these classes often sit on the
   same element as .container ("section container"), and a `padding: X 0`
   shorthand would wipe the side gutter .container gives. */
.section { padding-block: 4rem; }
.section-tight { padding-block: 2rem; }

/* Section variant for stacked panels (e.g. /account) — they sit just below
   the previous one with minimal vertical breathing room. */
.section-stacked { padding-block: 1rem; }
.section-stacked:first-of-type { padding-top: 0; }

/* Turbo's own progress bar (its <style> is let in by nonce, see
   content_security_policy.rb), in the house colour instead of its blue. */
.turbo-progress-bar { background: var(--c-accent); }

/* header */
.site-header {
  border-bottom: 1px solid var(--c-border);
  background: var(--c-header);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  position: sticky;
  top: 0;
  z-index: 20;
}
/* The header stays put, so whatever the browser scrolls into view (a field
   reached going back up with Shift+Tab, an anchor's section) would end up
   underneath it (WCAG 2.4.11). Everything keeps a margin as tall as the
   header when it is scrolled to: one row (75px) here, two where a tablet's
   wraps; a phone's three rows, and the scoreboard's tab bar, are in "Header
   on a phone". Everything but what sits in a sticky bar (the header, the
   tab bar, the score table's head): the bar holds it in view, and room
   made above it only scrolled what lies underneath. That is also why this
   is a margin on each element and not the root's scroll-padding, which
   counts the bars' own controls too: Tab into the header, the theme toggle
   handing its focus to its other button, or an arrow key in the tab bar
   threw the page half a screen up. */
:not(.site-header *, .chinezen-tabs *, .chinezen-table thead *) { scroll-margin-top: 6rem; }
@media (max-width: 1100px) {
  :not(.site-header *, .chinezen-tabs *, .chinezen-table thead *) { scroll-margin-top: 9rem; }
}
/* Brand, menu, account buttons and the switches (preferences.css) do not all
   fit one row on a tablet: the header wraps instead of running off the
   screen, and what wraps keeps to the right. On a tablet the switches sit
   on the brand's row, as on a phone, so what wraps is the account box
   (Sign in and Try it free, or Admin, "Signed in as" and Sign out) rather
   than EN | NL and the toggle on a row of their own. A phone gets its own
   arrangement (Header on a phone, below). */
.header-inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0.75rem 2rem;
  padding-top: 1.1rem;
  padding-bottom: 1.1rem;
}
.header-inner > .brand { margin-right: auto; }
@media (min-width: 701px) and (max-width: 1023px) {
  .header-inner > .site-prefs { order: 1; }
  .header-inner > .site-nav,
  .header-inner > .site-utility { order: 2; }
}
.brand {
  display: inline-flex;
  align-items: baseline;
  gap: 0.6rem;
  border: none !important;
  color: var(--c-text) !important;
}
.brand:hover { color: var(--c-accent-2) !important; }
.brand-mark {
  font-family: var(--f-serif);
  font-size: 1.6rem;
  font-weight: 600;
  color: var(--c-accent);
  line-height: 1;
}
.brand-dot { color: var(--c-accent-cool); }
.brand-word {
  font-family: var(--f-mono);
  font-size: 0.78rem;
  letter-spacing: 0.38em;
  color: var(--c-text-dim);
}

.site-nav {
  display: flex;
  gap: 1.75rem;
}
.site-nav a {
  color: var(--c-text-dim);
  font-size: 0.82rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  border: none;
  padding: 0.25rem 0;
  position: relative;
}
.site-nav a:hover { color: var(--c-text); }
.site-nav a.active { color: var(--c-accent); }
.site-nav a.active::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -6px;
  height: 1px;
  background: var(--c-accent);
  opacity: 0.6;
}

.site-utility {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
/* "Signed in as <name>": a long name is cut short with an ellipsis rather
   than widening the header (names run to 121 characters). */
.util-label {
  color: var(--c-text-dim);
  font-size: 0.8rem;
  font-family: var(--f-mono);
  min-width: 0;
  max-width: 20rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.util-label strong { color: var(--c-accent); font-weight: 500; }
.util-link { color: var(--c-accent); font-weight: 500; border: none; white-space: nowrap; }
.util-link:hover { color: var(--c-accent-2); }

/* buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.85rem 1.5rem;
  background: var(--c-accent);
  color: var(--c-on-accent);
  border: 1px solid var(--c-accent);
  border-radius: var(--r-sm);
  font-family: var(--f-mono);
  font-size: 0.78rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  cursor: pointer;
  transition: all 180ms ease;
  text-decoration: none !important;
  /* A word too long for its button breaks between syllables, in the page's
     language (<html lang>): TWEESTAPSVERIFICATIE is wider than a 320px
     phone's card. A label that fits is left alone. */
  -webkit-hyphens: auto;
  hyphens: auto;
}
.btn:hover {
  background: var(--c-accent-2);
  border-color: var(--c-accent-2);
  color: var(--c-on-accent);
  transform: translateY(-1px);
}
.btn-ghost {
  background: transparent;
  color: var(--c-accent);
  border-color: var(--c-border-strong);
}
.btn-ghost:hover {
  background: transparent;
  color: var(--c-accent-2);
  border-color: var(--c-accent);
}
.btn-small { padding: 0.55rem 1rem; font-size: 0.72rem; }
.btn-xs { padding: 0.3rem 0.6rem; font-size: 0.64rem; letter-spacing: 0.14em; }
.btn-danger {
  background: transparent;
  color: var(--c-danger);
  border-color: var(--c-danger-line);
}
.btn-danger:hover { background: var(--c-danger-wash); color: var(--c-danger); border-color: var(--c-danger); }

/* hero */
.hero {
  padding-block: 7rem 5rem;
  position: relative;
}
.hero-kicker {
  font-family: var(--f-mono);
  font-size: 0.78rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--c-accent);
  margin-bottom: 1.5rem;
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
}
.hero-kicker::before {
  content: "";
  display: inline-block;
  width: 36px;
  height: 1px;
  background: var(--c-accent);
}
.hero h1 {
  max-width: 22ch;
  margin-bottom: 1.5rem;
}
.hero h1 .accent { color: var(--c-accent); font-style: italic; }
.hero-lead {
  max-width: 58ch;
  font-size: 1.15rem;
  color: var(--c-text-dim);
  margin-bottom: 2.5rem;
}
.hero-cta { display: flex; gap: 1rem; flex-wrap: wrap; }

/* cards / features */
.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}
.grid-2 {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2rem;
}
@media (max-width: 900px) {
  .grid-3 { grid-template-columns: 1fr; }
  .grid-2 { grid-template-columns: 1fr; }
}

.card {
  background: var(--c-bg-2);
  border: 1px solid var(--c-border);
  padding: 2rem;
  border-radius: var(--r-md);
  transition: border-color 220ms ease, transform 220ms ease;
  position: relative;
  overflow: hidden;
}
.card::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--c-accent), transparent);
  opacity: 0.3;
}
.card:hover {
  border-color: var(--c-border-strong);
  transform: translateY(-2px);
}
.card-index {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  color: var(--c-accent);
  letter-spacing: 0.22em;
  margin-bottom: 1rem;
}
.card h3 { margin-bottom: 0.75rem; }

/* section label */
.eyebrow {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--c-accent);
  margin-bottom: 1rem;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
}
.eyebrow::before {
  content: "//";
  color: var(--c-text-faint);
}

.section-heading { max-width: 58ch; margin-bottom: 3rem; }
.section-heading p { color: var(--c-text-dim); font-size: 1.05rem; }

/* stats ribbon */
.ribbon {
  border-top: 1px solid var(--c-border);
  border-bottom: 1px solid var(--c-border);
  padding: 2.5rem 0;
  background: var(--c-bg-2);
}
.ribbon-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 2rem;
}
@media (max-width: 900px) { .ribbon-grid { grid-template-columns: repeat(2, 1fr); } }
.ribbon-item .num {
  font-family: var(--f-serif);
  font-size: 2.4rem;
  color: var(--c-accent);
  line-height: 1;
  display: block;
}
.ribbon-item .lbl {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--c-text-dim);
  margin-top: 0.6rem;
  display: block;
}

/* approach steps */
.steps { counter-reset: step; display: grid; gap: 1rem; }
.step {
  display: grid;
  grid-template-columns: 80px 1fr;
  gap: 2rem;
  padding: 2rem 0;
  border-top: 1px solid var(--c-border);
  counter-increment: step;
  align-items: start;
}
.step:last-child { border-bottom: 1px solid var(--c-border); }
.step-num {
  font-family: var(--f-mono);
  color: var(--c-accent);
  font-size: 1rem;
  letter-spacing: 0.14em;
  padding-top: 0.4rem;
}
.step-num::before { content: "0" counter(step); }
@media (max-width: 700px) {
  .step { grid-template-columns: 1fr; gap: 0.5rem; }
}

/* row list (games, members, presets) */
.posts-list { display: grid; gap: 0; }
.post-row {
  display: grid;
  grid-template-columns: 140px 1fr auto;
  gap: 2rem;
  padding: 2rem 0;
  border-top: 1px solid var(--c-border);
  align-items: baseline;
  transition: background 200ms ease;
}
.post-row:last-child { border-bottom: 1px solid var(--c-border); }
.post-row:hover { background: linear-gradient(90deg, var(--c-accent-hover), transparent); }
.post-meta {
  font-family: var(--f-mono);
  font-size: 0.78rem;
  color: var(--c-text-dim);
  letter-spacing: 0.06em;
}
.post-row h3 { margin: 0 0 0.4rem; font-size: 1.6rem; }
.post-row h3 a { color: var(--c-text); border: none; }
.post-row h3 a:hover { color: var(--c-accent); }
.post-excerpt { color: var(--c-text-dim); margin: 0; }
.post-status-tag {
  font-family: var(--f-mono);
  font-size: 0.68rem;
  letter-spacing: 0.2em;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-sm);
  color: var(--c-text-dim);
  text-transform: uppercase;
}
.post-status-tag.draft { color: var(--c-danger); border-color: var(--c-danger-line); }
@media (max-width: 800px) {
  .post-row { grid-template-columns: 1fr; gap: 0.5rem; }
}

/* forms */
.form-wrap {
  max-width: 520px;
  margin: 4rem auto;
  background: var(--c-bg-2);
  border: 1px solid var(--c-border);
  padding: 3rem;
  border-radius: var(--r-md);
  position: relative;
}
.form-wrap::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--r-md);
  padding: 1px;
  background: linear-gradient(180deg, var(--c-accent-glow), transparent 40%);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}
.form-wrap h1, .form-wrap h2 { margin-bottom: 0.4em; }
.form-wrap .lead { color: var(--c-text-dim); margin-bottom: 2rem; }

.field { margin-bottom: 1.4rem; display: flex; flex-direction: column; gap: 0.5rem; }
.field label {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--c-text-dim);
}
/* Fields sit a step above whatever they are on, edged at 3:1 so they can be
   found. Numbers, dates and selects were left to the browser, which drew
   them white on the dark page: they join in here under :where(), which
   weighs nothing, so a field's own class (.chinezen-score-input, the
   .group-select) still has the last word. */
.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="datetime-local"],
.field textarea,
input[type="text"],
input[type="email"],
input[type="password"],
input[type="datetime-local"],
textarea,
:where(input[type="number"], input[type="date"], input[type="search"], input[type="tel"], input[type="url"], select) {
  background: var(--c-field);
  border: 1px solid var(--c-control);
  color: var(--c-text);
  padding: 0.85rem 1rem;
  border-radius: var(--r-sm);
  font-family: var(--f-sans);
  font-size: 1rem;
  transition: border-color 160ms ease, box-shadow 160ms ease;
  width: 100%;
}
textarea { min-height: 220px; font-family: var(--f-mono); font-size: 0.95rem; line-height: 1.7; }
::placeholder { color: var(--c-text-faint); opacity: 1; }
input:focus, textarea:focus, select:focus {
  outline: none;
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px var(--c-accent-tint);
}
/* A checkbox has no border of ours to light up: give the keyboard a ring. */
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
/* Nor has a button: a gold one already wears the gold border, and the rule
   above it (input:focus matches a submit button too) took its ring away. */
.btn:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* Wraps where its buttons do not fit one row: a submit's label cannot
   wrap, and Cancel would go off the screen with it. */
.form-actions { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; margin-top: 1.5rem; }
.form-footer { margin-top: 2rem; text-align: center; color: var(--c-text-dim); font-size: 0.9rem; }

.error-summary {
  border: 1px solid var(--c-danger-line);
  background: var(--c-danger-wash);
  padding: 1rem 1.2rem;
  border-radius: var(--r-sm);
  margin-bottom: 1.5rem;
  color: var(--c-danger);
  font-family: var(--f-mono);
  font-size: 0.85rem;
}
.error-summary h4 { color: var(--c-danger); margin: 0 0 0.4rem; }

/* flash */
.flash-wrap { padding-top: 1.5rem; }
.flash {
  padding: 0.8rem 1.2rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--c-border-strong);
  font-family: var(--f-mono);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  margin: 0 0 0.6rem;
}
.flash-notice { color: var(--c-success); border-color: var(--c-success-line); background: var(--c-success-wash); }
.flash-alert  { color: var(--c-danger);  border-color: var(--c-danger-line);  background: var(--c-danger-wash); }

/* confirm dialog — styled replacement for the native turbo_confirm prompt */
.confirm-dialog {
  padding: 0;
  width: calc(100% - 3rem);
  max-width: 30rem;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-md);
  background: var(--c-bg-2);
  color: var(--c-text);
  box-shadow: var(--shadow-soft);
}
.confirm-dialog::backdrop {
  background: var(--c-backdrop);
  backdrop-filter: blur(2px);
}
.confirm-dialog-card {
  margin: 0;
  padding: 1.6rem 1.6rem 1.35rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.confirm-dialog-eyebrow {
  margin: 0;
  font-family: var(--f-mono);
  font-size: 0.68rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--c-text-faint);
}
.confirm-dialog-message {
  margin: 0;
  font-family: var(--f-serif);
  font-size: 1.3rem;
  line-height: 1.4;
  color: var(--c-text);
}
.confirm-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.6rem;
  margin-top: 1.1rem;
}
/* Subtle fade-in; degrades to an instant appearance where @starting-style
   or transition-behavior is unsupported. */
.confirm-dialog,
.confirm-dialog::backdrop {
  opacity: 0;
  transition: opacity 160ms ease, overlay 160ms ease allow-discrete, display 160ms ease allow-discrete;
}
.confirm-dialog[open],
.confirm-dialog[open]::backdrop {
  opacity: 1;
}
@starting-style {
  .confirm-dialog[open],
  .confirm-dialog[open]::backdrop {
    opacity: 0;
  }
}

/* footer */
.site-footer {
  border-top: 1px solid var(--c-border);
  background: var(--c-bg-deep);
  padding: 4rem 0 1.5rem;
  margin-top: 6rem;
}
.footer-grid {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1fr;
  gap: 3rem;
  padding-bottom: 3rem;
  border-bottom: 1px solid var(--c-border);
}
@media (max-width: 900px) { .footer-grid { grid-template-columns: 1fr 1fr; } }
.footer-brand p { max-width: 40ch; margin-top: 1rem; }
.footer-list { list-style: none; padding: 0; margin: 0; }
.footer-list li { margin-bottom: 0.6rem; }
.footer-list a { color: var(--c-text-dim); border: none; font-size: 0.92rem; }
.footer-list a:hover { color: var(--c-accent); }
.site-footer h4 { color: var(--c-text-dim); font-size: 0.72rem; letter-spacing: 0.26em; margin-bottom: 1.2rem; }

.footer-bottom {
  padding-top: 1.5rem;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

/* contact split */
.split {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: 4rem;
  align-items: start;
}
@media (max-width: 900px) { .split { grid-template-columns: 1fr; gap: 2rem; } }

.detail-list { list-style: none; padding: 0; margin: 0; }
.detail-list li {
  padding: 1.2rem 0;
  border-top: 1px solid var(--c-border);
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: 1.5rem;
  align-items: baseline;
}
.detail-list li:last-child { border-bottom: 1px solid var(--c-border); }
.detail-list .key {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--c-text-dim);
}
.detail-list .val { color: var(--c-text); }

/* quote block */
.quote-block {
  border-left: 1px solid var(--c-accent);
  padding: 1rem 0 1rem 2rem;
  font-family: var(--f-serif);
  font-size: 1.6rem;
  line-height: 1.4;
  font-style: italic;
  color: var(--c-text);
  max-width: 40ch;
}
.quote-attr {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--c-text-dim);
  margin-top: 1rem;
  font-style: normal;
}

/* divider ornament */
.ornament {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 3rem 0;
  color: var(--c-accent);
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  justify-content: center;
}
.ornament::before, .ornament::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--c-border-strong), transparent);
}

/* selection */
::selection { background: var(--c-accent-glow); color: var(--c-text); }

/* sign-in scene */
.auth-scene {
  min-height: 72vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3rem 1rem;
}

/* "or" divider between the passphrase form and the passkey button */
.auth-divider {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  margin: 1.5rem 0 1rem;
}
.auth-divider-line {
  flex: 1;
  height: 1px;
  background: var(--c-border);
}
.auth-divider-text { color: var(--c-text-dim); letter-spacing: 0.18em; text-transform: uppercase; }
.auth-passkey-row { gap: 0.8rem; flex-wrap: wrap; }

/* Passkey management on /account */
.passkey-list {
  list-style: none;
  margin: 1rem 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.passkey-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background: var(--c-bg-3);
}
.passkey-nick {
  font-family: var(--f-sans);
  font-size: 1rem;
  color: var(--c-text);
}
.passkey-add {
  margin-top: 1.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--c-border);
}
.passkey-add h3 {
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-size: 0.7rem;
  margin: 0 0 0.6rem;
  color: var(--c-text-dim);
}

/* show/edit actions */
.action-row { display: flex; gap: 1rem; flex-wrap: wrap; margin: 2rem 0; }

/* terminal-ish list */
.terminal-list { list-style: none; padding: 0; margin: 0; }
.terminal-list li {
  font-family: var(--f-mono);
  font-size: 0.9rem;
  color: var(--c-text-dim);
  padding: 0.4rem 0;
  padding-left: 1.4rem;
  position: relative;
}
.terminal-list li::before {
  content: "→";
  position: absolute;
  left: 0;
  color: var(--c-accent);
}

/* ── Two-factor setup ── */

/* QR codes stay black on white in both palettes: banking and authenticator
   apps read dark modules on a light ground, and some refuse the inverse.
   On paper the white tile needs its edge drawn. */
.otp-qr {
  background: #fff;
  border: 1px solid var(--c-border);
  padding: 1rem;
  border-radius: var(--r-md);
  margin: 0 auto 1.5rem;
  max-width: 256px;
}
.otp-qr svg { display: block; width: 100%; height: auto; }

.otp-secret {
  background: var(--c-bg-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  padding: 0.7rem 0.9rem;
  word-break: break-all;
  color: var(--c-accent);
  margin-bottom: 1.5rem;
}

.recovery-codes {
  list-style: none;
  padding: 1rem 1.4rem;
  margin: 0 0 1.5rem;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.45rem 1.5rem;
  background: var(--c-bg-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
}
.recovery-codes li {
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  color: var(--c-accent-2);
}
@media (max-width: 500px) {
  .recovery-codes { grid-template-columns: 1fr; }
}

/* ── Form & content layout helpers ── */

/* form pages: section padding + narrow content well */
.section-form { padding-block: 4rem; }
.section-narrow { max-width: 48rem; }
.form-wrap-narrow { max-width: 780px; }

/* centered prose / CTA block within a section */
.text-block-center {
  text-align: center;
  max-width: 60ch;
  margin: 0 auto;
}
.action-row.center {
  justify-content: center;
  margin-top: 2.5rem;
}

/* empty-state card (e.g. a group with no games yet) */
.empty-state {
  text-align: center;
  padding: 4rem 2rem;
}

/* checkbox-with-label inline row (form labels containing a checkbox) */
.checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
}

/* tighter two-column grid for form fields side by side */
.grid-2.grid-2-tight { gap: 1rem; }

/* user listing row variants */
.user-row-name { margin: 0; }
.user-row-meta { margin-top: 0.25rem; }
/* Wraps rather than widening its row: on a phone a row's column is as wide
   as what it holds, and three Dutch buttons (Gebruiken, Bewerken,
   Verwijderen) pushed a saved table past the screen's edge. */
.user-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

/* small contextual spacing */
.post-byline { margin-top: 0.6rem; }
.contact-fallback { margin-top: 0.75rem; }

/* ── Responsive ── */

/* ── Header on a phone ─────────────────────────────────────────────────────
 *
 * Three rows at most, none of them wider than a 360px screen:
 *   1. the brand, with EN | NL and the light/dark toggle at its right;
 *   2. the account buttons (Sign in and Try it free, or Admin and Sign out),
 *      at the right; they join the first row on a phone wide enough;
 *   3. under a hairline, the menu, with "Signed in as <name>" filling the
 *      rest of the row and cut short with an ellipsis when it must be.
 * The label used to take a row of its own above its buttons. .site-utility
 * steps aside (display: contents) so the buttons and the label can go to
 * different rows; .header-inner::after is the hairline and the line break:
 * a full-width flex item between the buttons and the menu.
 *
 * The scoreboard (body.chinezen-focus) keeps one row: the brand, the toggle
 * and Sign out, on a 360px screen too, so the board starts that much higher.
 * The rest waits elsewhere while a game is on: EN | NL in the footer, the
 * menu, "Signed in as" and the admin console on every other page.
 */
@media (max-width: 700px) {
  .header-inner {
    gap: 0.6rem 0.5rem;
    padding-top: 0.85rem;
    padding-bottom: 0;
  }
  .header-inner > .site-prefs { order: 1; }
  .header-inner > .site-utility { display: contents; }
  .site-utility > * { order: 2; }
  /* A little less air in the account buttons: Sign out (UITLOGGEN) then
     fits beside the brand and the toggle on the scoreboard. */
  .site-utility .btn-small { padding-inline: 0.8rem; letter-spacing: 0.16em; }
  .header-inner::after {
    content: "";
    order: 9;
    flex: 0 0 100%;
    height: 1px;
    background: var(--c-border);
  }
  .header-inner > .site-nav {
    order: 10;
    flex: 0 0 auto;
    max-width: 100%;
    margin-left: 0;
    margin-right: auto;
    gap: 1rem;
    padding: 0.2rem 0 0.85rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .site-nav::-webkit-scrollbar { display: none; }
  /* tighter type so the menu fits ~360px without scrolling */
  .site-nav a {
    font-size: 0.72rem;
    letter-spacing: 0.10em;
  }
  .site-utility > .util-label {
    order: 11;
    flex: 1 1 0;
    max-width: none;
    padding: 0.2rem 0 0.85rem;
    text-align: right;
  }

  /* The layout links Admin to admin_root_path, which is /admin. The label is
     the header's own: the board's "Watching only" note is a .util-label too,
     and stays. */
  body.chinezen-focus .site-nav,
  body.chinezen-focus .site-header .util-label,
  body.chinezen-focus .site-header .locale-switch,
  body.chinezen-focus .site-utility > a[href="/admin"],
  body.chinezen-focus .header-inner::after { display: none; }
  body.chinezen-focus .header-inner { padding-block: 0.6rem; }
  body.chinezen-focus .header-inner > .site-utility { display: flex; gap: 0.5rem; order: 2; }
  /* One sticky bar on the scoreboard: its tab bar. The bar lives in main,
     whose z-index puts all of it under the header's 20, so a sticky header
     covered the stuck bar for good; here the header scrolls away instead.
     Relative, not static: it keeps the header above the noise and grid. */
  body.chinezen-focus .site-header { position: relative; }

  /* As tall as the header's three rows; on the scoreboard, where the header
     scrolls away, as the tab bar. */
  :not(.site-header *, .chinezen-tabs *, .chinezen-table thead *) { scroll-margin-top: 11rem; }
  .chinezen-focus :not(.site-header *, .chinezen-tabs *, .chinezen-table thead *) { scroll-margin-top: 4.5rem; }
}

/* Below 360px the scoreboard's brand, toggle and Sign out need 289px
   (UITLOGGEN 297) of the 272px a 320px phone has, and Sign out took a
   second row for the whole game. The ♠. stays, and stays the home link,
   named by its aria-label; the footer's word mark is left alone. */
@media (max-width: 359px) {
  body.chinezen-focus .site-header .brand-word { display: none; }
}

/* hero and section vertical rhythm on phones */
@media (max-width: 600px) {
  .hero { padding-block: 3.5rem 2.5rem; }
  .section { padding-block: 2.5rem; }
  .site-footer { padding-top: 3rem; margin-top: 4rem; }
  .form-wrap { padding: 2rem 1.5rem; margin-top: 2rem; margin-bottom: 2rem; }
}

/* footer: single column below 500 px (already 2-col at 900px) */
/* detail-list: collapse key/val to stacked rows on very narrow screens */
@media (max-width: 500px) {
  .footer-grid { grid-template-columns: 1fr; gap: 2rem; }
  .detail-list li { grid-template-columns: 1fr; gap: 0.25rem; }
}


/* ── Chinezen scoreboard ───────────────────────────────────────────────── */

.chinezen-header {
  padding-top: 4rem;
  padding-bottom: 1.5rem;
}
.chinezen-title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}
.chinezen-title-row h1 { margin-bottom: 0.5rem; font-size: clamp(2rem, 4vw, 2.8rem); }
.chinezen-actions {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
  align-items: center;
}
.chinezen-live { color: var(--c-accent); }
/* The note a member who is not playing sees ("Watching only") wears the
   header label's look but not its ellipsis: it is a sentence, so it wraps
   rather than being cut short at 20rem. */
.chinezen-actions > .util-label { max-width: none; white-space: normal; }

/* The audio toggle uses an emoji + label; let the letterspacing relax
   a touch so the headphone glyph doesn't crowd "Audio on/off". */
.chinezen-voice-toggle { letter-spacing: 0.12em; }
.chinezen-voice-toggle[aria-pressed="true"] {
  color: var(--c-accent-2);
  border-color: var(--c-accent);
}

/* padding-block, not the shorthand: the board sits on .container (see
   .section), and the shorthand wiped the side gutter it gives. */
.chinezen-board { padding-block: 1rem 3rem; }

.chinezen-table-wrap {
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  /* Vertical AND horizontal scroll inside the wrap. Going through the
     wrap (instead of letting the page scroll) is what makes the sticky
     thead and totals row actually stick — sticky binds to the nearest
     scrolling ancestor. */
  overflow: auto;
  /* Fixed cap so the score table doesn't keep growing as more rounds
     pile up. ~5 round rows visible (header ~30px + 5 × 38px + totals
     ~40px = ~260px) + a bit of breathing room. */
  max-height: 280px;
  background: var(--c-bg-2);
  /* iOS Safari: keep the table touch-scroll friendly. */
  -webkit-overflow-scrolling: touch;
  /* Stop scroll-chaining: when the inner table reaches its top/bottom,
     don't hand the momentum off to the page (which reads as a jump /
     shudder on touch devices). */
  overscroll-behavior: contain;
}

.chinezen-table {
  width: 100%;
  min-width: 560px;
  border-collapse: separate;
  border-spacing: 0;
  font-family: var(--f-mono);
  font-size: 0.92rem;
}
.chinezen-table th,
.chinezen-table td {
  padding: 0.6rem 0.9rem;
  text-align: right;
  border-bottom: 1px solid var(--c-border);
}
.chinezen-table th {
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-text-dim);
  font-weight: 500;
  /* Opaque background is required so scrolling rows don't bleed
     through the sticky header. */
  background: var(--c-bg-3);
  position: sticky;
  top: 0;
  z-index: 3;
}
.chinezen-table .num-col,
.chinezen-table .ruilen-col {
  text-align: center;
  color: var(--c-text-dim);
  width: 1%;
  white-space: nowrap;
}
.chinezen-table tbody tr:hover { background: var(--c-accent-hover); }
.chinezen-table .neg { color: var(--c-danger); }
.chinezen-table .pos { color: var(--c-success); }

/* Pin the running point totals to the bottom of the scrolling wrap so
   the user can see each player's tally while paging through old rounds. */
.chinezen-totals-row td {
  background: var(--c-bg-3);
  border-top: 1px solid var(--c-border-strong);
  font-size: 1rem;
  letter-spacing: 0.04em;
  position: sticky;
  bottom: 0;
  z-index: 2;
}
.chinezen-empty-row td {
  text-align: center !important;
  padding: 2rem 1rem;
  color: var(--c-text-faint);
}

.chinezen-add-round {
  margin-top: 2rem;
  padding: 1.5rem;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-bg-2);
}
.chinezen-add-round h3 {
  font-family: var(--f-sans);
  font-size: 0.85rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-accent);
  margin-bottom: 1rem;
}
.chinezen-add-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem 1rem;
  align-items: flex-end;
}
/* As wide as its longest choice ("X · tegenover" in Dutch), not a fixed 100px
   that cut it short once the select got a field's padding. */
.chinezen-swap-field { min-width: 100px; }

/* Draft-round panel: per-player rows where each player saves their own
   score independently. Owner-only inputs glow gold; locked seats show the
   saved value as plain text or "waiting" until that player submits. */
.chinezen-draft .chinezen-draft-tag {
  display: inline-block;
  margin-left: 0.6rem;
  padding: 0.05rem 0.45rem;
  font-size: 0.65rem;
  font-family: var(--f-mono);
  letter-spacing: 0.12em;
  color: var(--c-accent);
  border: 1px solid var(--c-accent-line);
  border-radius: 999px;
  background: var(--c-accent-wash);
  text-transform: uppercase;
}
.chinezen-draft-rows {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.chinezen-draft-row {
  display: flex;
  /* Only a refused number's line wraps, onto a line of its own. */
  flex-wrap: wrap;
  align-items: center;
  gap: 0.8rem;
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background: var(--c-bg-3);
}
.chinezen-draft-row.is-mine {
  border-color: var(--c-accent-line);
  background: var(--c-accent-wash);
}
/* Sitting this hand out: a dashed row with the name in the faintest text.
   Not opacity, which took the row's Back in button and its "sits out" under
   AA contrast along with the name (see "Dimmed, never faded" below). */
.chinezen-draft-row.is-sat-out { border-style: dashed; }
.chinezen-draft-row.is-sat-out .chinezen-draft-name { color: var(--c-text-faint); }
.chinezen-draft-who {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.chinezen-draft-name {
  font-family: var(--f-sans);
  font-size: 0.95rem;
  color: var(--c-text);
}

/* What a row you fill in says about its score, under the name: a check and
   "Saved" once the server has it (rendered by the server, so every refresh
   says it again), "Not saved" while the field holds a number it does not
   have yet (draft_score_controller.js marks the field data-unsaved). The
   line is as tall when empty, so a score coming in moves nothing. */
.chinezen-draft-status {
  min-height: 1.35em;
  font-family: var(--f-mono);
  font-size: 0.72rem;
  line-height: 1.35;
  letter-spacing: 0.04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chinezen-status-saved { color: var(--c-success); }
.chinezen-status-unsaved { display: none; color: var(--c-accent); }
.chinezen-draft-row:has([data-unsaved]) .chinezen-status-saved { display: none; }
.chinezen-draft-row:has([data-unsaved]) .chinezen-status-unsaved { display: inline; }
.chinezen-draft-form .chinezen-score-input[data-unsaved] {
  border-style: dashed;
  border-color: var(--c-accent);
}
/* While its save is on its way Save says so in a word of its own, longer
   than "Save" ("Saving…") or shorter than "Opslaan" ("Bezig…"). Under the
   word, invisible copies of both, no taller than nothing, make the button
   as wide as the longer of the two in both states, so the field and the ±
   beside it do not shift while you watch. */
.chinezen-save {
  flex-direction: column;
  justify-content: center;
  row-gap: 0;
}
.chinezen-save::before,
.chinezen-save::after {
  height: 0;
  overflow: hidden;
  visibility: hidden;
}
.chinezen-save::before { content: attr(data-label); }
.chinezen-save::after { content: attr(data-turbo-submits-with); }
/* Save is quiet until there is something to save: then it is the button
   that stands out. */
.chinezen-draft-row:has([data-unsaved]) .chinezen-save {
  background: var(--c-accent);
  color: var(--c-on-accent);
  border-color: var(--c-accent);
}
/* While its save is on its way Turbo switches Save off, and it says
   "Saving…". At the fade every switched-off button gets, that word read at
   under 3:1, too faint at arm's length: Save keeps its full strength
   instead and changes its look, to the dashed accent edge of a number not
   yet saved. After the rule above, and as specific, so it wins over it. */
.chinezen-draft-row .chinezen-save[disabled] {
  opacity: 1;
  background: transparent;
  color: var(--c-accent);
  border-color: var(--c-accent);
  border-style: dashed;
}
/* Right after your own save, the row says so once more, calmly. */
@keyframes chinezen-just-saved {
  from {
    background-color: var(--c-success-wash);
    border-color: var(--c-success-line);
  }
}
.chinezen-draft-row.is-just-saved { animation: chinezen-just-saved 1.8s ease-out; }
/* A number the board refused, in the row it was typed in. */
.chinezen-draft-refusal {
  flex: 0 0 100%;
  margin: 0;
  font-family: var(--f-mono);
  font-size: 0.72rem;
  line-height: 1.45;
  color: var(--c-danger);
}
.chinezen-draft-refusal:empty { display: none; }
.chinezen-sitout-toggle { white-space: nowrap; }
/* Sat-out player's cell in the score table: a faint dot, not a 0, so it
   reads as "didn't play" rather than "scored zero". */
.chinezen-table td.sat-out-cell { color: var(--c-text-faint); }
.chinezen-draft-form {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
}
/* A text field (it asks for the number pad: games/show), so the field rule
   for type=text outranks a bare class: the figures are set here. */
.chinezen-draft-form .chinezen-score-input {
  width: 90px;
  /* As tall as the Save button beside it, not as a full form field. */
  padding: 0.45rem 0.6rem;
  text-align: right;
  font-family: var(--f-mono);
}
/* The ± in front of the field: the sign the phone's number pad lacks. */
.chinezen-draft-form .chinezen-sign {
  padding-inline: 0.75rem;
  font-family: var(--f-mono);
  font-size: 1rem;
  letter-spacing: 0;
  line-height: 1;
}
/* Without JavaScript it would do nothing. */
@media (scripting: none) {
  .chinezen-draft-form .chinezen-sign { display: none; }
}
.chinezen-draft-saved {
  min-width: 90px;
  text-align: right;
  font-family: var(--f-mono);
}

/* Standings panel — final settlement */
.chinezen-stand { padding-bottom: 4rem; }
.chinezen-stand .ornament { margin-bottom: 1.5rem; }
.chinezen-stand-table {
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  border-collapse: collapse;
  border: 1px solid var(--c-border);
  background: var(--c-bg-2);
  border-radius: var(--r-md);
}
.chinezen-stand-table th,
.chinezen-stand-table td {
  padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--c-border);
  text-align: left;
}
.chinezen-stand-table th {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--c-text-dim);
  font-weight: 500;
  background: var(--c-bg-3);
}
.chinezen-stand-table .num { text-align: right; }
/* An amount stays one piece: it broke after the minus ("-" | "€11.00") and,
   in Dutch, after the euro sign ("€" | "12,50"). The player column wraps
   instead, as billing's amounts do (.billing-nowrap). */
.chinezen-stand-table td.num { white-space: nowrap; }
.chinezen-stand-table .neg { color: var(--c-danger); }
.chinezen-stand-table .pos { color: var(--c-success); }
.chinezen-stand-table .chinezen-stand-totals td {
  background: var(--c-bg-3);
  border-top: 1px solid var(--c-border-strong);
  font-family: var(--f-mono);
}
.chinezen-stand p { margin-top: 1rem; text-align: center; }

/* Player picker on the New-game and preset forms: a seat's number and the
   member in it, members of the group only. A saved table's row of somebody
   who can no longer sit at it (a guest, or a member who left) says so under
   its picker. */
.player-slots {
  display: grid;
  gap: 0.6rem;
  margin-top: 0.6rem;
}
.player-slot {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.3rem 0.6rem;
  align-items: center;
}
.player-slot-num {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  color: var(--c-accent);
  width: 2rem;
}
.player-slot-gone { grid-column: 2; }
/* Who a saved table names but the new game does not seat, and why. */
.player-slots-left-out {
  margin-top: 0.8rem;
  padding: 0.6rem 0.9rem;
  border-left: 2px solid var(--c-accent-line);
}
.player-slots-left-out p { margin: 0; }
.player-slots-left-out p + p { margin-top: 0.35rem; }
.player-slots-invite { margin-top: 0.8rem; }

/* New game while no table is free (GameStart): the game being played and
   who can end it, the forgotten one a start ends first, the party night.
   The left-out note's look, a block per game. */
.new-game-tables {
  margin: 1.2rem 0 1.6rem;
  padding: 0.6rem 0.9rem;
  border-left: 2px solid var(--c-accent-line);
}
.new-game-tables p { margin: 0; }
.new-game-tables > :not(:first-child) { margin-top: 0.9rem; }
.new-game-table p + p { margin-top: 0.35rem; }
/* Start's label grows with the offer ("End it and start yours", "Beëindig
   het en start je spel"): wider than a 320px phone's card on one line, so
   it wraps, inside the card, rather than running out of it. */
.btn.new-game-start { max-width: 100%; white-space: normal; }

/* Settle-up payments list (after game ends) */
.chinezen-settle { padding-bottom: 4rem; }
.chinezen-settle .ornament { margin-bottom: 1.5rem; }
.chinezen-settlement-list {
  list-style: none;
  padding: 0;
  margin: 0 auto;
  max-width: 540px;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-bg-2);
}
.chinezen-settlement-list li {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto;
  gap: 1rem;
  align-items: center;
  padding: 0.75rem 1.2rem;
  border-bottom: 1px solid var(--c-border);
}
.chinezen-settlement-list li:last-child { border-bottom: 0; }
.settle-from {
  text-align: right;
  color: var(--c-danger);
  font-family: var(--f-serif);
  font-size: 1.05rem;
}
.settle-to {
  color: var(--c-success);
  font-family: var(--f-serif);
  font-size: 1.05rem;
}
.settle-arrow {
  color: var(--c-text-faint);
  font-size: 1rem;
}
.settle-amt {
  color: var(--c-accent);
  font-weight: 500;
  text-align: right;
  font-size: 0.95rem;
  min-width: 5rem;
}
.chinezen-settle p { text-align: center; margin-top: 1rem; }
@media (max-width: 500px) {
  .chinezen-settlement-list li {
    grid-template-columns: 1fr auto 1fr;
    grid-template-rows: auto auto;
    gap: 0.3rem 0.6rem;
  }
  .settle-amt { grid-column: 1 / -1; text-align: center; }
}

/* Chinezen dealer rotation: column + per-row dealer cell highlight. The
   face is the cells' alone: on the bare class it outranked
   `.chinezen-table th`, and the Dealer heading stood out from the other
   headings as if it were data. */
.chinezen-table td.dealer-col {
  text-align: left;
  font-family: var(--f-sans);
  font-size: 0.85rem;
  color: var(--c-text);
  white-space: nowrap;
}
.chinezen-table thead .dealer-col,
.chinezen-table thead th.dealer-col {
  text-align: left;
}
.chinezen-table td.is-dealer {
  position: relative;
  background: var(--c-accent-wash);
}
.chinezen-table td.is-dealer::before {
  content: "";
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 2px;
  background: var(--c-accent);
}
.chinezen-dealer-tag {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-accent);
  margin-left: 0.6rem;
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-sm);
  vertical-align: middle;
}
.chinezen-dealer-tag.is-flush { margin-left: 0; }

/* Chinezen voided round row: struck through, in the faintest text. The
   dealer's cell keeps the misdeal tag and the Restore button at full
   strength.
   Dimmed, never faded: opacity blends text into the surface under it, so
   the faintest token at 45% fell to 2:1 and the misdeal tag, at 85%, under
   4.5:1 in both palettes. The dimmed states (a voided round, a player away
   or sitting out) use the palette's own dim and faint text instead, which
   keep AA; test/stylesheets/palettes_test.rb holds opacity to decoration. */
.chinezen-table tr.voided-row td {
  color: var(--c-text-faint);
  text-decoration: line-through;
  font-style: italic;
}
.chinezen-table tr.voided-row td.dealer-col {
  color: var(--c-text-dim);
  text-decoration: none;
  font-style: normal;
}
.voided-tag {
  font-family: var(--f-mono);
  font-size: 0.65rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--c-danger);
  margin-left: 0.5rem;
  padding: 0.1rem 0.4rem;
  border: 1px solid var(--c-danger-line);
  border-radius: var(--r-sm);
  vertical-align: middle;
}
.voided-toggle {
  margin-left: 0.5rem;
  vertical-align: middle;
  padding: 0.25rem 0.55rem !important;
  font-size: 0.62rem !important;
  letter-spacing: 0.14em !important;
}

/* Chinezen settle-up — payment row with EPC-QR or mailto fallback */
.chinezen-settlement-list li {
  display: block;                /* we now host an inner grid + collapsible QR */
  padding: 0;
}
.settle-summary {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto;
  grid-template-rows: auto auto;
  gap: 0.5rem 1rem;
  align-items: center;
  padding: 0.75rem 1.2rem;
}
.settle-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
}
.settle-pay {
  display: inline-block;
}
.settle-pay > summary {
  cursor: pointer;
  list-style: none;
  display: inline-block;
}
.settle-pay > summary::-webkit-details-marker { display: none; }
.settle-pay[open] > summary { background: var(--c-accent-wash); }
.settle-qr {
  margin-top: 0.8rem;
  padding: 1rem;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-bg-3);
  text-align: center;
  width: 100%;
}
/* Phone-to-phone scanning sweet spot is ~220-260px. Above that, the
   scanning camera has to back away so far it can't focus on the
   modules and Bunq/ING/ABN refuse the read. width:100% + max-width
   keeps the QR scannable on any viewport. White in both palettes, like
   the 2FA code (.otp-qr), edged so the tile shows on paper too. The image
   carries its own quiet zone of four modules (EpcQr.svg), so it needs no
   padding. */
.settle-qr svg {
  background: #fff;
  border: 1px solid var(--c-border);
  padding: 0;
  border-radius: var(--r-sm);
  display: inline-block;
  width: 100%;
  max-width: 240px;
  height: auto;
}
.settle-qr p { margin: 0.7rem 0 0; }
@media (max-width: 600px) {
  .settle-summary {
    grid-template-columns: 1fr auto 1fr;
  }
  .settle-summary .settle-amt { grid-column: 1 / -1; text-align: center; }
}

/* Chinezen — preset chips on the New-game form */
.preset-chips-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin: 1rem 0 0.5rem;
}
.preset-chip { padding: 0.4rem 0.9rem !important; font-size: 0.7rem !important; }
.preset-chip.active {
  border-color: var(--c-accent) !important;
  background: var(--c-accent-tint) !important;
  color: var(--c-accent-2) !important;
}
.preset-manage {
  margin-left: auto;
  border: none !important;
  font-size: 0.72rem !important;
}

/* Chinezen — cumulative trend chart */
.chinezen-trend { padding-bottom: 2.5rem; }
.chinezen-trend .ornament { margin-bottom: 1.5rem; }
.chinezen-chart {
  border: 1px solid var(--c-border);
  background: var(--c-bg-2);
  border-radius: var(--r-md);
  padding: 1rem 1.2rem 1.2rem;
  max-width: 800px;
  margin: 0 auto;
}
.chinezen-chart-svg {
  display: block;
  width: 100%;
  height: auto;
}
/* ChinezenChartHelper draws the chart twice: wide, and compact with bigger
   type for a phone, where the wide one shrank its labels to 4px. One shows. */
.chinezen-chart-svg-compact { display: none; }
.chinezen-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  justify-content: center;
  margin-top: 0.6rem;
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--c-text-dim);
}
.chinezen-chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.chinezen-chart-legend-dot {
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
}
.center-text { text-align: center; }
.mt-1 { margin-top: 1rem; }

/* Chinezen lifetime-stats cards + player-name link in score table */
.chinezen-stats-grid .card { padding: 1.5rem; }
.chinezen-stat-num {
  font-family: var(--f-serif);
  font-size: 2.4rem;
  margin: 0.4rem 0;
  line-height: 1;
  /* Lining figures, for landing.css's reason: Cormorant's old-style ones
     read "100%" as "Ioo%". */
  font-variant-numeric: lining-nums;
}
.chinezen-stat-num.neg { color: var(--c-danger); }
.chinezen-stat-num.pos { color: var(--c-success); }
.chinezen-player-link {
  color: inherit;
  border: none;
  border-bottom: 1px dotted var(--c-border-strong);
}
.chinezen-player-link:hover { color: var(--c-accent); border-color: var(--c-accent); }

.btn[disabled],
input[type="submit"][disabled] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}

/* ──────────────────────────────────────────────────────────────────────
   Chinezen scoreboard SVG chart (ChinezenChartHelper), in the palette.
   The grid, the zero line and the axis labels carry classes for this. The
   players' lines, end dots and legend swatches are painted with
   presentation attributes from CHINEZEN_LINE_COLORS, which any stylesheet
   rule outranks, so they are repainted here by seating order, in the
   helper's own cycle of six: one polyline and one end dot per player, in
   order, and a legend that lists the players in that same order. The dark
   series are the helper's colours, so its attributes stay right wherever
   this does not reach.
   ────────────────────────────────────────────────────────────────────── */
.chinezen-chart-svg .chz-grid     { stroke: var(--c-border); }
.chinezen-chart-svg .chz-baseline { stroke: var(--c-border-strong); }
.chinezen-chart-svg .chz-axis     { fill: var(--c-text-faint); }

.chinezen-chart-svg polyline:nth-of-type(6n+1) { stroke: var(--c-series-1); }
.chinezen-chart-svg polyline:nth-of-type(6n+2) { stroke: var(--c-series-2); }
.chinezen-chart-svg polyline:nth-of-type(6n+3) { stroke: var(--c-series-3); }
.chinezen-chart-svg polyline:nth-of-type(6n+4) { stroke: var(--c-series-4); }
.chinezen-chart-svg polyline:nth-of-type(6n+5) { stroke: var(--c-series-5); }
.chinezen-chart-svg polyline:nth-of-type(6n+6) { stroke: var(--c-series-6); }
.chinezen-chart-svg circle:nth-of-type(6n+1),
.chinezen-chart-legend-item:nth-child(6n+1) circle { fill: var(--c-series-1); }
.chinezen-chart-svg circle:nth-of-type(6n+2),
.chinezen-chart-legend-item:nth-child(6n+2) circle { fill: var(--c-series-2); }
.chinezen-chart-svg circle:nth-of-type(6n+3),
.chinezen-chart-legend-item:nth-child(6n+3) circle { fill: var(--c-series-3); }
.chinezen-chart-svg circle:nth-of-type(6n+4),
.chinezen-chart-legend-item:nth-child(6n+4) circle { fill: var(--c-series-4); }
.chinezen-chart-svg circle:nth-of-type(6n+5),
.chinezen-chart-legend-item:nth-child(6n+5) circle { fill: var(--c-series-5); }
.chinezen-chart-svg circle:nth-of-type(6n+6),
.chinezen-chart-legend-item:nth-child(6n+6) circle { fill: var(--c-series-6); }
/* Lines as thick on the screen as the drawing says, however far the
   drawing is scaled down to fit its box. */
.chinezen-chart-svg polyline,
.chinezen-chart-svg line { vector-effect: non-scaling-stroke; }

/* Active-player count badge on the draft panel — prominent so it's clear
   at a glance whether the hand has a dealable number of players (2–4). */
.chinezen-active-count {
  font-family: var(--f-mono);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  margin: 0.4rem 0 0.2rem;
  padding: 0.45rem 0.8rem;
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  display: inline-block;
}
.chinezen-active-num { font-weight: 600; color: var(--c-text); }
.chinezen-active-ok {
  border-color: var(--c-success-line);
  background: var(--c-success-wash);
}
.chinezen-active-too-few,
.chinezen-active-too-many {
  border-color: var(--c-danger-line);
  background: var(--c-danger-wash);
}
.chinezen-active-warn { color: var(--c-danger); }

/* Add-player disclosure under the round panel. */
.chinezen-add-player { margin-top: 1.5rem; }
.chinezen-add-player > details > summary {
  cursor: pointer;
  list-style: none;
  display: inline-block;
}
.chinezen-add-player > details > summary::-webkit-details-marker { display: none; }
.chinezen-add-player-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem 1rem;
  align-items: flex-end;
  margin: 1rem 0 0.6rem;
}
.chinezen-add-player-form .field { margin: 0; }

/* Table roster — who's present vs away during a long session. */
.chinezen-roster {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  margin: 1.5rem 0 0.5rem;
  padding: 1rem 1.2rem;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-bg-2);
}
.chinezen-roster-group { flex: 1 1 240px; }
.chinezen-roster-group h4 {
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  margin: 0 0 0.6rem;
}
.chinezen-roster-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.chinezen-roster-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
}
.chinezen-roster-name { font-family: var(--f-sans); color: var(--c-text); }
.chinezen-roster-away .chinezen-roster-name { color: var(--c-text-dim); }
.chinezen-roster-away p { margin: 0.6rem 0 0; }

/* Away players in the score table + stand: dimmed and tagged, so the
   people currently at the table stand out. History stays visible, in the
   faintest text rather than faded (see "Dimmed, never faded" above): an
   away player's totals lose their red and green, not their sign. */
.chinezen-table th.is-away-col,
.chinezen-stand-table tr.is-away-row td { color: var(--c-text-faint); }
.chinezen-away-tag {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0.02rem 0.35rem;
  font-family: var(--f-mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-text-dim);
  border: 1px solid var(--c-border-strong);
  border-radius: 999px;
  vertical-align: middle;
}

/* ── Chinezen: the game meta line ─────────────────────────────────────────
 *
 * One line on a phone: the stake and the state stay, the start time and the
 * host drop out below 700px. Full line everywhere there is room for it.
 */
.chinezen-meta { margin-top: 0.25rem; }
/* Who ended the game from New game, after its quiet hours (the forgotten-
   game button): somebody away from its table may have, so a sentence of its
   own, on every screen. For its host, why it cannot be reopened yet, in
   place of the Reopen button. And until when a game takes new rounds, or
   why a paused one takes none (GamesHelper#play_line). */
.chinezen-ended-idle,
.chinezen-reopen-blocked,
.chinezen-play-line { margin: 0.4rem 0 0; max-width: 60ch; color: var(--c-text-dim); }

/* ── Chinezen: mobile tab bar ──────────────────────────────────────────────
 *
 * The scoreboard is five stacked sections, which on a phone means scrolling
 * past the board to reach the round you are trying to score. Below 700px the
 * sections become tabs; above it, nothing here applies and the page keeps its
 * full-height layout.
 *
 * Visibility is CSS's job, not the controller's. The controller only marks
 * which panel is active and sets data-tabs-ready once it has, so a browser
 * without JavaScript never hides anything.
 */
.chinezen-tabs { display: none; }

@media (max-width: 700px) {
  .chinezen-tabs {
    display: block;
    position: sticky;
    top: 0;
    z-index: 20;
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
    /* Opaque: rows scroll underneath this. */
    background: var(--c-bg);
    border-bottom: 1px solid var(--c-border);
  }
  /* Four tabs at most: Round and Table while the game runs, Settle up once
     it has ended. The host's four English labels are the widest set, and
     at this padding and tracking they fit the 272px a 320px screen leaves
     (test/stylesheets/scoreboard_tabs_fit_test.rb). Narrower than that the
     bar scrolls rather than wrapping into a second row that pushes the
     board further down. */
  .chinezen-tabs-inner {
    display: flex;
    gap: 0.3rem;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .chinezen-tabs-inner::-webkit-scrollbar { display: none; }

  .chinezen-tab {
    flex: 1 0 auto;
    min-height: 2.75rem;              /* comfortable thumb target */
    padding: 0.55rem 0.5rem;
    background: transparent;
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    color: var(--c-text-dim);
    font-family: var(--f-mono);
    font-size: 0.72rem;
    letter-spacing: 0.06em;
    white-space: nowrap;
    text-transform: uppercase;
    cursor: pointer;
  }
  .chinezen-tab[aria-selected="true"] {
    color: var(--c-accent-2);
    border-color: var(--c-accent);
    background: var(--c-accent-tint);
  }
  .chinezen-tab:focus-visible {
    outline: 2px solid var(--c-accent);
    outline-offset: 2px;
  }

  [data-tabs-ready] .chinezen-panel:not(.is-active) { display: none; }

  /* A panel now owns the screen, so the board can use the height it was
     capped out of when five sections shared one scroll. */
  .chinezen-panel.is-active .chinezen-table-wrap { max-height: 65vh; }

  /* The round panel is the one you hold in your hand all evening, so it gets
     the tightest treatment on the page. Each player was 131px tall — name on
     one line, the score field on a second, the sit-out button on a third —
     which put five players at 678px on a 664px screen. One line each instead:
     name truncates, the number field is only as wide as the numbers it holds,
     and the two controls shrink to compact chips. The ± in front of the
     field took 35px from the names, a host's at 320px down to 20px of
     "Sanne de Vries": the panel, the row and the chips give it back. */
  .chinezen-draft-rows { gap: 0.35rem; }
  .chinezen-draft-row {
    gap: 0.35rem;
    padding: 0.3rem 0.35rem;
  }
  .chinezen-draft-name {
    font-size: 0.88rem;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* The status line fits beside the controls under the name: the row stays
     as tall as its buttons. */
  .chinezen-draft-status { font-size: 0.62rem; }
  .chinezen-draft-refusal { font-size: 0.66rem; }
  .chinezen-draft-form,
  .chinezen-draft-saved { width: auto; }
  .chinezen-draft-form { gap: 0.25rem; }
  .chinezen-draft-form .chinezen-score-input {
    width: 3.25rem;
    padding: 0.35rem 0.4rem;
    text-align: right;
  }
  .chinezen-draft-saved { min-width: 2.25rem; text-align: right; }
  /* Compact, but not smaller than a thumb. The controls came out 23px tall
     on the first pass — comfortably mis-tappable at a card table. A 2.4rem
     floor puts them near the 44px guidance while the row still lands around
     48px, against the 131px it started at. */
  .chinezen-draft-form .btn,
  .chinezen-sitout-toggle {
    min-height: 2.4rem;
    padding: 0.3rem 0.45rem !important;
    font-size: 0.62rem !important;
    letter-spacing: 0.04em !important;
  }
  /* The ± is a sign, not a word: as large as the figures beside it. */
  .chinezen-draft-form .chinezen-sign {
    padding-inline: 0.4rem !important;
    font-size: 1rem !important;
    letter-spacing: 0 !important;
  }
  .chinezen-draft-form .chinezen-score-input { min-height: 2.4rem; }

  /* Half the panel was still frame rather than content: 275px of heading,
     help text and buttons around 272px of actual player rows. The static
     explainer above the rows goes — which row is yours is already said by the
     gold border on it, and the sentence never changes. The line *below* the
     rows stays: "row sum must be 0" is why Submit is disabled, and that is
     live information you need every round. */
  .chinezen-draft > p.muted:first-of-type { display: none; }
  .chinezen-draft h3 { margin-bottom: 0.5rem; }
  .chinezen-draft-rows { margin-bottom: 0.6rem; }
  /* The host's buttons wrap, under the rows, where another row moves
     nothing the host is reading. They used to make one row that scrolled
     sideways with its scrollbar hidden, and Cancel round, which needs 546px
     of row in English and 662px in Dutch, sat off every phone's screen
     behind a swipe nobody would guess was there. A little less air in them
     keeps English at 360px to two rows. Each button stays whole. */
  .chinezen-add-round .form-actions {
    margin-top: 0.6rem;
    gap: 0.4rem;
    flex-wrap: wrap;
  }
  .chinezen-add-round .form-actions .btn { padding-inline: 0.8rem; letter-spacing: 0.1em; }
  .chinezen-add-round .form-actions > * { flex: 0 0 auto; }

  /* The round heading wraps its tags onto a second line. As inline text a
     tag broke inside its border ("dealer · Sanne de" | "Vries", two
     open-ended half boxes); in a wrapping flex row each tag moves down
     whole, spaced by the gap. Phone only: a flex row drops the space
     between "Round" and "#4", which the gap stands in for here. */
  .chinezen-add-round h3 {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    font-size: 1rem;
    gap: 0.35rem;
  }
  .chinezen-add-round h3 > .chinezen-draft-tag,
  .chinezen-add-round h3 > .chinezen-dealer-tag { margin-left: 0; }
  .chinezen-draft .chinezen-draft-tag, .chinezen-dealer-tag {
    font-size: 0.58rem;
    padding: 0.12rem 0.4rem;
  }
  .chinezen-add-round { padding: 0.9rem 0.5rem; }
  .chinezen-add-round > p.mono { font-size: 0.68rem; line-height: 1.45; }

  /* The score table is about players and numbers; # / Swap / Dealer were
     taking well over half a 390px screen and pushing the columns that matter
     off the right edge. Swap goes (the swap for the open round is stated in
     the Round panel, where it is actually acted on), the cells tighten, and
     the per-row misdeal toggle shrinks to a compact control. */
  .chinezen-table th.ruilen-col,
  .chinezen-table td.ruilen-col { display: none; }
  .chinezen-table th,
  .chinezen-table td { padding: 0.5rem 0.55rem; }
  .chinezen-table .voided-toggle {
    display: block;
    margin-top: 0.2rem;
    padding: 0.15rem 0.4rem !important;
    font-size: 0.6rem !important;
    letter-spacing: 0.08em !important;
  }
  .chinezen-table .dealer-col { max-width: 8.5rem; }

  /* Keep the round number in view while scrolling sideways through players —
     without it the columns lose their anchor on a narrow screen. */
  .chinezen-table th.num-col,
  .chinezen-table td.num-col {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--c-bg-3);
  }
  .chinezen-table thead th.num-col { z-index: 4; }

  /* The management buttons tighten to share a row: the audio toggle already
     carries a glyph, so on a phone it becomes icon-only, and the rest take
     less air (light or dark has moved to the header's switch since). The
     words are hidden visually rather than removed: they stay the button's
     accessible name, which an icon-only control would otherwise lose.
     English fits one row from about 345px. The Dutch labels need 378px, so
     on most phones "Alle spellen" takes a second row: the row used to scroll
     sideways with its scrollbar hidden, which cut it off at the screen's
     edge, behind a swipe nobody would guess was there. */
  .chinezen-actions {
    padding-bottom: 0.25rem;
    gap: 0.35rem;
  }
  .chinezen-actions .btn {
    padding: 0.5rem 0.6rem !important;
    font-size: 0.62rem !important;
    letter-spacing: 0.08em !important;
  }
  .chinezen-actions .btn-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .chinezen-actions .btn-icon { font-size: 0.95rem; line-height: 1; }
  .chinezen-actions > * { flex: 0 0 auto; }
  /* The "Watching only" note takes a row of its own, above the buttons. */
  .chinezen-actions > .util-label { flex: 1 1 100%; }

  /* The header keeps only the brand, the light/dark toggle and the account
     buttons on this page: see "Header on a phone". The menu is dead weight
     while scoring a game, "Signed in as <you>" tells you what you know (the
     account page is one tap away), and EN | NL waits in the footer. */

  /* Before this, the header block alone ate 340px of a 664px screen and the
     first row of content started below the fold — measured, not guessed. The
     title, the meta line and the management buttons are all reference
     material; the tab bar and the panel under it are what you came for. */
  .chinezen-header { padding-top: 1rem; padding-bottom: 0.5rem; }
  .chinezen-title-row { gap: 0.75rem; }
  .chinezen-title-row h1 { font-size: 1.75rem; margin-bottom: 0.3rem; }
  .chinezen-header .hero-kicker { margin-bottom: 0.35rem; }
  .chinezen-header .mono.small,
  .chinezen-meta { font-size: 0.72rem; line-height: 1.5; }

  .chinezen-meta-detail { display: none; }

  .chinezen-title-row h1 { font-size: 1.5rem; }
  .chinezen-header .hero-kicker { font-size: 0.62rem; margin-bottom: 0.25rem; }

  /* Edge to edge on a phone, on purpose: the round panel's one-line rows
     give each name what the score field and the two buttons leave, and
     .container's 48px would cut every name short at 360px, sooner in Dutch,
     whose buttons are wider. */
  .chinezen-board { padding-top: 0.75rem; padding-inline: 0; }
  .chinezen-trend, .chinezen-stand, .chinezen-settle { padding-bottom: 2rem; }

  /* The amounts no longer wrap (above), so the cells give up some air to
     keep the table inside a 320px screen in Dutch ("-€ 11,00"). */
  .chinezen-stand-table th,
  .chinezen-stand-table td { padding-inline: 0.6rem; }

  /* The compact drawing, at a size its type was drawn for. */
  .chinezen-chart-svg-wide { display: none; }
  .chinezen-chart-svg-compact {
    display: block;
    max-width: 26rem;
    margin: 0 auto;
  }

  /* A section whose panels are all hidden still contributed its own padding —
     60px of nothing above the Standings and Settle up tabs. :has() lets the
     section collapse when it holds no active panel; where it is unsupported
     the page simply keeps the old spacing. */
  [data-tabs-ready] .chinezen-board:not(:has(.chinezen-panel.is-active)) {
    padding-top: 0;
    padding-bottom: 0;
  }
  .chinezen-panel .ornament { margin-top: 1.25rem; }
}

/* ── Chinezen: the board's messages ───────────────────────────────────────
 *
 * "Round 5 is open", "Kees sits out this round", a refusal: over the foot of
 * the screen, in a box that is always on the page (games/show), not in a
 * line above the board that pushed every row down when it came and pulled
 * them back up when the next refresh took it away. Neither the box nor a
 * message takes taps: a tap on a message reaches the board under it, as
 * under an Android toast. A message that went at a tap held back the tap
 * meant for Close round under it, so the host had to tap twice.
 * board_messages_controller.js takes it away after a few seconds.
 */
.board-messages {
  position: fixed;
  z-index: 30;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 0 1rem calc(1rem + env(safe-area-inset-bottom));
  pointer-events: none;
}
.board-message {
  width: 100%;
  max-width: 30rem;
  margin: 0;
  padding: 0.8rem 1.1rem;
  border: 1px solid var(--c-success-line);
  border-radius: var(--r-md);
  background: var(--c-bg-2);
  color: var(--c-success);
  box-shadow: var(--shadow-soft);
  font-family: var(--f-mono);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  animation: board-message-in 180ms ease-out;
}
.board-message.is-alert {
  border-color: var(--c-danger-line);
  color: var(--c-danger);
}
@keyframes board-message-in {
  from { transform: translateY(0.75rem); }
}
/* Without JavaScript no timer takes a message away, and every action loads
   a page of its own, so nothing moves in place: the message sits in the
   page above the board, where the layout's flash goes, and covers nothing. */
@media (scripting: none) {
  .board-messages {
    position: static;
    padding-top: 1.5rem;
  }
}

/* Read out, not shown: the row says it already (a saved score). */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* iOS Safari zooms the page in at two quick taps on anything that does not
   rule it out, and at the table people tapped Save twice when nothing
   seemed to happen. Manipulation keeps panning and pinch zoom (the viewport
   allows both) and drops the double-tap zoom: on the scoreboard's page, and
   on its controls and inner scrollers themselves, for browsers that look no
   further up than the nearest scroller. */
body.chinezen-focus,
.chinezen-focus :is(a, button, input, select, summary, textarea, .chinezen-table-wrap, .chinezen-tabs-inner) {
  touch-action: manipulation;
}

/* The message's slide goes under Reduce Motion. The saved row's light stays:
   it moves nothing, only its colours change, and without it a save looked
   the same as every save before it. */
@media (prefers-reduced-motion: reduce) {
  .chinezen-tab { transition: none; }
  .board-message { animation: none; }
}
