/* ═══════════════════════════════════════════════════════════════
   MAFIA — leather, brass and lamplight.
   Built to the reference Sasha chose: seat cards with portraits on the
   left, transcript centre, role card top right, action bar below.
   The round table was tried and rejected — it read stiff, and a list
   of faces is what a player actually scans.
   Warm sepia ground, NOT cold slate. That is the whole difference.
   ═══════════════════════════════════════════════════════════════ */

:root{
  /* ⚠️ ONE GAP BETWEEN THE BAR AND THE PAGE, and it is a token because it
     drifted four ways the moment the shared bar arrived. Measured, live, at
     1280px: clubs 44px, contact 44px, pricing 28px, legal 0px, guide 0px — so
     a reader moving Clubs → Terms → Guide watched the page jump under a bar
     that never moves. A document that starts flush against the navigation
     reads as a rendering fault, and reported as exactly that.
     ⛔ EVERY DOCUMENT WRAPPER USES THIS, never its own number. browser-test
     measures the gap on every public page and fails on any difference. */
  --doc-top:44px;
  /* ⚠️ BR round 8 — CHECK and Report a problem sat ≈8px apart (measured, real
     lesson, three viewports) because every element between them — markNote,
     an optional gate/due note, #verdict — collapses to zero height once it is
     empty, and CSS margins collapse straight through an empty box. `.act-bar`
     already carries margin-bottom:0, so the entire visible gap came from
     `.sub-hint`'s own 7px margin-top, wherever it landed.
     ⛔ NOT ANOTHER MARGIN. A margin here would collapse exactly the same way
     and reproduce the bug in a new number. `--gap-actions` is applied as
     PADDING on the row that follows the action bar (school.css, `.report-row`)
     — padding never collapses, so the floor holds no matter what is empty
     above it. One token, so the next screen that stacks an action and a
     secondary button underneath it reaches for this instead of a new guess. */
  --gap-actions:16px;
  /* leather and lamplight */
  --wood:#1A1310;            /* the room */
  --wood-2:#221913;          /* panels */
  --wood-3:#2C211A;          /* raised cards */
  --wood-4:#3A2C22;          /* hover */
  --felt:#17211C;            /* the desk surface */
  --edge:#3E2F24;            /* card edges */
  --edge-2:#2A1F18;
  /* ⚠️ круг 8, theme-consistency pass (2026-09-14, Женя's review — "разный
     цвет фона и обводки на всех страницах"). `linear-gradient(180deg,
     #241A14,#1C150F)` was the SAME literal, copied by hand into eight
     separate files (clubs.html, legal.html, contact.html, join.html,
     status.html, club.html, clubs-table.html, admin.html — three of them
     three more times over for uk/ru/de/fr) under eight different class
     names (.clubs-card, .legal, .contact-card, .join-card, .status-card,
     .club-card, .clubs-table-card, .card). Nothing there was actually
     wrong — all eight rendered identically — but a value with no name is a
     value nobody can tell is shared, and pricing.html's `.plan` drifted
     exactly that way: it wanted "the same card" and reached for
     `rgba(255,255,255,.02)` instead, which is transparent, not brown, and
     reads as a different colour under `main` because it lets the page's
     own gradient behind it show through. One token, so the next card reads
     the source of truth instead of retyping the numbers. The dark end is
     `var(--wood)` (the room itself, not the old page's own `#1C150F`,
     4 of 255 off it) so a full-width panel and the room it sits in now
     share a literal edge instead of two numbers close enough to pass. */
  --panel:linear-gradient(180deg,#241A14,var(--wood));

  --bone:#EFE7D8;            /* everything you read */
  --bone-2:#B3A692;
  /* ⚠️ #7C6E5E measured 3.49:1 against the room and 4.25:1 against the panel —
     both below WCAG AA's 4.5:1 for body text, and it is the colour used by
     EVERY label, hint, badge and footer link on the page. Thirty-two failures
     came from this one line. #A2957F clears 4.5:1 on both grounds and is still
     unmistakably the quiet tier. Measured by a11y-test.mjs, not eyeballed. */
  --bone-3:#A2957F;
  --bone-4:#7C6E5E;          /* the old value, for things that are not text */

  --brass:#C9A227;           /* frames, the current turn */
  /* ⚠️ RAISED FROM #8A6F1B, WHICH WAS 3.6–4.4:1 AND FAILED AA. It carries the
     speaker's name down the whole transcript — the most-read text in the game —
     and the audit never convicted it before because it never got far enough
     into a game to see one. A check covering more found something real rather
     than something new. #A08320 measures 4.7:1 on the transcript and 5.0:1 on
     the page ground. */
  --brass-2:#A08320;
  --brass-dim:#332912;

  --blood:#B4262C;           /* accusation, death, mafia — DOTS, BORDERS, GLOWS */
  /* ⚠️ AND A SECOND ONE FOR WORDS, because the note above is a rule and it was
     broken. `--blood` at 2.7:1 is fine behind a dot and illegible as TEXT — it
     was carrying "→ Ernest" beside every accusation in the transcript, which is
     the mark a player reads to follow who is naming whom. The audit had never
     seen it: it needs an accusation on screen, and it never got that far into a
     game until the suite stopped waiting for the voices. 5.2:1. */
  --blood-text:#D66E72;
  /* ⚠️ #B4262C as TEXT measures 2.43–2.84:1 on the four grounds it is used on —
     WCAG AA wants 4.5:1 — and it is what "MAFIA" and "NAMED" were written in,
     on the board, for the whole game. Found only when the game board itself was
     added to a11y-test.mjs; every earlier pass audited the start screen and the
     legal page. #E0787C clears 4.5:1 on all four (worst 5.34:1) and still reads
     as the same red. Use --blood for shapes, --blood-lit for anything read. */
  --blood-lit:#E0787C;

  /* ⚠️ PRIVATE KNOWLEDGE — your partner, a seat you checked. It was a violet
     for one afternoon and violet was wrong: the only cool colour in a warm
     room, and it read as borrowed from another game. Identity does not need a
     colour of its own — it needs a channel STATE does not use. So it is the
     reading colour on a brass hairline, as an outlined chip rather than a
     filled pill, and it never competes with the red that means danger. */
  --known:#EFE7D8;              /* the reading colour — 13:1 on every seat ground */
  --known-2:#8A6F1B;            /* the hairline, same brass as the rest of the room */
  --blood-2:#7E1A1F;
  --blood-dim:#2E1213;
  --alive:#5E9A62;           /* the little green dot */

  --card:5px;
  --lift:0 1px 2px rgba(0,0,0,.5), 0 10px 24px -14px rgba(0,0,0,.95);
  --rule:linear-gradient(90deg,transparent,var(--brass-2) 12%,var(--brass) 50%,var(--brass-2) 88%,transparent);
}

*{box-sizing:border-box}
/* ⚠️ `height:100%` IS FOR THE APP, WHICH IS A FULL-HEIGHT INSTRUMENT. A document
   page must grow with its content instead, and this is why: the sticky bar
   sticks inside its containing block, which is the body. With the body pinned
   to the viewport the bar could only stay put for one screenful — measured on
   the character page as 800px of body around 5,187px of content, and reported
   as "the navigation is fixed until the content starts, then it gets covered
   and scrolls away".
   ⛔ TWO PAGES ALREADY PATCHED THIS INLINE, in their own <style> blocks, and the
   three that did not were exactly the three where the bar failed. A fix pasted
   into some of the pages is not a fix; it is a coincidence waiting to be
   noticed. It belongs here, once, keyed on the class the document pages
   already carry. */
/* ⛔ THE ROOT BOX MUST BE AS TALL AS THE DOCUMENT (owner, 16.09: "the background
   breaks off near the bottom of the page"). `body`'s background is painted on
   the canvas, but its POSITIONING AREA is the root element's box — and with
   `html{height:100%}` that box was one window tall, so on any page longer than
   a window the lamp gradient ended there and TILED below it: a visible seam
   above the footer. `min-height` keeps a short page filling the window;
   `height:auto` lets the root grow with the content so one gradient covers
   the whole page. The table's full-height shell sets its own `100dvh` below. */
html{height:auto; min-height:100%}
body{height:auto}
body.doc{ height:auto; min-height:100dvh }

/* ⛔ THREE MEASURES FOR THE WHOLE PRODUCT, AND THERE WERE FOUR ARBITRARY ONES.
   The front door sat at 448px, the account card at 460, the school at 760 and
   the guide at 1120 — and the document pages had NO frame at all, so each one
   was as wide as whatever happened to be inside it. Walking from Lessons to
   Guide to Pricing, the page changed shape three times, which reads as three
   different sites.
   Each of these has a reason, which is why there are three and not one:
     --page  the frame every full page shares. Nothing outside it, ever.
     --read  a column of prose. 760px is about 90 characters at this size;
             wider than that and the eye loses the line it is on.
     --card  a form or a screen of controls. Narrow on purpose: a settings
             panel at reading width is a wall of switches. */
:root{
  /* ⛔ PREFIXED, AND THE PREFIX IS NOT DECORATION. `--card` was already taken —
     it is the corner radius, 5px, used across the whole stylesheet — and
     declaring it again as 460px silently made every rounded corner in the
     product 460 pixels. Caught by this file's own check on the first run, which
     is the entire argument for having one. */
  --w-page: 1120px;
  --w-read: 760px;
  --w-card: 460px;
}

/* ⚠️ THE FRAME IS ON THE PAGE, NOT ON THE CONTENT. Setting it here means a new
   page is centred and bounded before anybody writes a line of its CSS — which
   is what stops the fourth arbitrary width from ever being added. */
body.doc > main,
body.doc > .contact,
body.doc > .clubs,
body.doc > .club,
body.doc > .join{ width:100%; max-width:var(--w-page); margin-left:auto; margin-right:auto }
/* ⛔ EXCEPT WHERE THE PAGE IS NOTHING BUT PROSE. `body.doc > main` is more
   specific than the `.legal` class the legal page sets on itself, so the frame
   silently won and four thousand words of terms went to 1076px — a line so long
   the eye loses its place returning to the left margin. That is the whole
   reason --w-read exists. It is named here, not in legal.html, so the page
   frame and its one exception are read together. */
body.doc > main.legal{ max-width:var(--w-read) }

/* ⛔ THIS LIVED IN school.css AND THE GUIDE NEVER LOADED IT. guide.html marks
   its content `<div class="wrap-narrow">` exactly as the school does — and
   loads only style.css, so the class matched no rule at all and the guide's
   prose ran the full width of the page frame: 1076px against the school's 760.
   Measured, not guessed. That is the width difference between the guide and
   the lessons that was reported.
   ⚠️ A LAYOUT CLASS USED BY TWO PAGE FAMILIES BELONGS IN THE SHARED SHEET.
   page-frame-test now fails when a page uses one of these and does not load
   the file that defines it, because nothing about the symptom points at the
   cause: the markup is right, the CSS is right, and they never meet. */
.wrap-narrow{ max-width:var(--w-read); margin:0 auto; padding:0 18px 60px }
/* ⛔ AND THE FIRST CHILD'S MARGIN MUST NOT ESCAPE. `.crumb` — the "session 3 of
   202" line the school injects — carries `margin-top:26px`, and with no top
   padding or border on this container that margin COLLAPSES THROUGH it and
   moves the container itself. The school therefore began 70px below the bar
   while all seven other document pages began at 44.
   ⚠️ IT USED TO LINE UP BY ACCIDENT. The container also carried `waiting`, a
   loading state whose name collided with the club queue's card — and that
   card's `padding:14px 15px` was blocking the collapse. Removing the collision
   removed the accident, which is how a real misalignment became visible. The
   spacing above a container belongs to the container. */
.wrap-narrow > :first-child{ margin-top:0 }

/* ⛔ FOUR PAGES, FOUR REPORTS, ONE RULE. The school, the guide, pricing and
   clubs were each reported as "a different width from the others", and each was
   nearly fixed on its own — which is how the site got four widths in the first
   place. The measure is not a judgement any more: it is the progress screen,
   named, and every document page is compared against it in a browser.
   ⚠️ THE CHILDREN, NOT THE FRAME. Constraining <main> itself would put the
   content at 716@362 — inside a 760 box with the frame's own padding — and the
   thing a reader's eye lines up on is the CONTENT edge, not the box around it.
   Each direct child takes the reading measure and centres, so every page's
   content is 760 wide starting at the same place as #progress.
   ⚠️ `.wrap-narrow` already did exactly this and is unaffected. */
body.doc > main > *,
body.doc > .contact > *,
body.doc > .clubs > *{
  max-width:var(--w-read);
  margin-left:auto;
  margin-right:auto;
}
/* ⚠️ CM fonts (round 8) — self-hosted, latin+cyrillic, zero third-party
   request. Owner's call (2026-09-13): Lato for everything read as a
   sentence, a more legible serif for anything read as a headline — see the
   readability comparison in the round-8 report for the x-height numbers
   that picked Bitter over Merriweather/Lora at 390px. Both faces ship with
   real Cyrillic (Lato 2.0 from latofonts.com — NOT the Google Fonts build,
   which is Latin-only; Bitter from Google Fonts, which does carry Cyrillic),
   so unlike the old Cinzel/Barlow pair there is no second "fallback" name
   needed in either stack for Russian/Ukrainian to render in the right font.
   Subsets: latin + the exact typographic characters this site sets
   (en/em dash, smart quotes, ellipsis, €, №) + Cyrillic (0400-045F, plus
   0490-0491 for Ukrainian ґ) — kern only, unhinted, ≤160KB for all six
   files combined (tests/browser/fonts-test.mjs checks the byte count, the
   glyph coverage and that no request ever reaches fonts.googleapis.com or
   fonts.gstatic.com again). One file preloads (Lato 400, the very first
   text painted); the rest load as the page asks for that weight.
   ⚠️ font-display:SWAP, not optional (FONT-CYRILLIC, owner 25.09.2026 —
   REVERSES the `optional` choice BUG-92 made on 17.09, measured wrong).
   BUG-92's complaint was real (`swap`'s reflow when a face was not ready at
   first paint — a one-time twitch), but the `optional` fix traded it for a
   worse, silent failure: measured with Playwright and a throttled connection
   (CDP `Network.emulateNetworkConditions`, 50kbps/300ms) against a face that
   is not already resident, `optional` locks the page into the FALLBACK face
   forever — `document.fonts` reports the real face `[loaded]` seconds later,
   but the rendered text never swaps to it on that page view, ever. `swap`
   self-corrects the moment the file arrives. A visitor who lands slow gets a
   twitch with `swap`; with `optional` they get a heading that is silently,
   permanently the wrong family/weight — Georgia standing in for Bitter, and
   CSS weight-matching rounds a requested 600 UP to Georgia's 700 (no 600 in
   that fallback stack), which is the exact "boldness changes" this ticket
   was filed over: two navigations (e.g. a language switch — always a fresh
   document) independently re-roll this race against whatever happens to be
   cached, so the SAME heading can render at two different weights depending
   on which one lost the race. Preload (see the entry HTML files) is what
   actually answers BUG-92 now: a preloaded file is normally resident before
   the CSSOM asks for it, so `swap` rarely has anything left to swap FROM. */
@font-face{
  font-family:'Lato'; font-style:normal; font-weight:400; font-display:swap;
  src:url('/fonts/lato-400.woff2') format('woff2');
  unicode-range:U+0020-007E,U+00A0-00FF,U+2010-2027,U+2030,U+2039-203A,U+2044,U+20AC,U+2116,U+2122,U+2212,U+FEFF,U+0400-045F,U+0490-0491;
}
@font-face{
  font-family:'Lato'; font-style:italic; font-weight:400; font-display:swap;
  src:url('/fonts/lato-400-italic.woff2') format('woff2');
  unicode-range:U+0020-007E,U+00A0-00FF,U+2010-2027,U+2030,U+2039-203A,U+2044,U+20AC,U+2116,U+2122,U+2212,U+FEFF,U+0400-045F,U+0490-0491;
}
@font-face{
  font-family:'Lato'; font-style:normal; font-weight:600; font-display:swap;
  src:url('/fonts/lato-600.woff2') format('woff2');
  unicode-range:U+0020-007E,U+00A0-00FF,U+2010-2027,U+2030,U+2039-203A,U+2044,U+20AC,U+2116,U+2122,U+2212,U+FEFF,U+0400-045F,U+0490-0491;
}
@font-face{
  font-family:'Lato'; font-style:normal; font-weight:700; font-display:swap;
  src:url('/fonts/lato-700.woff2') format('woff2');
  unicode-range:U+0020-007E,U+00A0-00FF,U+2010-2027,U+2030,U+2039-203A,U+2044,U+20AC,U+2116,U+2122,U+2212,U+FEFF,U+0400-045F,U+0490-0491;
}
@font-face{
  font-family:'Bitter'; font-style:normal; font-weight:600; font-display:swap;
  src:url('/fonts/bitter-600.woff2') format('woff2');
  unicode-range:U+0020-007E,U+00A0-00FF,U+2010-2027,U+2030,U+2039-203A,U+2044,U+20AC,U+2116,U+2122,U+2212,U+FEFF,U+0400-045F,U+0490-0491;
}
@font-face{
  font-family:'Bitter'; font-style:normal; font-weight:700; font-display:swap;
  src:url('/fonts/bitter-700.woff2') format('woff2');
  unicode-range:U+0020-007E,U+00A0-00FF,U+2010-2027,U+2030,U+2039-203A,U+2044,U+20AC,U+2116,U+2122,U+2212,U+FEFF,U+0400-045F,U+0490-0491;
}
/* ⚠️ THE LOGO ONLY. Cinzel is retired everywhere else on the site (Bitter or
   Lato replace it, see the rules below and the comment beside `a.nav-mark`
   further down) — this is the one face still self-hosted for `#start h1`
   and `a.nav-mark`, subset to plain ASCII (the wordmark is always "Mafia",
   never translated — see i18n) at the one weight both of them use, 700. */
@font-face{
  font-family:'Cinzel'; font-style:normal; font-weight:700; font-display:swap;
  src:url('/fonts/cinzel-700.woff2') format('woff2');
  unicode-range:U+0020-007E,U+00A0-00FF;
}
body{
  margin:0; color:var(--bone);
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif;
  font-size:16px; line-height:1.5; -webkit-font-smoothing:antialiased;
  /* plain wood — the same flat room on every page, in every state (BUG-91). */
  background:var(--wood);
}
/* BUG-85 (owner, 17.09: "every page must have the same background as the
   main page"). The lamp used to be painted on `body` itself, and a radial
   gradient sized by the DOCUMENT puts its centre at 45% of the page height:
   on the short door that is mid-screen, on a long lesson it is two screens
   down, and the top of a long page reads as a different, darker room. A fixed
   pseudo-element is sized by the viewport instead — the same lamp, in the same
   place, on the door, the school, legal, the trainer and the clubs alike.
   `html` carries the plain wood so overscroll and printing stay dark. */
/* BUG-87 (owner, 17.09: "a light flash when moving from memory.html to
   #progress"). Between two documents the browser paints its DEFAULT canvas —
   white — until the next page's first frame; declaring the dark colour scheme
   (here and as a <meta> on every page) makes that canvas dark, and `html`
   carries the wood so even the un-styled first frame is the room. */
:root{ color-scheme:dark }
html{ background:var(--wood) }
/* BUG-91 (owner, 17.09: "when I turn html's background off I see a LIGHTER
   one, and some pages blink with it on reload"). The lamp lived here as a
   fixed `body::before` at z-index:-1. In the settled page it was INVISIBLE —
   body's own opaque wood paints over a negative-z pseudo-element — but the
   moment <body> became a stacking context (the door's .14s opacity reveal,
   any opacity < 1) the lamp rose above body's background and the room turned
   lighter for the length of the fade; the same happened whenever html's
   background was gone and body's propagated to the canvas. Measured on the
   stand: centre pixel rgb(26,19,16) settled, rgb(31,22,17) mid-fade. A layer
   that only ever shows in transitional states IS the blink, so it is gone:
   html and body both paint the plain wood, and no state paints anything
   else. w19-1-look §4 holds the door and the trainer at opacity .99 and with
   html's background removed and expects the settled pixels. */
h1,h2,h3,.deco{font-family:Bitter,Georgia,serif; margin:0; font-weight:600; letter-spacing:.02em}
button{font:inherit; cursor:pointer; border:none; background:none; color:inherit}
:focus-visible{outline:2px solid var(--brass); outline-offset:2px}
/* BUG-23 (owner, 16.09: "the yellow outline and the field highlight burn the
   eyes"): a text field ALREADY shows its focus by its own border turning brass
   — the global ring on top of it made a double yellow frame around every box.
   The border alone is the focus mark for fields; the ring stays for buttons
   and links, which have no border to change. */
input:focus-visible,textarea:focus-visible,select:focus-visible{outline:none}
/* BUG-33 (owner, 16.09): a number field shows no system spinner — the grey
   up/down arrows are the browser's own widget in a room that draws every
   control itself; the field still takes digits and the arrow keys. */
input[type=number]{ -moz-appearance:textfield; appearance:textfield }
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button{ -webkit-appearance:none; appearance:none; margin:0 }
.hidden{display:none !important}
/* ⛔ THE `hidden` ATTRIBUTE MEANS HIDDEN, EVERYWHERE, AND THIS STYLESHEET HAS
   NOW BROKEN THAT FOUR TIMES. The browser's own rule is `[hidden]{display:none}`
   — a UA rule, which ANY author `display` outranks. So every time somebody wrote
   `form{display:grid}` or `.learn-chip{display:inline-flex}`, they silently
   switched off `hidden` for that element, and the JavaScript setting
   `el.hidden = true` went on looking correct while the thing stayed on screen.
   What it cost, each time:
     · `.contact form{display:grid}` — a signed-out visitor was told "you need
       an account to write to us" and shown the whole form underneath it,
       with a Send button that could only fail. The comment above that form
       says a form you can fill in and then be refused is worse than no form;
       that is exactly what shipped, for as long as the rule existed.
     · `.learn-chip{display:inline-flex}` — "Learning mode" painted over every
       game, including tables opened from the front door.
     · `.nav-mine` — the same, in the navigation.
     · `form[hidden]` in clubs.html — the first time, patched locally.
   Patching it per element is how it happened four times. One rule, at author
   level, ends the whole class of it: from here on `hidden` cannot be overruled
   by accident. Anything that needs to be visible must not carry the attribute.
   ⚠️ `.hidden` (the class) is deliberately separate and stays — it is the
   React shell's own switch and it already carries `!important`. */
[hidden]{ display:none !important }
/* ⚠️ `overflow:hidden` used to live on bare `body`, so legal.html — which
   shares this stylesheet — inherited a page that could not scroll and a
   viewport-height box that cut the text off. A document page is a document. */
/* ⚠️ THE FOOTER WAS UNREACHABLE, and had been since it was written.
   The shell used to be `height:100%; overflow:hidden` with `.screen` filling
   it, so the footer — a SIBLING of the screens — began exactly one pixel past
   the bottom of the window. The body could not scroll (overflow:hidden) and
   scrolling the screen did not move it, because it was never inside it.
   Terms, Privacy and Refunds were on the page and could not be clicked.
   The first fix (2026-09) kept the fixed shell and turned the footer into a
   pinned last row of a column — which worked, and then read as a FIXED
   footer everywhere except the door (the one screen tall enough that the
   footer sat below the fold instead), which is the owner's bug report
   2026-09-16: one page fixed, the rest floating, and he wants one behaviour
   everywhere. ⚠️ DECISION 2026-09-16 (r8w bug6): `body.app` now lays out
   exactly like `body.doc` above — the PAGE scrolls, every screen is
   auto-height, and the footer is the last flex row: at the bottom of the
   window on a short screen, at the bottom of the content on a long one.
   Nowhere is it fixed or floating; it is the same footer, in the flow, on
   every page. The one exception is `body.playing` below — the table is a
   full-height instrument with no footer at all, and it keeps the OLD
   fixed, non-scrolling shell on purpose. */
body.app{ min-height:100dvh; height:auto; overflow:visible; overflow-x:clip;
          display:flex; flex-direction:column }
body.app > .screen{flex:1 0 auto}
body.app > .site-foot{flex:0 0 auto}

/* ⚠️ #root IS TRANSPARENT TO THE SHELL, and these rules are what make the
   React client lay out identically to a static document page. React mounts
   into a <div>, which puts one element between the body and every screen —
   #root inherits the column and passes the sizing straight through, exactly
   like `body.doc`'s `main` above it. */
body.app > #root{display:flex; flex-direction:column; flex:1 0 auto}
body.app > #root > .screen{flex:1 0 auto; height:auto}
body.app > #root > #start{display:flex; align-items:center; justify-content:center}
body.app > #root > #start > .start-inner{width:100%; margin-block:auto}

/* ⚠️ `body.playing` (the table) is the ONE screen that keeps the fixed shell
   `body.app` gave up above. A hand of Mafia is a full-height instrument — the
   action bar is pinned to the bottom of the WINDOW and only the transcript
   scrolls (see `body.playing #root #game` further down) — and there is no
   footer to reach (`.site-foot{display:none}` below), so a scrolling page
   would only add empty space under a board that already fills it. Same
   specificity trick the rest of this file already uses for `body.playing`
   (`.site-nav`, `.topbar`): the class sits later in the cascade than
   `body.app` above, so it wins ties on equal-specificity selectors without
   needing `!important` anywhere. */
body.playing{ height:100dvh; min-height:0; overflow:hidden }   /* its own window height — the root is document-tall now */
body.playing > #root{ flex:1 1 auto; min-height:0 }

/* ⚠️ A STICKY FOOTER, BECAUSE A SHORT PAGE LEFT IT FLOATING. join.html — the
   page an emailed sign-in link lands on — is a card and one line, so the footer
   sat at 481px with 419 pixels of empty ground beneath it. That reads as a page
   that failed to finish loading, on the one page somebody arrives at from a
   letter and is deciding whether to trust.
   A column with the content growing and the footer as the last row: the footer
   is at the bottom of the WINDOW on a short page and at the bottom of the
   CONTENT on a long one, which is the behaviour everybody expects and nobody
   can name. */
/* ⚠️ `overflow-x: clip` AND NOT `hidden`, and the difference is load-bearing.
   The legal page bleeds its sticky header to the full window with the standard
   `margin: 0 calc(50% - 50vw)` trick, and `50vw` counts the width a scrollbar
   WOULD take — so on a 360px phone the band came out 379px and the whole
   document scrolled sideways by 19. Measured in Chromium with and without a
   mobile context, and on clubs.html for a control: 379 on legal, 360
   everywhere else. A page that slides under your thumb is the single most
   obvious thing a phone layout can get wrong, and it was on the page a person
   opens to read the terms.
   `hidden` would fix the scroll and break the header: it makes the element a
   scroll container, and `position: sticky` does not stick inside one. `clip`
   clips without that. */
body.doc{ min-height:100dvh; overflow:visible; overflow-x:clip;
          display:flex; flex-direction:column }
body.doc > main, body.doc > .contact, body.doc > .clubs, body.doc > .club, body.doc > .join{ flex:1 0 auto }
body.doc > .site-foot{ flex:0 0 auto }

/* ── the reveal beside a password ──────────────────────────────────────────
   ⚠️ THE BUTTON IS INSIDE THE FIELD, NOT BESIDE IT. Put next to the input it
   takes a column of its own and the input shrinks, which on a 448px form column
   costs about eight characters of visible password — the exact thing the button
   exists to give back. `padding-right` on the input is what reserves the room.
   44px of hit area, because WCAG 2.2 SC 2.5.8 applies here: this IS a target. */
/* ── a control that is off because the LANGUAGE says so ────────────────────
   ⚠️ THREE SIGNALS, NOT ONE. Grey alone reads as "loading"; a strike alone
   reads as a typo; `disabled` alone is invisible until somebody presses it.
   Together they say "this cannot do anything, and it is not broken" — which is
   the whole point, because the reason is quality (there is no natural voice for
   Ukrainian or Russian and the robotic fallback is worse than silence), not a
   failure. The sentence beside it carries the why. */
.icon-off{ opacity:.42; cursor:not-allowed; position:relative }
.icon-off::after{
  content:''; position:absolute; left:14%; right:14%; top:50%;
  border-top:2px solid var(--bone-3); transform:rotate(-20deg);
}
.check.is-off{ opacity:.45; cursor:not-allowed }
.check.is-off input{ cursor:not-allowed }
.muted-title{ opacity:.62 }
.no-voice{ border-left:2px solid var(--brass-2); padding-left:11px; line-height:1.55 }

/* A join request an organiser can actually decide on. */
/* The one-line box that opens inside a club row. It has to be narrow enough
   that the table does not reflow, and tall enough to hit on a phone. */
.ask-note-in{ display:block; width:100%; box-sizing:border-box; margin:0 0 6px;
  background:#160F0B; border:1px solid var(--edge); border-radius:4px;
  color:var(--bone); padding:8px 10px; font:inherit; font-size:.9rem; min-height:38px }

.ask-who{ display:flex; flex-direction:column; gap:2px; flex:1 1 auto; min-width:0 }
.ask-who .muted{ font-size:.8rem }
.ask-note{ font-size:.88rem; color:var(--bone-2); font-style:italic; line-height:1.45;
  margin-top:3px; overflow-wrap:anywhere }

/* The one cross-link between the two reading pages. Brass, so it reads as a
   way OUT of this page rather than another anchor inside it — the jump bar is
   full of same-page anchors and a link that leaves has to look different. */
.ch-jump .ch-sib{ color:var(--brass); border-bottom:1px solid var(--brass-2); margin-right:6px }

/* The one gap, after a game. Brass rule on the left like the no-voice note —
   the page's way of saying "this is the product talking to you about you",
   distinct from both a warning and an advertisement. */
.gap-found{ border-left:2px solid var(--brass-2); padding:2px 0 2px 12px; margin:16px 0 0 }
.gap-lead{ font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.74rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--brass); margin:0 0 5px }
.gap-what{ color:var(--bone-2); line-height:1.5; margin:0 0 5px; font-size:.95rem }
/* BUG-58 (owner, 16.09, Женя's rule from BUG-49 applied to the report): the
   tell and the count are not singled out by weight or colour inside the line. */
.gap-what b{ color:inherit; font-weight:inherit }
.gap-found .sub-hint{ margin:0 }

.pw-wrap{position:relative; display:block}
.pw-wrap > input{width:100%; box-sizing:border-box; padding-right:46px}
.pw-eye{
  position:absolute; top:0; right:0; height:100%; width:44px;
  display:flex; align-items:center; justify-content:center;
  background:none; border:0; padding:0; cursor:pointer;
  line-height:0; color:var(--bone-3);
  /* ⚠️ IT WAS 👁 AND 🙈 AND THAT WAS THE PROBLEM. An emoji renders in the
     platform's own colour and style whatever we ask — a photographic eyeball
     on one machine, a cartoon monkey covering its face on another — so a
     password field carried the only two pictures on the page nobody had drawn.
     The two SVGs below are one line each and inherit the text colour, which is
     also how the state stops depending on opacity alone. */
  opacity:.72; transition:opacity .12s ease;
}
.pw-eye:hover{opacity:1}
.pw-eye[aria-pressed="true"]{opacity:1}
.pw-eye:focus-visible{outline:2px solid var(--brass); outline-offset:-2px; border-radius:3px}
.pw-eye svg{ width:20px; height:20px; display:block }

.label{font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.75rem; font-weight:600; letter-spacing:.16em; text-transform:uppercase; color:var(--bone-3)}

/* ── screens ────────────────────────────────────────────── */
/* `height:100%` is the fallback for `body.playing` (see the shell rules
   above) — the table screen fills the fixed window and scrolls only its
   transcript. Everywhere else `body.app > .screen{height:auto}` below wins:
   the screen is exactly as tall as its content and the PAGE scrolls, so
   `overflow-y:auto` never actually triggers there — the box never gets
   taller than what it holds. */
.screen{height:100%; overflow-y:auto; overflow-x:hidden}
body.app > .screen{height:auto}
/* ⚠️ UI/UX pass 3 (2026-09-13) finding #4 — door→table and table→result used
   to cut with `display:none`, the same instant `screen` state changed, on
   all three transitions alike. A short cross-fade instead, door→table and
   table→result only (App.jsx passes `leaving` to Start and Game while the
   OUTGOING screen fades) — WITHOUT a layout shift: `.leaving` is pulled out
   of `#root`'s flex flow with `position:absolute`, so the INCOMING screen
   (still normal flow, already visible) is what decides #root's height, and
   the outgoing one only overlays it while it fades. `hidden` still applies
   exactly as before, just 180ms later — App.jsx's timeout matches the
   transition duration below.
   ⚠️ NOTHING NEW FOR REDUCED MOTION. The site-wide
   `@media(prefers-reduced-motion:reduce){*{transition:none!important}}`
   already cancels the opacity transition; `.leaving` still goes straight to
   opacity 0 for its 180ms, same as it always would have — a reduced-motion
   visitor sees the exact same instant cut as before this change, never a
   NEW thing to sit through. */
#root{position:relative}
.screen{opacity:1; transition:opacity .18s ease}
.screen.leaving{position:absolute; inset:0; opacity:0; pointer-events:none; z-index:1}
/* Centre only while it fits. `align-items:center` on a container that is
   shorter than its content clips BOTH ends — that is how the title went off
   the top the moment the profile panel opened. */
#start,#over{display:flex; align-items:flex-start; justify-content:center; padding:40px 22px}
/* ⚠️ This was `margin:auto 0`. An auto margin on a flex item taller than its
   container centres the overflow — half above the scroll origin, where no
   amount of scrolling reaches it. That is how the title disappeared. The panel
   now starts at the top and simply scrolls, at every window height. */
.start-inner,.over-inner{width:100%; max-width:var(--w-card)}

/* Two figures flanking the start panel — a menu composition, not a background.
   A picture BEHIND text is the mistake; beside it, it is atmosphere.
   They vanish below 1180px rather than crowding the controls. */
/* Two figures flanking the panel — a menu composition, not a background.
   A picture BEHIND text is the mistake; beside it, it is atmosphere.

   The panel gets its own solid ground so nothing bleeds under the controls,
   and the figures are pushed out far enough that the two never touch. They
   disappear entirely when the window is too narrow to hold both. */
/* Both figures were drawn facing the same way, so one of them looked out of
   the page instead of into it. The woman is mirrored to turn her inward; the
   flip lives in a custom property because the entrance keyframes rewrite
   `transform` and would otherwise drop it on the first frame. */
.hero{
  --flip:1;
  position:fixed; top:50%; width:clamp(240px, 26vw, 400px); height:auto;
  transform:translateY(-50%) scaleX(var(--flip));
  pointer-events:none; user-select:none;
  opacity:.85; z-index:0;
}
/* Close enough to the panel to be in the same room as it. They pass BEHIND it
   (z-index 0 against the panel's 1), so the overlap reads as depth, and the
   panel's outer glow does the blending. */
/* BUG-47 (Женя's review, relayed by the owner 16.09): on a wide screen the
   figures stand a little further from the panel — +60px from the centre
   line (the later rule near the type-scale pass, 585 → 645, is the one that
   wins; this one is kept in step). */
.hero-l{left:max(8px, calc(50% - 645px))}
.hero-r{right:max(8px, calc(50% - 645px)); --flip:-1}

/* The panel sits on its own ground — the figures may pass behind it, never
   through the text. A soft outer glow melts the edge into the room. */
.start-inner{
  position:relative; z-index:1;
  padding:30px 32px 34px; border-radius:6px;
  background:radial-gradient(ellipse 120% 100% at 50% 30%, #241A14, var(--wood) 72%);
  box-shadow:
    0 0 0 1px rgba(62,47,36,.55),
    0 0 70px 40px var(--wood),
    0 30px 80px -30px rgba(0,0,0,.95);
}

/* ⚠️ The outer edge was visibly cut. `.hero-r` is the same artwork mirrored with
   scaleX(-1), so whichever side the source image feathers, the mirror puts the
   HARD edge outward — and at 1180–1300px the figure sits only 8px from the
   window edge, where that hard line is impossible to miss.
   Fading the outer 18% of each figure to transparent fixes it at every width,
   without touching the artwork. The mask is applied in the element's own
   (already mirrored) space, so "left" here means "outward" for both. */
/* BUG-47 (owner, 16.09): BOTH edges fade, not only the outer one. The man's
   artwork (hero-m, not mirrored) ends in a hard right edge, and as the window
   narrows the panel's glow stops covering it — the same soft edge the woman
   shows outward is now on the inner side of both figures too. */
/* BUG-84 (owner, 17.09, twice: "the brown square behind the man and the woman
   is a different size"). Each portrait carries its OWN painted backdrop inside
   the artwork — a dark rectangle of a different height in each file — and a
   left/right fade left both rectangles' tops and bottoms on screen. The mask is
   now one soft ELLIPSE, the same for both figures: whatever rectangle the
   artwork has, only an oval around the head and shoulders is painted and it
   melts into the room on every side. */
.hero{
  -webkit-mask-image: radial-gradient(ellipse 48% 52% at 50% 46%, #000 50%, rgba(0,0,0,.6) 72%, transparent 100%);
          mask-image: radial-gradient(ellipse 48% 52% at 50% 46%, #000 50%, rgba(0,0,0,.6) 72%, transparent 100%);
}

/* Below 1180px there is no longer room for a figure, the panel and a margin. */
@media (max-width:1180px){ .hero{display:none} }
/* BUG-99 (owner, 17.09 16:1x: "add a fade-in to the man and the woman, and a
   bit of a resize from 80% to 100%"): the figures come in over .9s from 80%
   size and transparent to their resting .92 opacity. This supersedes the
   BUG-87 note that removed the old heroIn — the blink itself was the lamp
   layer (BUG-91), not the figures. Reduced motion: no animation, resting
   state at once. */
.hero{opacity:.92}
@media (prefers-reduced-motion:no-preference){
  .hero{animation:heroIn .9s ease-out both}
  .hero-r{animation-delay:.12s}
}
@keyframes heroIn{
  from{opacity:0;   transform:translateY(-50%) scaleX(var(--flip)) scale(.8)}
  to  {opacity:.92; transform:translateY(-50%) scaleX(var(--flip)) scale(1)}
}

/* the title on a brass plate, as in the reference */
.title-block{text-align:center; margin-bottom:30px}
.plate{
  display:inline-block; padding:14px 40px; position:relative;
  background:linear-gradient(180deg,#3A2C1E,#241A11);
  border:1px solid var(--brass-2); border-radius:3px;
  box-shadow:inset 0 1px 0 rgba(201,162,39,.28), var(--lift);
}
.plate::before,.plate::after{
  content:''; position:absolute; top:50%; width:7px; height:7px; margin-top:-3.5px;
  border-radius:50%; background:radial-gradient(circle at 34% 30%, #E3C566, #7A6115);
}
.plate::before{left:12px} .plate::after{right:12px}
/* ⚠️ THE M WAS TALLER THAN THE REST OF THE WORD. Not a stray `::first-letter`
   rule and not a second span — measured on canvas at this exact font-size:
   Cinzel's capitals (M/A/F/I) all ink out around 38-40px, its lowercase
   (a/f/i) around 33-34px, ~15% shorter by design. `t('Mafia')` is Title Case,
   so the one real capital in it stood taller than its own neighbours.
   `text-transform:uppercase` renders every glyph from the same cap set —
   the same fix `#over h2`, `.seg button` and `.label` already use for this
   font elsewhere on the page. */
#start h1{font-family:Cinzel,'Playfair Display',Georgia,serif; font-size:clamp(2.4rem,8vw,3.4rem); line-height:1; letter-spacing:.2em; font-weight:700; text-indent:.2em; color:var(--bone); text-transform:uppercase}
.tagline{font-size:.92rem; color:var(--bone-2); margin:16px 0 0}
.tagline b{color:var(--blood); font-weight:600}

.field{margin-bottom:19px}
.field > .label{display:block; margin-bottom:9px}
.field.row{display:flex; gap:22px; flex-wrap:wrap}
/* ⚠️ EVERY TEXT-LIKE INPUT, NOT A LIST THAT FORGETS ONE. This named
   input[type=text] and input[type=email] — so the day a password field was
   added it rendered as the browser's own white box in the middle of a dark
   form, on the screen where somebody is deciding whether to trust this with a
   password. A list of types is a list somebody has to remember to extend;
   :not() covers the ones that are genuinely different (checkbox, radio) and
   admits everything else by default. */
input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]),
textarea,select{
  width:100%; background:var(--wood-2); border:1px solid var(--edge); border-radius:3px;
  color:var(--bone); padding:11px 13px; font:inherit; font-size:.94rem;
}
input::placeholder,textarea::placeholder{color:var(--bone-3)}
input:focus,textarea:focus,select:focus{border-color:var(--brass-2); box-shadow:0 0 0 3px rgba(201,162,39,.10)}
/* The native dropdown arrived as a blue system popup in a sepia room. The
   closed control is fully ours; the open list is styled as far as the browser
   allows (Chrome, Edge and Firefox honour it — Safari draws its own popup and
   there is no web API that changes that). */
.pick{position:relative}
.pick::after{
  content:''; position:absolute; right:15px; top:50%; width:7px; height:7px;
  margin-top:-5px; pointer-events:none;
  border-right:1.5px solid var(--brass); border-bottom:1.5px solid var(--brass);
  transform:rotate(45deg);
}
select{
  appearance:none; -webkit-appearance:none; cursor:pointer; padding-right:38px;
  background:linear-gradient(180deg,#241A13,var(--wood-2));
}
select:hover{border-color:var(--brass-2)}
select option{background:#1B140F; color:var(--bone)}
select option:checked,select option:hover{background:#3A2C1E; color:var(--brass)}
textarea{resize:vertical; font-size:.89rem}
.sub-hint{font-size:.86rem; color:var(--bone-3); margin:7px 0 0; line-height:1.45}
/* ⚠️ SAME RULE AS .legal p > a:only-child BELOW, SAME REASON. A <p> whose
   ENTIRE content is one link is not a sentence for WCAG 2.2 SC 2.5.8's inline
   exception (see tests/browser/a11y-test.mjs's own comment on that check) —
   it is a button wearing a link's clothes, and .sub-hint's line-height alone
   rendered it at 17px tall. Settings.jsx's "Read the help page" link (the
   help fold added 2026-09-13) was the first one caught; this fixes the class,
   not just that one link, the same way school.css already fixes it for the
   lesson pages. */
.sub-hint > a:only-child{ display:inline-flex; align-items:center; min-height:24px; }
/* ⚠️ Every link inside the panel was rendering in the browser's default blue,
   underlined in blue — the one colour on the page that belongs to 1994. It is
   the same defect as the white checkbox and the blue dropdown: a control left
   to the user agent. Brass, with a brass underline that only appears on hover. */
/* ⚠️ AND THE RULE IS THE DEFAULT NOW, NOT A LIST OF THREE CONTAINERS. Naming
   the places where links look right means every new page starts with links that
   look wrong — pricing.html shipped with browser-default blue at 2.23:1 against
   this background, on the page that decides whether anybody pays, and no check
   had ever looked at it. A page should have to opt OUT of being legible, never
   in. The old selectors stay as a no-op reminder of what they cost. */
/* BUG-63 (owner, 16.09): a link in the running text is WHITE, underlined —
   brass only when the pointer is on it. Brass at rest made every link a
   highlight in prose that is meant to read evenly (Женя's BUG-49 rule); the
   underline still says "this is a link". Button-shaped links (.primary,
   .outline, .ghost), the bar (.nav-links), chips (.ch-index-links) and the
   footer set their own colours and are not touched by this. */
a{
  color:var(--bone); text-decoration:none;
  border-bottom:1px solid rgba(239,231,216,.38);
  transition:border-color .14s, color .14s;
}
a:hover{ color:var(--brass); border-bottom-color:var(--brass); }
/* ⛔ A BUTTON THAT HAPPENS TO BE AN <a> IS NOT A LINK (owner, 16.09: "Write to
   us" on help.html grew a yellow line underneath on hover, and its text went
   yellow). `a:hover` above recolours the bottom border and the text of EVERY
   anchor; `.primary` alone (0,1,0) loses to `a:hover` (0,1,1). `.btn`, `.ghost`
   and `.outline.big-play` already win with their own two-class hover rules;
   the plain red button did not. Its own colours, in every state: */
a.primary:hover{ color:#fff; border-bottom-color:var(--blood-2); }
/* The consent line is a sentence, not a row of links: they stay in the running
   text colour so the eye reads the sentence and not four blue words. */
/* ⚠️ ONE MECHANISM FOR ONE ACT. The terms checkbox is gone: the button states
   the agreement and this sentence names what is being agreed to, immediately
   beside it. `.wanted` is the nudge when somebody submits with Enter before the
   act has happened. */
.consent-line{
  margin:10px 0 0; font-size:.82rem; line-height:1.5; color:var(--bone-3);
}
.consent-line a{color:inherit; text-decoration:underline; text-underline-offset:2px}
.consent-line a:hover{color:var(--brass)}
.consent-line.wanted{color:var(--brass)}

.check.consent a{color:inherit; border-bottom:none; text-decoration:underline; text-underline-offset:2px}
.check.consent a:hover{color:var(--brass)}

/* ⚠️ Hard-coded at repeat(4,1fr). Coaching has THREE buttons, so it drew a
   fourth, empty, clickable-looking cell — the "missing button" on screen.
   Auto columns fit whatever is actually in the control. */
.seg{display:grid; grid-auto-flow:column; grid-auto-columns:1fr; gap:1px; background:var(--edge); border:1px solid var(--edge); border-radius:3px; overflow:hidden}
/* pages-review 2026-09-16, finding #6 (cosmetic) — 11px was 41px tall,
   short of the 44px WCAG 2.2 AAA / Apple HIG target size; a11y-test.mjs only
   requires 24px and still passed, so this is polish, not a fix. Covers the
   difficulty pills AND the Timer/Manual toggle — both are `.seg button`. */
.seg button{padding:13px 4px; background:var(--wood-2); color:var(--bone-2); font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.8rem; letter-spacing:.06em; text-transform:uppercase; transition:.14s}
.seg button:hover{background:var(--wood-3); color:var(--bone)}
/* ⚠️ BRASS, NOT BLOOD, AND THE PHONE HAD IT RIGHT FIRST. Red is this site's
   ACT colour — it is on Play, on Deal again, on the one button a screen wants
   pressed. Difficulty is a setting: pressing it does nothing, it only says
   which of four you are on. Painting it in the same red as Play put two
   equally loud reds on the front door and made a preference look like a call
   to action. The phone has used the brass tier for this since it was written.
   ⛔ There is one red control per screen. If a second one appears, one of them
   is not an act. */
/* ⚠️ CONTRAST, NOT JUST COLOUR — UI review by Zhenya, round 8 (2026-09-13). The
   old "on" state was brass text on a dark brass-tinted gradient — warm on
   warm, and it read as low-contrast even where it technically cleared 4.5:1.
   Swapped to the SAME solid-fill pairing already used elsewhere in this file
   for "this is the highlighted one" (.grade, .upsell-buttons .plan.best): a
   solid brass fill with near-black wood text measures ~7.6:1, comfortably
   clear, and reuses tokens that already exist rather than inventing a color. */
.seg button.on{ background:var(--brass); color:var(--wood);
                box-shadow:inset 0 0 0 1px var(--brass-2) }

/* FED-LESSONS — the organiser's own federation picker (club.js). A <select>,
   not a checkbox, so it does not reuse `.check`'s custom-drawn box below. */
.fed-pick{display:flex; flex-wrap:wrap; align-items:center; gap:9px; margin-top:14px; font-size:.95rem; color:var(--bone-2)}
.fed-pick select{min-width:180px}

.check{display:flex; align-items:center; gap:9px; cursor:pointer; font-size:.95rem; color:var(--bone-2)}
/* A native checkbox is a bright white OS control dropped into a dark room —
   the one element on the page that belongs to somebody else's design. Drawn
   here instead: an unlit brass plate that lights when it is ticked. */
/* ⚠️ WEBKIT WILL NOT LET YOU STYLE THIS ELEMENT, and two attempts proved it:
   `border:1px solid var(--bone-4)` computed to rgb(15,13,11) in Safari and
   rgb(124,110,94) in Chrome, and replacing the token with the literal changed
   nothing — WebKit keeps its own near-black border on a checkbox whatever the
   page says. On Safari the one box a player MUST tick to play was invisible.
   So the input is not styled at all any more. It stays for semantics, keyboard
   and the form; the box a person sees is drawn by the LABEL, which is an
   ordinary element that both engines render the way they are told. */
.check{ position:relative; }
.check input[type=checkbox]{
  position:absolute; left:0; top:50%; transform:translateY(-50%);
  width:17px; height:17px; margin:0; opacity:0; cursor:pointer;
}
.check::before{
  content:''; flex:0 0 auto; width:17px; height:17px; border-radius:3px;
  border:1px solid #7C6E5E;
  background:linear-gradient(180deg,#241A13,#1B140F);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.045);
  transition:border-color .14s, background .14s, box-shadow .14s;
}
/* ⛔ THE TICK WAS A ROTATED BORDER, NUDGED BY GUESSWORK, AND IT SAT LOW IN
   EVERY BOX ON THE SITE. The old rule drew an L out of two borders on a 5x9
   element and turned it 45 degrees, then corrected the result by eye with
   `translateY(-62%)` — a number nobody can derive and nobody checked. Measured
   at 4x on a real checkbox: horizontally true to within a tenth of a pixel,
   and 0.8px BELOW centre vertically, in a box 17px tall. Reported repeatedly as
   the tick not being centred, and it was not.
   ⚠️ THE FIX IS TO STOP DOING TRIGONOMETRY IN CSS. A stroked path in a square
   viewBox, painted as a centred background, is centred by construction: the
   ink's own bounding box is symmetric about the middle of the 24-unit box, and
   `background-position: center` does the rest at any size. No rotation, no
   magic percentage, and it stays true if the box is ever resized.
   ⚠️ A data URI, not a file — one request fewer, and `img-src 'self' data:`
   already allows it. The `#` in the colour must be written `%23` or the URI
   ends at the hash and the tick silently never paints. */
/* ⛔ AND THERE IS NO SEPARATE TICK ELEMENT ANY MORE — that was the whole bug.
   `::before` is the box and is a flex ITEM; `::after` was the tick and was
   ABSOLUTELY POSITIONED against the label. Those two things are only in the
   same place when the label is exactly one line tall, so the stylesheet had
   grown a nudge per label shape: `top:6px` for the multi-line ones,
   `translateY(-62%)` for the one-line ones, and a paragraph of comment
   explaining that they must be kept in step by hand. Measured against the box
   itself, the tick still sat 2.1px high in a 19px box. Reported, correctly, as
   the tick not being centred — on every checkbox on the site.
   ⚠️ SO THE TICK IS PAINTED INSIDE THE BOX, as a second background layer of
   `::before`. A background centred in its own element cannot drift from that
   element, whatever the label does around it: one element, no anchor to get
   wrong, and every per-variant nudge deleted.
   ⚠️ `#` MUST BE `%23` in a data URI or the URI ends at the hash and the tick
   silently never paints. */
.check::before{ background-repeat:no-repeat; background-position:center; background-size:0 0 }
.check:hover::before{ border-color:var(--brass-2); }
.check:has(input:checked)::before{ border-color:var(--brass);
  background-color:#2E2116; background-image:none; }
.check:has(input:checked)::before{
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23C9A227' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12 L10 17 L19 7'/%3E%3C/svg%3E");
  background-size:11px 11px;
}
.check:has(input:focus-visible)::before{ outline:2px solid var(--brass); outline-offset:2px; }
.check:has(input:disabled){ opacity:.5; }
/* And when the game is waiting for exactly this tick, it says so in the only
   language a form has: the control itself. */
.check.wanted::before{ border-color:var(--brass); box-shadow:0 0 0 3px rgba(201,162,39,.22); }
/* ── jumping to an anchor ────────────────────────────────────────────────────
   Both document pages have a sticky bar and a list of anchors, and a hard jump
   drops the reader somewhere with no sense of having travelled — on the
   characters page it is indistinguishable from the page having reloaded.
   `scroll-margin-top` is the other half and matters more than the animation:
   without it the heading lands UNDER the sticky bar, which is the actual defect
   an anchor link usually has. */
html{ scroll-behavior:smooth; }
:target{ scroll-margin-top:76px; }
.ch, h2[id], h3[id]{ scroll-margin-top:76px; }
/* ⚠️ A person who has asked their operating system to stop moving things has
   asked for exactly this too. */
@media (prefers-reduced-motion:reduce){ html{ scroll-behavior:auto; } }

@media (prefers-reduced-motion:reduce){ .check input[type=checkbox]::after{transition:none} }

.primary{
  width:100%; padding:15px; border-radius:3px; color:#fff;
  background:linear-gradient(180deg,#C22C33,var(--blood-2));
  border:1px solid var(--blood-2); box-shadow:inset 0 1px 0 rgba(255,255,255,.14), var(--lift);
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-weight:600; font-size:.93rem; letter-spacing:.15em; text-transform:uppercase;
  transition:filter .14s;
}
.primary:hover{filter:brightness(1.13)}
.primary:disabled{opacity:.45; cursor:default}
.ghost{padding:12px 20px; border:1px solid var(--edge); border-radius:3px; background:var(--wood-2); color:var(--bone-2); font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.8rem; letter-spacing:.08em; text-transform:uppercase; transition:.14s}
.ghost:hover{border-color:var(--brass); color:var(--bone)}
.ghost.small{padding:9px 15px; font-size:.76rem}
.ghost.on{border-color:var(--brass); color:var(--brass); background:var(--brass-dim)}
/* ⚠️ UI/UX pass 3 (2026-09-13) finding #3 — `:hover` never fires on a touch
   screen, and this product's own audience is mostly phones. Every one of
   these was pressable and gave NO visual answer until the server did — Play,
   Deal again, a voice confirmation, all silent under a thumb. A small scale
   + brightness dip, gone the instant the finger lifts: never a colour swing,
   because RULE 5 keeps exactly one red button on screen and a red flash on
   press would read as a second one. The phone's own buttons already get
   this for free from Pressable/TouchableOpacity's native ripple+haptics. */
.primary:active,.btn:active,.ghost:active{transform:scale(.97); filter:brightness(.92)}
.stats{font-size:.86rem; color:var(--bone-3); margin:16px 0 0; text-align:center; font-variant-numeric:tabular-nums}

.fold{border:1px solid var(--edge); border-radius:3px; background:var(--wood-2); margin-bottom:17px}
.fold summary{padding:13px 14px; cursor:pointer; list-style:none; display:flex; align-items:center; gap:10px; color:var(--bone-2); font-size:.92rem}
.fold summary::-webkit-details-marker{display:none}
/* ⚠️ ONLY ONE AUTO-MARGIN PER ROW — UI review by Zhenya, round 8 (2026-09-13).
   This chevron used to carry its OWN margin-left:auto, alongside .badge's
   below. Two auto margins in the same flex row split the free space between
   them, so the badge landed at a variable mid-point (proportional to how much
   free space that row's title text left) instead of sitting flush against the
   chevron — reported as values "scattered" at different widths per row.
   Removing it here leaves .badge as the ONLY auto margin: it now absorbs all
   the free space, pushing [badge, chevron] as one flush unit to the row's
   right edge — the chevron is fixed-width ('+' or '−'), so the badge's
   right edge lands at the same x-coordinate on every row, regardless of the
   title's or the badge's own text length. */
.fold summary::after{content:'+'; color:var(--bone-3); font-family:"JetBrains Mono",monospace}
.fold[open] summary::after{content:'−'}
.fold summary:hover{color:var(--bone)}
.fold-body{padding:0 14px 14px; border-top:1px solid var(--edge-2)}
/* ⚠️ SAY IT OUT LOUD RATHER THAN RELYING ON THE BROWSER. A <details> hides its
   own contents by default, and this page was NOT doing it: measured at 360px,
   `#dangerFold` was closed and `#pauseAccount` still reported display:block and
   a 56px box at y=697, sitting on top of the profile fold's own text. The
   user-agent rule that hides details content is easy for any author `display`
   to beat, and once it is beaten nothing tells you — the page just quietly
   draws two sections in one place.
   Found by responsive-test, which measures overlapping text at the three
   viewports people actually hold. It was invisible on a laptop because there
   is room for the mistake there. */
details.fold:not([open]) > .fold-body{ display:none }
.badge{font-family:"JetBrains Mono",monospace; font-size:.72rem; color:var(--bone-3); margin-left:auto}

.engine{display:grid; gap:1px; background:var(--edge); border:1px solid var(--edge); border-radius:3px; overflow:hidden; margin-bottom:12px}
.engine button{display:grid; grid-template-columns:1fr auto; gap:10px; padding:11px 13px; background:var(--wood-2); text-align:left; transition:.14s}
.engine button:hover{background:var(--wood-3)}
.engine button.on{background:var(--brass-dim)}
.engine .en{font-size:.89rem; color:var(--bone)}
.engine .ed{display:block; font-size:.83rem; color:var(--bone-2); margin-top:2px}
.engine button.on .en{color:var(--brass)}
.engine .tick{font-family:"JetBrains Mono",monospace; font-size:.7rem; color:var(--bone-3); align-self:center}
.engine button.on .tick{color:var(--brass)}
.dl{height:3px; background:var(--edge); border-radius:2px; overflow:hidden; margin-top:10px}
.dl i{display:block; height:100%; background:var(--brass); width:0; transition:width .3s}

/* ── game shell ─────────────────────────────────────────── */
#game{display:flex; flex-direction:column}
.bar{
  display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:14px;
  padding:0 18px; height:56px; flex:0 0 auto; position:relative; z-index:5;
  background:linear-gradient(180deg,#261C15,#1C1410); border-bottom:1px solid var(--edge);
}
.bar::after{content:''; position:absolute; left:0; right:0; bottom:-1px; height:1px; background:var(--rule); opacity:.35}
.bar-l{display:flex; align-items:center; gap:11px}
.bar-r{display:flex; align-items:center; gap:11px; justify-self:end}
.day{font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.98rem; letter-spacing:.14em; text-transform:uppercase}
.dot{width:3px; height:3px; border-radius:50%; background:var(--bone-3)}
#aliveCount{font-size:.83rem; color:var(--bone-2); font-variant-numeric:tabular-nums}
/* ⛔ THE PHASE IS A LABEL, NOT A BUTTON (owner, 16.09; Женя's review 13.09
   said the same: "выглядит кнопкой, но не нажимается"). The old plate carried
   a brass border, a gradient and an inset highlight — exactly the recipe the
   site's outlined buttons use, so a status read as something to press. A
   status is set as text only: uppercase, letter-spaced, brass, no box. */
.phase-plate{
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.8rem; letter-spacing:.18em; text-transform:uppercase;
  padding:6px 0; color:var(--brass); white-space:nowrap;
  background:none; border:0; border-radius:0; box-shadow:none;
}
/* ⛔ THE ROLE IS A LABEL, NOT A BUTTON — same complaint, same fix as the phase
   plate above: no border, no fill. The team is carried by the colour of the
   word and a small dot before it, which no button on this site has. */
.role-chip{font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.78rem; letter-spacing:.13em; text-transform:uppercase; padding:0 4px; border:0; border-radius:0; background:none; color:var(--bone-2)}
.role-chip::before{ content:''; display:inline-block; width:7px; height:7px; border-radius:50%; margin-right:7px; background:currentColor; flex:0 0 auto }
.role-chip:empty::before, .role-chip.none::before{ display:none }
.role-chip.civ,.role-chip.sheriff{ color:var(--brass) }
.role-chip.mafia{ color:var(--blood-lit) }
/* ⚠️ w15-fix-game-3 #1/#2 — 44×44px IS THE FLOOR, NOT A SUGGESTION. Measured
   in the DOM at 390×844: #quit was 29.2×34.1px with an 11px gap to #mute
   right next to it — a destructive control (leave a LIVE game) one light
   thumb-miss away from the button that only silences the table. min-width/
   min-height give both the tap target WCAG 2.5.8 asks for without changing
   the glyph's size or the bar's 56px height (44 fits inside it); the
   inline-flex centres the emoji now that the box is bigger than its text. */
.icon{font-size:.92rem; padding:6px 9px; border-radius:3px; color:var(--bone-3);
      min-width:44px; min-height:44px; display:inline-flex; align-items:center; justify-content:center}
.icon:hover{background:var(--wood-3); color:var(--bone)}
.icon:active{background:var(--wood-3); color:var(--bone); transform:scale(.92)}

/* ── CB game page (r8w game-fixes, Женя review 13.09) ──────────────────────
   ⚠️ NOTHING IN THE HEADER LOOKS LIKE A BUTTON UNLESS IT IS ONE. The phase
   plate, the role chip and the coach chip are all plain `<span>`s with no
   click handler — but `.phase-plate`'s border + inset shadow and the chips'
   own borders read, at a glance, as pressable chrome, and Женя's review named
   exactly that ("выглядит кнопкой, но не нажимается"). None of the three ever
   had a `cursor:pointer` or a `:hover` state — this makes that explicit and
   permanent rather than merely accidental, so a future hover effect added
   for one of the OTHER `.icon`/button elements in this bar can never bleed
   onto these by a shared selector. */
/* BUG-34 (owner, 16.09: "an acid-yellow frame when the game opens"): the
   screen switch moves focus onto this [role=status] plate for screen readers
   (App.jsx), and the sitewide :focus-visible ring drew a box around the phase
   word. Nobody can Tab to it (tabindex -1), so the ring told a sighted reader
   nothing — the announcement is the whole point, the frame was noise. */
.phase-plate:focus,.phase-plate:focus-visible{ outline:none }
/* BUG-52 (owner, 16.09: "Your progress" boxed in acid yellow on arrival): the
   same thing on every screen — App.jsx moves focus to the screen's heading
   (tabindex="-1") so a screen reader announces where it landed. Nobody can
   Tab to a tabindex="-1" element, so the ring told a sighted reader nothing
   and framed the title. No ring on any programmatic-only focus target. */
[tabindex="-1"]:focus,[tabindex="-1"]:focus-visible{ outline:none }
.phase-plate,.role-chip,.coach-chip{ cursor:default }
.phase-plate:hover,.role-chip:hover,.coach-chip:hover{ background:inherit; color:inherit }

/* The mute icon's own tooltip. `title=` alone never appears on a touch
   screen, and the disabled-with-a-tooltip version of this control is gone
   (see Game.jsx's own note by `#mute`) — when the icon exists at all it is a
   real toggle, and this is how it explains itself: instantly, in the page's
   own type, on hover, on keyboard focus AND on a tap (`:active`), the same
   three ways in `.demo-tag`'s tooltip already uses elsewhere on this site. */
.icon[data-tip]{ position:relative }
.icon[data-tip]::after{
  content:attr(data-tip); position:absolute; top:calc(100% + 8px); right:0;
  width:max-content; max-width:220px; white-space:normal; text-align:left;
  font-family:inherit; font-size:.78rem; font-weight:400; line-height:1.4;
  background:#241A14; color:var(--bone-2); border:1px solid var(--brass-2); border-radius:4px;
  padding:7px 10px; box-shadow:0 12px 30px -12px rgba(0,0,0,.9);
  opacity:0; pointer-events:none; transform:translateY(-4px); transition:opacity .12s ease, transform .12s ease;
  z-index:70;
}
.icon[data-tip]:hover::after,.icon[data-tip]:focus-visible::after,.icon[data-tip]:active::after{
  opacity:1; transform:translateY(0);
}
@media (prefers-reduced-motion:reduce){ .icon[data-tip]::after{ transition:none } }

/* The door's "continue where you left off" button (Start.jsx) — outlined,
   like `.outline.big-play` (Learn), never filled: Play is the one primary
   action a stranger sees, and a returning player with an open table gets a
   second, quieter door rather than a second competing primary. */
/* ⚠️ CENTRED BY THE ROW, NOT BY THE BUTTON. `.outline.big-play` below makes the
   button `display:inline-flex`, which throws away `.big-play`'s `display:block;
   margin:auto` — an inline-level box with a max-width sits at the LEFT of its
   line, and the owner saw exactly that (16.09: "Continue game · day 1" hugging
   the left edge of the card). `text-align:center` on the paragraph is what
   centres an inline-level child; the width cap stays the shared 320px measure. */
.continue-row{ margin:14px 0 0; text-align:center }
.continue-row .outline{ width:100%; max-width:320px; box-sizing:border-box }

/* ⚠️ 250px was not enough for a status row carrying two labels — SPEAKING and
   PARTNER ran into each other, and so did NAMED ×2. Widened rather than made
   smaller: the labels are words on purpose, and shrinking type to fit is how a
   card becomes unreadable instead of crowded. */
.board{flex:1; display:grid; grid-template-columns:296px 1fr; min-height:0}
@media (max-width:900px){ .board{grid-template-columns:1fr} }

/* ── seat cards ─────────────────────────────────────────── */
.seats{
  background:linear-gradient(180deg,#20170F,#1A1310);
  border-right:1px solid var(--edge); overflow-y:auto;
  padding:12px 10px 20px; display:flex; flex-direction:column; gap:7px;
}
.seats-head{padding:2px 4px 8px}
.seat{
  display:grid; grid-template-columns:22px 46px 1fr; align-items:center; gap:11px;
  padding:8px 10px; width:100%; text-align:left; position:relative;
  background:linear-gradient(180deg,var(--wood-3),#241A14);
  border:1px solid var(--edge); border-radius:var(--card);
  box-shadow:var(--lift); transition:.15s;
}
.seat .num{font-family:"JetBrains Mono",monospace; font-size:.78rem; color:var(--bone-3); text-align:center}
/* portrait slot — a monogram now, a painted face when the art arrives */
/* The portraits already carry their own gold ring, so the slot must add
   nothing — a square border around a framed circle reads as a double frame.
   The monogram fallback keeps the frame, because there it IS the frame. */
.portrait{
  width:46px; height:46px; display:grid; place-items:center; position:relative;
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:1.02rem; font-weight:700; color:var(--bone-3);
}
.portrait:not(:has(img)){
  border-radius:50%; border:1px solid var(--edge);
  background:linear-gradient(165deg,#3C2D22,#241A13);
}
.portrait img{width:100%; height:100%; object-fit:contain; display:block}
.seat.speaking .portrait img{filter:drop-shadow(0 0 7px rgba(201,162,39,.55))}
.seat.accused .portrait img{filter:drop-shadow(0 0 7px rgba(180,38,44,.6))}
.seat-main{min-width:0}
.seat .nm{display:block; font-size:.97rem; color:var(--bone); overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.pill{
  display:inline-flex; align-items:center; gap:5px; margin-top:3px;
  font-family:"JetBrains Mono",monospace; font-size:.75rem; letter-spacing:.07em; text-transform:uppercase;
  color:var(--bone-3);
}
.pill i{width:5px; height:5px; border-radius:50%; background:var(--bone-3); display:block}
.pill.alive i{background:var(--alive); box-shadow:0 0 6px rgba(94,154,98,.7)}
.pill.accused{color:var(--blood-lit)} .pill.accused i{background:var(--blood); box-shadow:0 0 6px rgba(180,38,44,.8)}
.pill.speaking{color:var(--brass)} .pill.speaking i{background:var(--brass); box-shadow:0 0 7px rgba(201,162,39,.85)}
.pill.known{color:var(--known)} .pill.known i{background:var(--known-2); box-shadow:0 0 6px rgba(201,162,39,.5)}

.seat.you{border-color:var(--brass-2)}
.seat.you .portrait{border-color:var(--brass-2); color:var(--brass)}
.seat.you .nm{font-weight:600}
.seat.speaking{
  border-color:var(--brass);
  background:linear-gradient(180deg,#3A2E17,#241C10);
  box-shadow:0 0 0 1px rgba(201,162,39,.22), 0 0 24px -6px rgba(201,162,39,.4), var(--lift);
}
.seat.accused{border-color:var(--blood-2); background:linear-gradient(180deg,#33191A,#241213)}
/* Identity, so it must survive being named: `accused` still paints the red
   border and the red ground, and this adds a mark the red cannot remove. */
.seat.known.accused{border-color:var(--blood-2)}
/* A labelled chip, in the same place every time, in words. It sits UNDER the
   state pill rather than in a corner, because a corner is where the eye goes
   last and this is a fact the player is not allowed to miss. */
/* ⚠️ First version was appended to the SEAT, which is a flex row, so the chip
   wrapped onto a line of its own and hung outside the card. It belongs in the
   text column, directly under the state pill — same column, same rhythm. */
/* One status row: what is happening to this seat on the left, what only you
   know on the right. */
/* BUG-44 (owner, 16.09: "the 'familiar face' marker does not fit"): the row
   may wrap — a long marker (знайоме обличчя / знакомое лицо) takes the next
   line instead of being clipped beside the state pill. */
/* `position:relative` is BUG-142's anchor for `.pick-tag` below — a plain
   flex container otherwise, unaffected while nothing pickable is inside it. */
.pill-row{ display:flex; align-items:center; gap:4px 8px; min-width:0; flex-wrap:wrap; position:relative; }
.pill-row .pill{ min-width:0; }
.seat-known{
  /* BUG-57 (owner, 16.09): the sheriff's check (and a partner mark) is a
     third line under the name and the state pill, left-aligned with them —
     not pushed to the card's right edge. flex-basis:100% wraps it onto its
     own row; max-width:max-content keeps the box to its own words. The phone
     already lays it out this way (Seats.jsx knownChip, alignSelf flex-start). */
  margin-left:0; padding:1px 6px; flex:0 0 100%; max-width:max-content;
  font-family:"JetBrains Mono",monospace; font-size:.6rem; letter-spacing:.11em;
  text-transform:uppercase; line-height:1.7; white-space:nowrap;
  color:var(--bone-3); border:0; border-radius:2px;
  background:rgba(201,162,39,.10);
  pointer-events:none;
  max-width:100%; overflow:hidden; text-overflow:ellipsis;
}
/* And a stripe down the edge, so the card is identifiable at a glance without
   reading anything at all. */
.seat.known{ box-shadow:inset 3px 0 0 var(--known-2); }
.seat{position:relative}
.seat.dead{opacity:.34}
.seat.dead .nm{text-decoration:line-through; text-decoration-thickness:1px}
.seat.dead .portrait{filter:grayscale(1) brightness(.7)}
/* ⚠️ BUG-120 — A PICKABLE SEAT LOOKS PICKABLE BEFORE IT IS TOUCHED. Until now
   the ONLY thing that separated a legal target from a card that does nothing
   was `cursor:pointer` and a hover state — and hover does not exist on a phone
   at all, which is where this game is mostly played. So the player was told
   "click a seat" over a column of ten identical cards and had to find out which
   ones answer by trying them. The resting state is now a brass frame (3:1
   against the wood, WCAG 1.4.11 — a state indicator, not decoration), a brass
   left stripe that survives at a glance, and the word "pick" on the card,
   because colour alone is unreadable to roughly one man in twelve. Hover and
   press keep the blood-red they had: that is the "about to happen" state and it
   has to stay louder than the "may happen" one.
   ⚠️ ONE RING, NOT TWO. A full brass frame AND an inset ring on nine cards at
   once turned the column into nine glowing boxes that competed with the
   player's own "your turn" seat — the one card that must stay the loudest
   thing in the list. A dim frame plus the 2px brass stripe down the edge reads
   as "available" at a glance and leaves the turn ring alone. */
.seat.pickable{cursor:pointer; border-color:var(--brass-dim); box-shadow:inset 2px 0 0 var(--brass)}
/* ⚠️ BUG-142 (owner, 18.09, screenshot) — `.pick-tag` used to sit ABOVE this
   row, a sibling of `.pill-row` inside `.seat-main` (a plain block, not a
   flex container) — so it fell into normal document flow as a second line
   under the name, `order:4` never applying to anything, and every eligible
   card grew a line taller than a resting one the instant a targeting step
   opened, jumping the whole seat list.
   ⚠️ THE FIRST FIX HERE swapped the pill's own TEXT to "pick" in place —
   same line, same height — but game-board-contract-test.mjs holds that text
   to the server's truth (a named seat must literally say "named", not
   "pick"), so that version broke a real guarantee to fix a cosmetic one.
   This version changes NEITHER the pill's text NOR its DOM position: the
   real pill is hidden with `visibility:hidden` (which — unlike `display:
   none` — keeps its box's size, so the row's height is exactly what it was
   with the real text painted), and `.pick-tag` is an absolutely positioned
   overlay INSIDE the now-`position:relative` row, filling that same box.
   Out-of-flow means it can never add height or move a sibling, at rest or
   while armed. `position:relative` on `.pill-row` itself lives with its own
   rule, above. */
.seat.pickable .pill{visibility:hidden}
.seat.pickable .pick-tag{
  position:absolute; inset:0 auto 0 0; display:flex; align-items:center;
  font-family:"JetBrains Mono",monospace; font-size:.75rem; letter-spacing:.07em;
  text-transform:uppercase; color:var(--brass); white-space:nowrap; pointer-events:none;
}
/* ⚠️ AND WHAT IS NOT ON OFFER IS QUIET. A partner at night, a seat already
   checked, a name that is not on the ballot: offered-and-ignored is the failure
   this project has fixed three times now, and a dimmed card with `aria-disabled`
   says the same thing to a screen reader. Dead seats keep their own .34 and the
   player's own seat keeps the brass "your turn" ring — see Seats.jsx. */
.seats.picking .seat.dimmed{opacity:.46; filter:saturate(.6)}
.seat.pickable:hover{border-color:var(--blood); background:linear-gradient(180deg,#3B2020,#2A1616); transform:translateX(2px)}
/* UI/UX pass 3 (2026-09-13) finding #3 — the most-pressed control in the
   whole game (every vote, kill and check is a seat tap) had no press state
   at all on a touch screen. Keeps the hover shift, adds a small press-down. */
.seat.pickable:active{border-color:var(--blood); background:linear-gradient(180deg,#3B2020,#2A1616); transform:translateX(2px) scale(.97); filter:brightness(.92)}
/* ⚠️ Absolutely positioned at the right edge, so it sat ON TOP of the identity
   tag — "NAMED ×2" printed straight through "PARTNER". Nothing on this card is
   positioned over anything any more: the row lays itself out. */
.seat .tally{
  order:3; flex:0 0 auto; margin-left:6px;
  font-family:"JetBrains Mono",monospace; font-size:.66rem; color:var(--blood-lit);
}

/* ⚠️ UX REVIEW, WEB PASS 2 (2026-09-13), FINDING #3. Ten of these while
   `/api/game` is in flight, same grid, same size as the real thing that
   replaces them — so the column never collapses to nothing while a hand is
   being dealt. Same idiom as `.seat.your-turn` below: a keyframe pulse, and a
   solid state (never `animation:none` with nothing to look at) under reduced
   motion, so a skeleton that cannot move still visibly IS one. */
.seat.skeleton{pointer-events:none}
.seat.skeleton .num, .seat.skeleton .portrait, .seat.skeleton .sk-line{
  background:rgba(255,255,255,.07); border:0; border-radius:3px;
  animation:skeleton-pulse 1.4s ease-in-out infinite;
}
.seat.skeleton .sk-line{display:block; height:11px}
.seat.skeleton .sk-name{width:58%; margin-bottom:7px}
.seat.skeleton .sk-pill{width:36%; height:9px}
@keyframes skeleton-pulse{ 0%,100%{opacity:.5} 50%{opacity:1} }
@media (prefers-reduced-motion: reduce){
  .seat.skeleton .num, .seat.skeleton .portrait, .seat.skeleton .sk-line{
    animation:none; opacity:.75;
  }
}

/* ── transcript ─────────────────────────────────────────── */
.talk{display:flex; flex-direction:column; min-width:0; min-height:0; position:relative;
  background:radial-gradient(ellipse 90% 70% at 50% 0%, rgba(201,162,39,.03), transparent 60%)}
.transcript{flex:1; overflow-y:auto; padding:26px 34px 14px}
.line{margin-bottom:18px; max-width:66ch; animation:rise .2s ease-out}
@keyframes rise{from{opacity:0; transform:translateY(3px)} to{opacity:1; transform:none}}
.line .who{font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.76rem; letter-spacing:.14em; text-transform:uppercase; color:var(--brass-2); display:block; margin-bottom:5px}
.line .txt{font-size:1.06rem; line-height:1.66; color:var(--bone)}
.line.you .who{color:var(--brass)}
/* BUG-35 (owner, 16.09: "mark your own lines, quietly"): the same thin left
   rule the table's own notes carry (.line.sys), in brass at half strength —
   scannable down the column, never a highlight. The "· you" after the name
   and the brass name stay. */
.line.you{ border-left:2px solid rgba(201,162,39,.45); padding-left:14px }
/* ⚠️ .95rem AGAINST 1.06rem FOR SPEECH, AND THAT IS THE POINT — a system line
   is quieter than a person talking. A measurement pass flagged "11.84px" here
   and I nearly enlarged it; the 11.84px belongs to `.day` below, which is not
   body text at all but a centred small-caps chapter divider with rules either
   side. A number read without its context is not evidence. */
.line.sys{max-width:none; border-left:2px solid var(--edge); padding-left:14px; color:var(--bone-2); font-size:.95rem}
.line.sys b{color:var(--bone); font-weight:600}
.line.sys.day{
  border-left:none; text-align:center; margin:26px 0 22px; padding:0;
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.74rem; letter-spacing:.26em; text-transform:uppercase; color:var(--brass);
  position:relative;
}
.line.sys.day::before,.line.sys.day::after{
  content:''; position:absolute; top:50%; width:24%; height:1px; background:var(--rule); opacity:.5;
}
.line.sys.day::before{left:4%} .line.sys.day::after{right:4%}
.line.sys.death{border-left-color:var(--blood)} .line.sys.death b{color:var(--blood)}
.line.sys.check{border-left-color:var(--brass)} .line.sys.check b{color:var(--brass)}
.caret{display:inline-block; width:8px; height:1.05em; background:var(--brass); vertical-align:-2px; animation:blink .9s steps(2) infinite}
@keyframes blink{50%{opacity:0}}

/* ── action bar ─────────────────────────────────────────── */
.act{
  grid-column:1 / -1; flex:0 0 auto; padding:14px 24px 16px; position:relative;
  background:linear-gradient(180deg,#241A13,#1B1410); border-top:1px solid var(--edge);
}
.act::before{content:''; position:absolute; left:0; right:0; top:-1px; height:1px; background:var(--rule); opacity:.35}
/* BUG-73 (owner, 17.09: "when it is my turn it is not clear I have to press a
   button"): the whole bar becomes the cue — a lifted ground and the prompt a
   notch larger. It arrives with a short fade, never a blink, and stays still
   until the turn is over. Buttons keep their own look: one accent, on the
   container, is enough.
   ⛔ BUG-120 (the same owner, the same day, with the screenshot): THE 3px BRASS
   BAR IS GONE. `inset 0 2px 0 var(--brass)` plus a third line under it drew a
   solid acid-yellow rule the full width of the panel — the loudest thing on a
   screen whose whole design is quiet wood and bone, and it shouted a fact
   ("it is your turn") the words underneath it were already stating. Lichess
   makes the same call in the other direction and defends it: a legal-move dot
   and a ~10% tint, deliberately kept subtle against years of requests to make
   them louder, because the board is what the player is reading. What replaces
   the bar is a soft brass glow rising off the top edge, the lifted ground, and
   — the part that actually answers the complaint — an instruction line that
   changes with the step (game.js/Controls.jsx) over a board that marks the
   seats which may be picked (.seat.pickable below). */
.act.your-turn{ border-top-color:var(--brass-dim); background:linear-gradient(180deg,#2A2017,#1B1410);
  box-shadow:0 -14px 26px -18px rgba(201,162,39,.55) }
.act.your-turn .act-head .label{ font-size:.95rem; letter-spacing:.12em }
@media (prefers-reduced-motion:no-preference){ .act.your-turn{ animation:turnIn .5s ease-out both } }
@keyframes turnIn{ from{ box-shadow:0 -14px 26px -18px rgba(201,162,39,0) } to{ box-shadow:0 -14px 26px -18px rgba(201,162,39,.55) } }
.act-head{display:flex; align-items:baseline; gap:12px; margin-bottom:12px; min-height:17px}
.act-head .label{color:var(--brass)}
.act-head .sub{font-size:.86rem; color:var(--bone-3)}
.act-row{display:flex; gap:10px; align-items:center; flex-wrap:wrap; min-height:44px}

/* ⛔ TWO BUTTONS THAT DO THE SAME KIND OF THING MUST BE THE SAME SIZE. Pause and
   Delete sat in a wrapping row beside a note, so each was as wide as its own
   word — "ПРИЗУПИНИТИ АКАУНТ" against "ВИДАЛИТИ МОЇ ДАНІ" — and in Ukrainian,
   where both words are longer, they wrapped onto separate lines at different
   widths with the note stranded on the right. Two ragged buttons under a
   heading read as an unfinished screen, and this is the screen where somebody
   deletes their account.
   ⚠️ The note goes on its own line rather than fighting them for the row: it
   describes the whole panel, not the button beside it. */
.act-row.danger-row{ flex-wrap:wrap; align-items:stretch }
.act-row.danger-row > button{ flex:1 1 200px; min-width:0; text-align:center; justify-content:center }
.act-row.danger-row > .saved-note{ flex:1 0 100%; text-align:left; margin-top:2px }
/* pages-review 2026-09-16, finding #6 (cosmetic) — 11px measured 38-39px tall
   on some instances (drill's answer options); +1px closes the gap to the
   44px WCAG 2.2 AAA / Apple HIG target without a visible size change. */
.btn{
  padding:12px 20px; border-radius:3px; color:var(--bone-2);
  background:linear-gradient(180deg,var(--wood-3),#231A14); border:1px solid var(--edge);
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.84rem; letter-spacing:.07em; text-transform:uppercase;
  box-shadow:var(--lift); transition:.14s;
}
.btn:hover{border-color:var(--brass-2); color:var(--bone)}
/* ⛔ `.primary` AND `.btn` HAVE THE SAME SPECIFICITY, AND `.btn` IS DECLARED
   LATER — so `class="primary btn"` rendered as a plain `.btn` and the red fill
   never appeared. Measured on the pricing page: all three plan buttons came
   back with the identical gradient, the identical text colour and the identical
   border. That page carries a BEST VALUE badge over the middle plan and then
   gave its call to action no more weight than the two beside it — the one
   element the whole page exists to have pressed, drawn as an also-ran.
   ⚠️ `.btn.primary`, not `!important`: two classes beat one, the cascade stays
   readable, and the rule says what it is for. */
.btn.primary{
  color:#fff;
  background:linear-gradient(180deg,#C22C33,var(--blood-2));
  border-color:var(--blood-2);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14), var(--lift);
}
.btn.primary:hover{ border-color:var(--blood-2); color:#fff; filter:brightness(1.13) }
.btn.on{
  background:linear-gradient(180deg,#C22C33,var(--blood-2)); border-color:var(--blood-2); color:#fff;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14), var(--lift);
}
/* ⚠️ UX REVIEW, WEB PASS 2 (2026-09-13), FINDING #2. `pricing.js` sets
   `aria-disabled="true"` on a buy button the instant it is pressed, and this
   is the rule that makes that visible — before it, the attribute changed and
   nothing on screen did: no dimming, no cursor change, nothing but the
   `#buySaid` line (itself misplaced — see pricing.html). On the page where
   somebody is about to pay, a press that LOOKS like nothing happened is a
   press repeated, which is a second Stripe Checkout session started for no
   reason. `pointer-events:none` also stops that second press outright, not
   only the look of it. */
.buy[aria-disabled="true"]{opacity:.5; pointer-events:none; cursor:default}
.spacer{flex:1}
.mic{
  display:flex; align-items:center; gap:9px; padding:10px 16px; border-radius:3px;
  background:linear-gradient(180deg,var(--wood-3),#231A14); border:1px solid var(--edge);
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.8rem; letter-spacing:.1em; text-transform:uppercase; color:var(--bone-2);
}
.mic:hover{border-color:var(--brass-2)}
.mic:active{border-color:var(--brass-2); transform:scale(.97)}
.mic.live{border-color:var(--blood); background:var(--blood-dim); color:var(--blood)}
.mic kbd{font-family:"JetBrains Mono",monospace; font-size:.61rem; padding:2px 6px; border:1px solid var(--edge); border-radius:2px; color:var(--bone-3); letter-spacing:0}
.heard{min-height:21px; margin-top:9px; font-size:.9rem; color:var(--bone-2); font-style:italic; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.heard.err{color:var(--blood-2); font-style:normal}

/* ── result ─────────────────────────────────────────────── */
#over h2{font-size:clamp(1.8rem,6vw,2.5rem); letter-spacing:.14em; text-transform:uppercase}
#over h2.won{color:var(--brass)} #over h2.lost{color:var(--blood)}
.reveal{display:flex; flex-direction:column; gap:5px; margin-bottom:24px}
.rv{display:grid; grid-template-columns:36px 1fr auto; gap:12px; align-items:center; padding:10px 13px;
  background:linear-gradient(180deg,var(--wood-3),#241A14); border:1px solid var(--edge); border-radius:3px; font-size:.9rem}
.rv .tk{width:34px; height:34px; border-radius:3px; display:grid; place-items:center; font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-weight:700; font-size:.85rem; background:linear-gradient(165deg,#3C2D22,#241A13); border:1px solid var(--edge); color:var(--bone-3); overflow:hidden}
.rv .tk img{width:100%; height:100%; object-fit:contain}
.rv .tk:has(img){border:none; background:none}
.rv .r{font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; color:var(--bone-3)}
.rv.black{border-color:var(--blood-2)}   /* BUG-74 (owner, 17.09): no red fill — the border and the role word carry the team */
.rv.black .r{color:var(--blood)}
/* BUG-70 (owner, 17.09): the sheriff stands out in the reveal — the game's own
   green (the "alive" dot), a hairline of it on the row. */
.rv.sheriff{border-color:rgba(94,154,98,.55)}   /* BUG-74: no green fill either */
.rv.sheriff .r{color:var(--alive)}
.rv.me{border-color:var(--brass-2)}
/* ⚠️ UI/UX pass 3 (2026-09-13) finding #2 — the reveal is the tensest moment
   in the game (Gnosia, Town of Salem and Wolvesville all stagger it) and it
   used to draw all ten cards in one frame. `animation-delay` is set inline,
   per card, in Over.jsx (`i * 70`ms) — the keyframe and duration live here so
   the delay is the ONLY thing JSX has to know about. `backwards` fill-mode
   holds each card at the keyframe's `from` state (opacity 0) for its own
   delay instead of flashing visible-then-hidden-then-animating.
   ⚠️ REDUCED MOTION NEEDS NO EXTRA RULE. The existing site-wide
   `@media(prefers-reduced-motion:reduce){*{animation:none!important}}` below
   already cancels this — every card reverts to its un-animated base state
   (opacity 1, no delay to wait out) and the whole table shows at once. */
@keyframes revealIn{from{opacity:0; transform:translateY(4px)} to{opacity:1; transform:none}}
.rv{animation:revealIn .32s ease backwards}
.over-actions{display:flex; gap:10px; margin-bottom:22px}
/* BUG-42 (owner, 16.09): every button in the row is the same size — same
   width, same height, same label size — "Deal again" and "Back to the start"
   used to be a wide red block next to a small ghost pill. */
.over-actions > .primary, .over-actions > .ghost, .over-actions > a{
  flex:1 1 0; min-width:0; width:auto; padding:14px 10px; text-align:center; white-space:normal;
  font-size:.8rem; letter-spacing:.08em; text-transform:uppercase; line-height:1.25;
  display:inline-flex; align-items:center; justify-content:center;
}

.cast{display:flex; flex-direction:column; gap:6px}
.cast-row{display:grid; grid-template-columns:70px 1fr 32px; gap:9px; align-items:center}
.cast-row .who{font-size:.82rem; color:var(--bone-2)}
.cast-row select{font-size:.78rem; padding:6px 8px}
.cast-row .play{padding:6px 0; border:1px solid var(--edge); border-radius:2px; font-size:.73rem; color:var(--bone-3)}
.cast-row .play:hover{border-color:var(--brass); color:var(--brass)}

/* ── mobile ─────────────────────────────────────────────── */
@media (max-width:820px){
  .bar{grid-template-columns:1fr auto; height:50px}
  .phase-plate{display:none}
  .board{grid-template-columns:1fr; grid-template-rows:auto 1fr}
  .seats{border-right:none; border-bottom:1px solid var(--edge); flex-direction:row; overflow-x:auto; overflow-y:hidden; padding:10px 12px; gap:8px; position:relative}
  .seats-head{display:none}
  /* ⚠️ GATE16-FIX (gate w16, first pass, finding #3) — ONLY 4 OF 10 SEATS FIT
     THIS ROW AT 390px, AND NOTHING ON SCREEN SAID SO. Same fix and same
     reasoning as `.nav-links::after` above: a sticky, zero-disruption
     `::after` — the LAST flex child, so no `.seat` before it moves or
     resizes, and `seat-overflow-test.mjs` (which measures each `.seat`'s own
     name/pill/badge boxes) never touches it — pinned to the visible right
     edge of the scrollport, faded to `.seats`'s own background colour
     (the darker end of its own `linear-gradient(180deg,#20170F,#1A1310)`,
     see the base `.seats` rule) so it reads as the row itself running out,
     not a foreign overlay. */
  .seats::after{
    content:''; flex:0 0 26px; align-self:stretch;
    position:sticky; right:-1px; pointer-events:none;
    background:linear-gradient(to right, transparent, #1A1310 65%);
  }
  /* ⚠️ UK-SEAT (round 8, 14.09) — a name or badge that outgrew this card used
     to paint straight over the next seat: nothing here ever clipped its own
     content, and `.pill-row` never wrapped, so a state pill (living/named)
     PLUS an identity badge (partner/checked) together are wider than an
     84px column the instant either string is longer than English's
     shortest case — measured on this exact build: uk "живий"+"партнер" and
     ru "жив"+"партнёр" both overflow the card by 14–23px and bleed 6–15px
     into the next seat (tests/browser/seat-overflow-test.mjs). `overflow:
     hidden` on `.seat` is the hard backstop — nothing ever bleeds past THIS
     card's own border again, in any language — and the wrap + ellipsis
     rules below are what keep that backstop from visibly clipping anything
     in the first place: the badge drops to its own line before it would
     have to cut a single letter. Widened 84→92px only to give short badges
     (uk "живий", ru "жив") room to sit on one line with the name above them,
     the way they already do in English. */
  .seat{grid-template-columns:auto; grid-auto-flow:row; justify-items:center; gap:5px; min-width:92px; padding:8px 10px; text-align:center; overflow:hidden}
  .seat .num{display:none}
  .seat .nm{font-size:.8rem; max-width:82px}
  .seat .seat-main{max-width:82px}
  .seat .pill-row{flex-wrap:wrap; justify-content:center; row-gap:3px}
  .seat .pill{max-width:82px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
  /* BUG-44: on a phone the marker wraps onto two short lines inside its
     82px column rather than ending in an ellipsis. */
  .seat .seat-known{margin-left:0; max-width:82px; white-space:normal; line-height:1.35; text-align:center}
  .transcript{padding:18px 18px 8px}
  .act{padding:12px 16px 14px}
  .btn{padding:10px 14px; font-size:.7rem}
}
@media (prefers-reduced-motion:reduce){*{animation:none !important; transition:none !important}}

/* ── your own face ──────────────────────────────────────── */
/* Sex tabs — the grid used to be one scrolling column of 36 with the women
   below the fold, so a player saw only men and reasonably concluded that was
   all there was. */
.face-tabs{display:flex; gap:1px; background:var(--edge); border:1px solid var(--edge); border-radius:3px; overflow:hidden; margin-bottom:10px}
.face-tabs button{flex:1; padding:8px 4px; background:var(--wood-2); color:var(--bone-2); font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.7rem; letter-spacing:.1em; text-transform:uppercase; transition:.14s}
.face-tabs button:hover{background:var(--wood-3); color:var(--bone)}
.face-tabs button.on{background:var(--brass-dim); color:var(--brass)}

/* 24 faces, men and women mixed, six across — four rows, no scrolling.
   ⚠️ SIX ACROSS STOPS BEING SIX ON A SMALL PHONE. At 320px each face came out
   23.8 pixels square — under WCAG 2.2's 24px floor, and measured rather than
   guessed, because rounding in the report showed them as "24×24" and made them
   look like they only just passed.
   24 is the floor, not the target: the same guideline calls 44px the
   comfortable size, and these are round targets, so the corners of the nominal
   square are not hittable at all. Auto-fit rather than a fixed six means the
   grid gives up a column instead of shrinking below what a thumb can hit. */
.face-grid{display:grid; grid-template-columns:repeat(auto-fit, minmax(40px, 1fr)); gap:9px; padding:3px}
@media (min-width:400px){ .face-grid{grid-template-columns:repeat(6,1fr)} }
.face-grid button{
  aspect-ratio:1; border-radius:50%; padding:0; overflow:hidden; position:relative;
  border:2px solid transparent; transition:.14s; background:none;
}
.face-grid button img{width:100%; height:100%; display:block; object-fit:contain}
.face-grid button:hover{transform:scale(1.09)}
.face-grid button.on{border-color:var(--brass); box-shadow:0 0 0 3px rgba(201,162,39,.28)}
/* ⚠️ The tick lived here and was CLIPPED: the button is round with
   `overflow:hidden`, so a mark at right:-1px bottom:-1px was cut in half
   against the portrait and read as a smudge on the picture rather than a
   confirmation. The replacement is further down — it sits outside the clip,
   on its own ground, with a ring that separates it from the face. */

/* ── the one upsell, and only when payments are actually switched on ── */
/* ⚠️ ONE BLOCK, NOT A BAR PLUS A CLOSED FOLD. The offer used to be a flex row
   with the button in it, and the reason to press that button lived in a
   separate <details> underneath that started shut — so the price and the
   argument for it were never on screen at the same time. Now it is a column:
   headline, the two tiers side by side, the button inside the paid tier, and
   the terms directly under the button, where the decision is actually made. */
.offer{
  /* BUG-61 (Женя, 16.09): no fill behind "The coach" — the frame alone. */
  display:block; margin-bottom:17px; padding:15px 16px;
  border:1px solid var(--brass-2); border-radius:3px; background:none;
}
.offer .tiers{margin-top:13px}
/* (the layout of this row lives in ONE place — search `.upsell-buttons`) */
.offer .upsell-buttons .ghost{border-color:var(--brass-2); color:var(--brass); white-space:nowrap}
.offer .upsell-buttons .ghost:hover{background:var(--brass); color:var(--wood)}
/* ⚠️ THIS USED TO FILL THE MONTHLY BUTTON AND GHOST THE YEARLY ONE, which lit
   the more expensive plan. Emphasis follows the arithmetic now and lives on
   `.plan.best` — search `a plan is a card you press`. */
.offer-terms{margin:13px 0 0; text-align:center}
@media (max-width:560px){ .offer .tiers{grid-template-columns:1fr} }

.upsell{
  display:flex; align-items:center; gap:14px; margin-bottom:17px; padding:13px 15px;
  border:1px solid var(--brass-2); border-radius:3px; background:var(--brass-dim);
}
.upsell-l{flex:1; min-width:0}
.upsell-t{display:block; font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.86rem; letter-spacing:.08em; text-transform:uppercase; color:var(--brass)}
.upsell-d{display:block; font-size:.84rem; color:var(--bone-2); margin-top:3px}
.upsell .ghost{border-color:var(--brass-2); color:var(--brass); white-space:nowrap}
.upsell .ghost:hover{background:var(--brass); color:var(--wood)}

/* ══ BT progress coach — round 8, Женя's review 2026-09-13 ═══════════════
   TWO BUGS, ONE PAYING SUBSCRIBER'S PANEL: (a) `.offer.mine` inherited the
   brass sales wash `.offer` wears while it is still an OFFER — right for a
   stranger deciding whether to pay, wrong once they already have, where
   Progress.jsx's own comment above already says this panel "is not an
   offer, it is a renewal date". It gets the same quiet card `.fold` gives
   "Reads you have made" / "Badges" above it: panel background, a thin
   `--line` border, no gold fill. The status word stays gold — `.upsell-t`
   (#upsellTitle) already carries `color:var(--brass)`, so "Coach — active"
   reads gold IN THE TEXT once the wash behind it is gone, with no change
   needed there. The pre-purchase `.offer` (not `.mine`) is untouched: that
   one is still selling and keeps its highlight on purpose.
   (b) #requestRefund/#autoRefund/#storeRefundInfo carried their spacing as
   a JSX inline style (`style={{ marginTop: 10 }}`) repeated three times —
   moved here as one rule so the number lives in one place, the same way
   every other spacing decision on this page does. The button itself was
   already `.ghost.small` (9px 15px padding, .76rem font) and already read
   under the 44px / MANAGE-height ceiling; this just locks that in with a
   test rather than leaving it to coincidence. */
/* ⚠️ круг 8, theme-consistency pass (2026-09-14) — `--line` REMOVED, NOT
   RE-DEFINED HERE. It was added as a stopgap alias for --edge (var(--line)
   had no fallback and computed to the guaranteed-invalid value, so five
   borders were silently dropped) but an alias with no value of its own is a
   second name for the same colour — a reader comparing computed styles on
   two panels sees `--edge` on one and `--line` on the other and reasonably
   assumes they differ. They never did. Every caller below now reads
   `var(--edge)` directly; the token itself is retired. */
#progress .offer.mine{
  background:var(--wood-2);
  border-color:var(--edge);
}
#progress .offer.mine #requestRefund,
#progress .offer.mine #autoRefund,
#progress .offer.mine #storeRefundInfo{
  margin-top:10px; /* ≤12px, one place now — was a repeated inline style */
}

/* ═══ the coach report — the paid feature ══════════════════════════════
   It has to look like the reason to pay, so it gets the brass rule and the
   raised card the rest of the room only hints at. Everything here is text and
   two-colour bars: no images, no fonts, nothing that costs a request. */
.coach{
  margin:26px 0 4px; padding:20px 22px 22px;
  background:var(--wood-2); border:1px solid var(--edge); border-radius:var(--card);
  box-shadow:var(--lift); text-align:left;
}
.coach::before{ content:''; display:block; height:2px; margin:-20px -22px 18px; background:var(--rule) }
.coach-head{ display:flex; gap:14px; align-items:flex-start; margin-bottom:18px }
.coach-line{ margin:0; color:var(--bone); font-size:1rem; line-height:1.5 }
/* the one-line synopsis — "Best read: … · N found · M fair · K missed" — one
   line by design, so it never pushes the free turning point below the fold
   (report-ux, M-1 row 13). Full report only; the teaser never has `.verdict`
   to count. */
.coach-summary{ margin:0 0 16px; font-size:.86rem; color:var(--bone-2); letter-spacing:.02em }
.sum-best{ color:var(--brass); font-weight:700 }
.coach-h{
  margin:22px 0 12px; font-size:.78rem; font-weight:700;
  letter-spacing:.10em; text-transform:uppercase; color:var(--bone-3);
}

/* the grade, on a brass token */
.grade{
  flex:none; width:44px; height:44px; border-radius:50%;
  display:grid; place-items:center; font-size:1.35rem; font-weight:700;
  color:var(--wood); background:var(--brass); border:2px solid var(--brass-2);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.35);
}
.g-D,.g-F{ background:var(--blood); color:var(--bone); border-color:var(--blood-2) }

/* you, against a perfect reader of the same evidence */
.bars{ display:grid; gap:9px }
.bar-row{ display:grid; grid-template-columns:104px 1fr 44px; gap:10px; align-items:center }
.bar-lab{ font-size:.82rem; color:var(--bone-2) }
.bar-track{ height:9px; border-radius:5px; background:var(--edge-2); overflow:hidden }
.bar-fill{ display:block; height:100%; border-radius:5px }
.bar-fill.you{ background:var(--brass) }
.bar-fill.engine{ background:var(--alive) }
.bar-fill.chance{ background:var(--bone-3) }
.bar-val{ font-size:.82rem; color:var(--bone); text-align:right; font-variant-numeric:tabular-nums }

/* the lock. It names what is behind it — a locked panel that says nothing
   sells nothing — without naming a single player. */
.coach-lock{
  margin-top:20px; padding:18px; text-align:center;
  border:1px dashed var(--brass-2); border-radius:var(--card);
  background:linear-gradient(180deg, rgba(201,162,39,.06), transparent);
}
.lock-h{ margin:0 0 6px; color:var(--bone); font-size:.95rem; line-height:1.5 }
.coach-lock .sub-hint{ margin:0 0 14px }

/* the teaching moments */
.misses{ display:grid; gap:12px }
.miss{ display:grid; grid-template-columns:52px 1fr; gap:12px; align-items:start }
.miss-day{
  font-size:.72rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--brass); padding-top:2px;
}
.miss-note{ margin:0 0 3px; color:var(--bone); font-size:.92rem; line-height:1.5 }
.miss-t .sub-hint{ margin:0 }

/* the ledger */
.rounds{ display:grid; gap:1px; background:var(--edge-2); border-radius:var(--card); overflow:hidden }
.rnd{
  display:grid; grid-template-columns:34px 1fr 1fr 52px; gap:10px; align-items:center;
  padding:9px 12px; background:var(--wood-3); font-size:.86rem;
}
.rnd-d{ color:var(--bone-3); font-weight:700; font-size:.76rem }
.rnd-you{ color:var(--bone) }
.rnd-eng{ color:var(--bone-2) }
.rnd-v{
  text-align:right; font-size:.7rem; font-weight:700;
  letter-spacing:.08em; text-transform:uppercase; color:var(--bone-3);
}
.rnd.hit .rnd-v{ color:var(--alive) }

@media (max-width:560px){
  .rnd{ grid-template-columns:30px 1fr 44px }
  .rnd-eng{ display:none }
  .bar-row{ grid-template-columns:88px 1fr 40px }
}

/* ═══ what the subscription actually is ═══════════════════════════════
   A price with no list beside it is a button people do not press. */
.tiers{display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-top:12px}
.tier{
  background:var(--wood-3); border:1px solid var(--edge); border-radius:var(--card);
  padding:15px 16px;
}
.tier.paid{border-color:var(--brass-2); background:linear-gradient(180deg, rgba(201,162,39,.07), var(--wood-3))}
.tier-n{
  display:block; font-size:.72rem; font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color:var(--bone-3); margin-bottom:10px;
}
.tier.paid .tier-n{color:var(--brass)}
.tier-n b{font-weight:700; color:var(--bone); letter-spacing:0; text-transform:none}
.tier ul{margin:0; padding-left:17px; display:grid; gap:6px}
.tier li{font-size:.86rem; color:var(--bone-2); line-height:1.45}
.tier.paid li{color:var(--bone)}
.tier li strong{color:var(--brass)}

/* ═══ the footer nobody reads until they are about to pay ═════════════ */
.site-foot{
  width:100%; max-width:var(--w-page); margin:0 auto; padding:9px 18px 11px;
  /* ⛔ NO RULE ABOVE IT. `border-top` here drew a 900px line across the page,
     and on a SHORT page — contact, a marked lesson — the footer is pushed to
     the bottom of the window while the content ends far above it, so the line
     hung alone in 164px of empty room with nothing on either side of it.
     Measured on contact.html: card bottom 785px, line 949px. Reported as "a
     horizontal line that is not needed", and it was right — a separator is
     only a separator when there is something directly above it to separate.
     Distance does the same job here and cannot land in the wrong place. */
  display:flex; gap:14px; flex-wrap:wrap; align-items:center; justify-content:center;
  font-size:.7rem; color:var(--bone-3);
}
.site-foot a{color:var(--bone-3); text-decoration:none; border-bottom:1px solid var(--edge)}
.site-foot a:hover{color:var(--brass); border-bottom-color:var(--brass-2)}
.site-foot a:focus-visible{outline:2px solid var(--brass); outline-offset:3px}
/* No footer at the table. A game screen is a full-height instrument — every
   row of legal chrome under it steals a row from the transcript, which is what
   pushed the board up the moment the footer appeared. Every game in this genre
   does the same: chrome on the lobby, nothing on the board. */
body.playing .site-foot{display:none}

/* ⚠️ THE ACTION BAR SLID DOWN WHEN THE TRANSCRIPT FILLED UP. `#game` is a
   `.screen`, and `.screen` carries `overflow-y:auto` — so once the talk column
   was taller than the window the WHOLE game screen scrolled, taking the buttons
   with it. Only one thing on this screen is allowed to scroll, and it is the
   transcript. The board takes the room that is left; the bar is pinned to the
   bottom of the window and stays there whatever the table says.
   ⚠️ `#root #game` (two ids), NOT `body.playing #game` (one id + one class) —
   since the r8w bug6 footer fix, `body.app > #root > .screen` (one id + two
   classes) sets every OTHER screen to `flex:1 0 auto; height:auto` so it can
   grow with the page instead of being clipped to the window. Same specificity
   class, so the table needs a selector that outranks it outright rather than
   relying on cascade order, or the board would grow with the transcript and
   push the page instead of scrolling it internally. */
body.playing #root #game{ flex:1 1 auto; min-height:0; height:auto; overflow:hidden; }
body.playing #game > .board{ min-height:0; overflow:hidden; }
body.playing #game > .act{ flex:0 0 auto; }

@media (max-width:560px){ .tiers{grid-template-columns:1fr} }

/* A renewal inside seven days changes the panel's colour. Not alarming — just
   impossible to miss, because the alternative is a surprise charge. */
.upsell.warn{
  border-color:var(--brass-2);
  background:linear-gradient(180deg, rgba(201,162,39,.10), var(--wood-3));
}
.upsell.warn .upsell-t{color:var(--brass)}


/* ── registration + plan picker (added with the coach positioning) ───────── */
/* The promise under the title. It is the sentence that says what this is, so
   it gets the width of the panel and none of the tagline's weight. */
/* ⚠️ I18N-LAZY-CLS (r8w/clsdict, круг 8) — MEASURED, NOT GUESSED: this line
   wraps to 62px (English and Russian, three physical lines inside its own
   34ch column) or 41px (Ukrainian, two lines) — AT EVERY WIDTH, because 34ch
   is narrower than any viewport this site supports, so a viewport media
   query cannot scope it. The English prerender is always what a reader sees
   first (App.jsx: "English until hydration"); a uk/ru reader whose
   dictionary chunk is still loading over Slow 3G then watches this line
   shrink out from under the title the moment it lands — measured 0.027–0.030
   CLS on the door, only on uk/ru, only on throttled networks
   (docs/UX/2026-09-13---СКОРОСТЬ-НА-ТЕЛЕФОНЕ--замер.md, 14.09 entry). Fast
   networks never showed it because the dictionary usually wins the race
   against first paint. `min-height` pinned to the TALLEST of the three
   variants removes the vertical move outright, in both directions, for
   every reader — English included, since English is what the shrink was
   measured FROM. */
.promise { max-width: 34ch; margin: 8px auto 0; line-height: 1.5; min-height: 62px; }

/* ⛔ THE LIVE TABLE'S RULES WERE HERE AND ARE GONE, round 8, along with the
   component itself (LiveTable.jsx, deleted) and its generated lines — see
   Start.jsx for why. */
.check.consent { align-items: flex-start; line-height: 1.45; }
.check.consent a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
/* ⚠️ THREE RULES CALLED `.upsell-buttons` USED TO EXIST IN THIS FILE — here,
   at the top of the offer block, and again nine hundred lines down. The last
   one won, so the monthly button took `flex:1 1 auto; min-width:200px` and the
   whole row, its label wrapped onto two lines, and the yearly button fell to
   the next line and was pushed to the right edge by the `justify-content` in
   THIS rule. Nobody chose that layout; it is what three authors each fixing
   half the problem produces.
   ⛔ ONE RULE NOW, and it is this one. Do not add a second — search the file
   before styling this row again.
   Two prices, equal width, side by side, in a grid rather than a wrap: a wrap
   is what let one button end up alone on a line. Under 420px they stack, which
   is the same grid with one column. */
.upsell-buttons{
  display:grid; grid-template-columns:1fr 1fr; gap:10px;
  align-items:stretch; margin:16px 0 0; justify-content:stretch;
}
.upsell-buttons button{ width:100%; min-width:0; white-space:normal; line-height:1.3 }
/* The two prices must not be different heights — one taller button reads as a
   mistake even when the label is simply longer. */
.upsell-buttons #upgrade, .upsell-buttons #upgradeYear{ min-height:46px }

/* ── a plan is a card you press ──────────────────────────────────────────────
   ⚠️ THE PRICE IS THE LABEL AND THE ARITHMETIC IS UNDER IT. $49 is a bigger
   number than $5.99 and reads as more expensive until the line underneath says
   $4.08 a month — that line is the entire reason the yearly plan sells, and it
   used to live in a `title` tooltip, which is invisible on every phone. */
.upsell-buttons .plan{
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
  position:relative; padding:13px 12px 12px; border-radius:3px; cursor:pointer;
  border:1px solid var(--brass-2); background:transparent; color:var(--brass);
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; transition:.14s;
}
.upsell-buttons .plan:hover{ background:rgba(201,162,39,.10) }
/* ⚠️ THE PRICE MUST FIT ON ONE LINE. "$5.99 CAD / month" wrapped at .92rem and
   the two cards stopped being the same shape — one price on two lines beside
   one price on one line reads as a broken card, not as a longer word. */
.upsell-buttons .plan .plan-price{
  font-size:.84rem; letter-spacing:.03em; font-weight:600; line-height:1.25;
  white-space:nowrap;
}
/* ⚠️ ROUND 8 CM — was "Cinzel has no lowercase, so the price's parent
   (Cinzel) was wrong for this sentence, give it the prose face instead".
   The parent is Lato now (normal lowercase, no small-caps quirk), so this
   override no longer escapes anything — kept explicit anyway, so this line
   never silently drifts if the parent's font ever changes again. */
.upsell-buttons .plan .plan-note{
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif;
  font-size:.72rem; letter-spacing:0; line-height:1.35; font-weight:400;
  opacity:.78; text-transform:none;
}
/* The plan nobody is being steered to still has to look like a button, not
   like a disabled one. A faint fill separates it from the panel behind it. */
.upsell-buttons .plan:not(.best){ background:rgba(0,0,0,.18) }
.upsell-buttons .plan:not(.best):hover{ background:rgba(201,162,39,.12) }
/* ⚠️ THE CHEAPER-PER-MONTH ONE IS THE LIT ONE, and it is lit because of
   arithmetic rather than because we would prefer the sale. The monthly plan
   stays fully available beside it — a plan drawn as a mistake is a dark
   pattern, and this one is a perfectly reasonable thing to buy. */
.upsell-buttons .plan.best{
  background:var(--brass); color:var(--wood); border-color:var(--brass);
  padding-top:19px;
}
.upsell-buttons .plan.best:hover{ background:var(--brass); filter:brightness(1.08) }
.upsell-buttons .plan.best .plan-note{ opacity:.8 }
.plan-tag{
  position:absolute; top:5px; left:50%; transform:translateX(-50%);
  font-size:.58rem; letter-spacing:.14em; text-transform:uppercase;
  font-family:'JetBrains Mono',monospace; opacity:.72; white-space:nowrap;
}
.plan:not(.best) .plan-tag{ display:none }
/* A button that manages rather than buys is one line of verb — no price, no
   arithmetic, and the tag has nothing to be best at. */
.upsell-buttons .plan.verb{ padding:13px 12px }
.upsell-buttons .plan.verb .plan-tag{ display:none }
.upsell-buttons .plan.verb .plan-price{ font-size:.8rem; letter-spacing:.09em; text-transform:uppercase }
/* When the yearly plan is hidden — payments off, or already subscribed — the
   monthly one takes the whole row instead of leaving a gap where a button was. */
.upsell-buttons #upgradeYear.hidden{ display:none }
.upsell-buttons:has(#upgradeYear.hidden){ grid-template-columns:1fr }
@media (max-width:420px){ .upsell-buttons{ grid-template-columns:1fr } }


/* ── progress panel ──────────────────────────────────────────────────────── */
/* Two bars per week, engine behind and you in front. No axis and no grid: the
   question is "is the gap closing", which is a shape, not a reading. */
/* BUG-129 — fixed-height rows for the numbers this panel shows, and tabular
   figures so a count changing from one digit to two (a revisit, a
   pull-to-refresh) resizes nothing beside it. */
.prog-head { display:flex; align-items:baseline; gap:10px; margin:14px 0 6px; flex-wrap:wrap; min-height:34px; }
.prog-big  { font-size:28px; font-weight:600; font-family:'JetBrains Mono',monospace; font-variant-numeric:tabular-nums; }
.prog-big.up   { color:#7FB069; }
.prog-big.down { color:var(--blood-2,#c0392b); }
.down { color:var(--blood-2,#c0392b); }

/* AF-3b — the result screen's rating swing. Same colour the trend line above
   uses for "moved the right way" ('.up' has no bare rule the way '.down'
   does — the trend panel only ever needed it scoped to '.prog-big' — so it
   is scoped here the same way, rather than adding a global '.up' this file
   has never had). */
.rating-line .up   { color:#7FB069; }
.rating-line .down { color:var(--blood-2,#c0392b); }
.prog-weeks { display:flex; align-items:flex-end; gap:6px; height:64px; margin:14px 0 6px; }
.prog-week  { position:relative; flex:1 1 0; min-width:10px; height:60px; display:flex; align-items:flex-end; }
.prog-week .bar-e, .prog-week .bar-y { position:absolute; bottom:0; border-radius:2px 2px 0 0; }
.prog-week .bar-e { left:0; right:0;   background:rgba(255,255,255,.16); }
.prog-week .bar-y { left:22%; right:22%; background:#7FB069; }
.key-y, .key-e { display:inline-block; width:9px; height:9px; border-radius:2px; vertical-align:middle; }
.key-y { background:#7FB069; } .key-e { background:rgba(255,255,255,.16); }
.prog-spots { margin:8px 0 0; padding-left:18px; line-height:1.6; font-size:13px; }
.prog-spots li { margin-bottom:4px; }


/* Languages: two columns on anything but the narrowest phone. Ten single-file
   rows would push the consent box below the fold, which is where consent goes
   to be ignored. */
.lang-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px 14px; margin-top:8px; }
.lang-grid .check { align-items:center; }
.lang-grid input:disabled + span { opacity:.65; }
@media (max-width:380px) { .lang-grid { grid-template-columns:1fr; } }


/* ── the start screen after registration was surfaced ────────────────────── */
/* Email and consent sit above the button now, so they need to read as one
   small block rather than as two more rows in a long form. */
.signup { margin-bottom: 16px; }
.signup .check.consent { margin-top: 10px; }
.signup #accountMsg:empty { display: none; }
.signup #accountMsg { margin: 8px 0 0; color: var(--brass); }
/* A fold inside a fold needs to look subordinate, or it reads as a sibling. */
.fold.inner { margin-top: 12px; border-left: 2px solid rgba(255,255,255,.07); }
.fold.inner > summary { font-size: .92em; opacity: .9; }


/* ── the in-game hint ────────────────────────────────────────────────────── */
/* Above the transcript, not inside it: a coaching line must never be mistaken
   for something a character said. Different ground, different colour, an icon. */
.hint-bar{
  display:flex; gap:9px; align-items:flex-start; margin:0 0 10px;
  padding:10px 13px; border-radius:3px; line-height:1.5; font-size:.93rem;
  background:rgba(201,162,39,.10); border:1px solid rgba(201,162,39,.30);
  color:#E4D9C4;
}
.hint-i{flex:0 0 auto; opacity:.85}
@media (max-width:560px){ .hint-bar{font-size:.88rem; padding:9px 11px} }


/* ── below the fold ──────────────────────────────────────────────────────── */
/* Everything a player does not need in order to press the button once. */
/* ⚠️ A COLUMN, SO ONE CHILD CAN MOVE. The offer is first in the markup and
   first on the screen — somebody who cannot see their coach report yet is
   exactly who it is for. A subscriber gets the same element last, because for
   them it is a renewal date and a cancel link, and selling a person what they
   already own is how a calm renewal becomes a cancellation. */
/* ⚠️ круг 8, theme-consistency pass (2026-09-14) — border TOKEN CHANGED,
   var(--edge) → var(--edge-2). `.more` is the divider above the fold stack
   on both #settings and #progress; every OTHER divider of the same kind
   (`.fold-body`'s own top border, `.ch-key`, `.ch` card, the three dividers
   at "the badge a game just earned" and the app-shell ones above) already
   uses `--edge-2`, the darker of the two — `.more` was the one holdout,
   so Settings and Progress drew a visibly lighter line than every fold they
   contain. --edge stays the border for a PANEL (`.fold`, `.badge-cell`,
   `.ghost`); --edge-2 is the border for a DIVIDER inside one page. */
.more{margin-top:26px; padding-top:22px; border-top:1px solid var(--edge-2)}
/* ⛔ SCOPED TO THE PROGRESS SCREEN. `.more` is also the settings stack — four
   <details> panels that were laid out as blocks and would have silently become
   flex children. Two screens share the class; only one of them needs to be able
   to move a child. */
#progress .more{ display:flex; flex-direction:column }
#progress .more > .offer{ order:-1 }
#progress .more > .offer.mine{ order:9 }
/* ── the toast ───────────────────────────────────────────────────────────── */
/* There is no Save button any more. A change is written the moment it is made,
   and this is how the game says so — brass for done, blood for refused. */
.toast{
  position:fixed; left:50%; bottom:26px; z-index:60;
  transform:translate(-50%,14px); opacity:0; pointer-events:none;
  max-width:min(430px, calc(100vw - 32px));
  padding:12px 18px; border-radius:3px; font-size:.9rem; line-height:1.45;
  background:linear-gradient(180deg,#2A1F16,#1B140F);
  border:1px solid var(--brass-2); color:var(--bone);
  box-shadow:0 18px 40px -18px rgba(0,0,0,.95), inset 0 1px 0 rgba(201,162,39,.18);
  transition:opacity .22s ease, transform .22s ease;
}
.toast.on{opacity:1; transform:translate(-50%,0)}
.toast.bad{border-color:var(--blood); background:linear-gradient(180deg,#33161A,#1B0F10)}
.toast .t-i{color:var(--brass); margin-right:9px}
.toast.bad .t-i{color:#E0656C}
@media (prefers-reduced-motion:reduce){ .toast{transition:none} }

/* ── the scroll hint — REMOVED 2026-09-16 (r8w bug6) ───────────────────────
   Built for pages-review finding #1: `.screen` scrolled on its own but the
   PAGE never did (`body.app{overflow:hidden}`), so a short window never
   learned there was more below. The bug6 footer fix removed the cause
   instead — `body.app` now scrolls like `body.doc`, so the browser's own
   scrollbar (and every screen reader's and keyboard user's native "there is
   more" cue) does the job this pill existed to fake. See ScrollHint.jsx,
   removed the same day. */

/* ── the offline banner ──────────────────────────────────────────────────
   RESILIENCE T-1 #6,7,8,11 (2026-09-13 research). Unlike the toast above it
   does not time out on its own — a request that cannot reach the server is
   not a four-second fact — and it carries a button, so TOP rather than
   BOTTOM: they would otherwise sit on top of each other, and this one must
   never be mistaken for a toast that is about to go away by itself. */
.netdown{
  position:fixed; left:50%; top:14px; transform:translateX(-50%); z-index:70;
  max-width:min(430px, calc(100vw - 32px));
  padding:10px 16px; border-radius:3px; font-size:.9rem; line-height:1.4;
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  background:linear-gradient(180deg,#33161A,#1B0F10);
  border:1px solid var(--blood); color:var(--bone);
  box-shadow:0 18px 40px -18px rgba(0,0,0,.95);
}
.netdown button{
  background:transparent; border:1px solid var(--brass-2); color:var(--bone);
  border-radius:3px; padding:4px 10px; font-size:.85rem; line-height:1.3; cursor:pointer;
}
.netdown button:hover{border-color:var(--brass)}
/* Rule 21 — the "is it just me?" link, next to the retry button, not fighting
   it for attention: text, not a bordered control. */
.netdown a{color:var(--bone); text-decoration:underline; font-size:.85rem}
.netdown a:hover{color:var(--brass)}

/* A label and its live read-out on one line, above the control they describe. */
.label-row{display:flex; align-items:baseline; gap:10px; margin:15px 0 8px}
.label-row .badge{margin-left:auto}


/* ═══ FIXES 2026-08-31 ═══════════════════════════════════════════════════ */

/* ── 3 · the page must not twitch on load ────────────────────────────────
   The HTML ships with NORMAL and OFF lit, and app.js then moves the highlight
   to whatever was saved and rewrites the sentence under it. The player sees the
   red block jump one segment and the whole panel shift by a line. That is the
   "дрыкается".

   The page is held for one frame while the saved settings are applied, then
   faded in. ⚠️ The keyframe is the SAFETY NET: if app.js never runs — a syntax
   error, a blocked module, a browser with modules off — the page still appears
   on its own after 1.2s. A reveal that depends on script is a blank page. */
body.app{ opacity:0; animation:appReveal 0s linear 1.2s forwards; }
body.app.ready{ opacity:1; animation:none; transition:opacity .14s ease; }
@keyframes appReveal{ to{ opacity:1 } }
@media (prefers-reduced-motion:reduce){ body.app.ready{transition:none} }

/* ── 3 · 27 · and it must not shift when a segment is pressed ────────────
   The sentence under Difficulty and Coaching is one line for some choices and
   two for others, so switching OFF → NUDGE moved everything below it, the
   button included. Two lines are reserved, always. */
/* ⚠️ 2.6em was a guess and it was 4px short: the hint is .86rem at line-height
   1.45, so one line is 19.95px and two are 39.9 — and 2.6em of 13.76px is 35.8.
   The button still moved by 4px between OFF and NUDGE. Measured, in px. */
.sub-hint.fixed-2{ min-height:40px; }
@media (max-width:560px){ .sub-hint.fixed-2{ min-height:60px; } }
@media (max-width:400px){ .sub-hint.fixed-2{ min-height:80px; } }

/* ── 5 · the tick on a chosen face was inside the ring ───────────────────
   `overflow:hidden` on a round button clipped it against the portrait, where it
   read as a smudge on the picture. It sits on the edge now, on its own ground,
   outside the clip. */
.face-grid button{ overflow:visible; }
.face-grid button img{ border-radius:50%; }
.face-grid button.on::after{
  content:'✓'; position:absolute; right:-3px; bottom:-3px;
  width:19px; height:19px; border-radius:50%;
  display:grid; place-items:center;
  font-size:11px; font-weight:700; line-height:1;
  color:#1B140F; background:linear-gradient(180deg,#E3C566,var(--brass));
  border:2px solid var(--wood); box-shadow:0 1px 4px rgba(0,0,0,.6);
}

/* ── 7 · the country list, without the operating system's blue ───────────
   `option:checked` alone is drawn by the OS on macOS. A background-image
   gradient is honoured where a flat background-color is not — it is the one
   lever a page has over a native popup in Chrome, Edge and Firefox.
   Safari draws its own popup and no web API changes that. */
select option{ background:#1B140F; color:var(--bone); }
select option:checked,
select option:hover,
select option:focus{
  background:linear-gradient(#3A2C1E,#3A2C1E) !important;
  background-color:#3A2C1E !important;
  color:var(--brass) !important;
}

/* ── 10 · which coaching level is running, at the table ──────────────────*/
/* ⚠️ THE TWO CHIPS IN THE GAME HEADER MUST BE THE SAME HEIGHT, and after the
   interface was translated they were not: 28px against 36px. Nothing about the
   CSS changed — the WORDS did. "TEACH ME" is eight characters and «НАВЧАЙ
   МЕНЕ» is eleven, so at the header's width the longer one wrapped to two lines
   and grew, and the two chips beside each other in the title bar stopped
   lining up. This is the whole shape of what translation does to a layout: it
   does not break rules, it changes the size of every string that was measured
   in English.
   Both are fixed-height rows now, centred, with the text kept on one line. A
   label that will not fit is truncated rather than allowed to reflow the
   header — the badge says which mode is on, and losing a letter of it costs
   nothing next to a header that jumps. */
.role-chip, .coach-chip{
  display:inline-flex; align-items:center; height:28px; box-sizing:border-box;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:15ch;
  line-height:1;
}
.coach-chip{
  font-family:'JetBrains Mono',monospace; font-size:.66rem; letter-spacing:.08em;
  text-transform:uppercase; padding:0 9px; border-radius:0;
  /* a status like the role chip beside it — no border, no fill (16.09) */
  border:0; color:var(--brass); background:none;
}

/* ⚠️ THE LESSON BADGE IS THE SAME OBJECT AS THE COACHING CHIP, on purpose. Two
   badges in one header drawn two ways read as two unrelated systems; drawn the
   same they read as "here is what is switched on". It sits at the LEFT end,
   before the day counter, because it is true of the whole game rather than of
   this round — and it is quieter than the coaching chip, which is a thing the
   player chose, while this one is just where they came from. */
.learn-chip{
  display:inline-flex; align-items:center; height:28px; box-sizing:border-box;
  /* BUG-34: no max-width above a phone — "РЕЖИМ ОБУЧЕНИЯ" in mono caps with
     letter-spacing is wider than the 16ch that fit the English words, and the
     chip clipped its own label. It sizes to its text now; the phone keeps its
     own 11ch cap below. */
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; line-height:1;
  font-family:'JetBrains Mono',monospace; font-size:.66rem; letter-spacing:.08em;
  text-transform:uppercase; padding:0 9px; border-radius:3px; margin-right:9px;
  border:1px solid var(--edge); color:var(--brass); background:transparent;
}
/* ⛔ AN AUTHOR `display` BEATS THE BROWSER'S OWN `[hidden]{display:none}`, and
   this rule sets one. Without the line below the chip was painted in EVERY
   game — `hidden={!lesson}` was doing its job in React and the stylesheet was
   quietly overruling it, so a table opened from the front door would have worn
   "Learning mode" for ever. The same trap is documented on `form[hidden]` in
   clubs.html; it is the second time this stylesheet has fallen into it. */
.learn-chip[hidden]{ display:none }
@media (max-width:420px){ .learn-chip{ max-width:11ch; padding:0 6px } }

/* ⚠️ THE WAY BACK INTO THE COURSE IS A LINK, NOT A BUTTON, because it leaves
   the single-page app for a server-rendered page. `.primary` and `.ghost` were
   written for <button>, which is already a block with no underline; an anchor
   needs both said out loud or it renders as underlined text at the wrong
   height, in the row above Deal again. */
a.primary, a.ghost{ display:inline-flex; align-items:center; justify-content:center;
                    text-decoration:none; box-sizing:border-box; text-align:center }
a.primary{ width:auto; flex:1 }
.over-lesson{ margin:0 0 18px; padding:14px 0 0; border-top:1px solid var(--edge) }
.over-lesson .over-actions{ margin-bottom:0 }

/* ── 13 · the microphone explains itself ────────────────────────────────*/
.mic-tip{ margin:6px 0 0; }
.heard{ transition:color .14s; }

/* ── 14 · the paywall, in the game's own voice ──────────────────────────
   `alert('Payments are not switched on yet.')` is the browser's dialog, in the
   browser's font, on a grey sheet, with an OK button — the single most
   out-of-place thing that can appear on a page like this. */
.sheet{
  position:fixed; inset:0; z-index:80; display:grid; place-items:center;
  padding:22px; background:rgba(12,8,6,.78); backdrop-filter:blur(3px);
}
.sheet[hidden]{ display:none !important; }
.sheet-card{
  width:100%; max-width:430px; padding:28px 30px 24px; border-radius:5px;
  background:linear-gradient(180deg,#2A1F16,#1B140F);
  /* BUG-23: a dialog's frame is a hairline of brass at 30 %, not the full
     metal — the eye should go to the title and the red button, not the edge. */
  border:1px solid rgba(201,162,39,.30);
  box-shadow:0 40px 90px -30px rgba(0,0,0,.95), inset 0 1px 0 rgba(201,162,39,.16);
  text-align:center;
}
.sheet-card h3{ font-size:1.25rem; color:var(--brass); margin-bottom:10px; }
.sheet-card p{ color:var(--bone-2); font-size:.93rem; line-height:1.6; margin:0 0 8px; }
/* ⛔ BR round 8 — REPORTED: in "Report a problem" (and every other dialog
   built on this same markup — Cancel subscription and Delete my data in
   Overlays.jsx's `.sheet`, both driven by ui.js's `sheet()`, use the IDENTICAL
   `.sheet-card .sheet-actions` shape), CANCEL sat under a wide red SEND at a
   fraction of its width and height: `.primary` claims `width:100%` inside a
   `display:flex` row, `.ghost.small` does not, so the row wrapped to two lines
   and the second button shrank to its own content. One dialog markup, so one
   fix here reaches every caller of `sheet()` and report-problem.js's own node
   — nobody has to remember to repeat it in a new dialog.
   ⚠️ STACKED, GAPPED BY THE SAME TOKEN THE ACTION BAR USES. Column, not row —
   `--gap-actions` is "the distance between one loud action and one quiet one,
   wherever that pair appears," not a number specific to the lesson screen. */
.sheet-card .sheet-actions{ display:flex; flex-direction:column; gap:var(--gap-actions); justify-content:center; margin-top:20px; }
.sheet-card .sheet-actions > button,
.sheet-card .sheet-actions > a{ width:100%; }
/* Same height, padding and type as `.primary` — only the fill and colour stay
   `.ghost`'s own, which is what keeps it the quiet button (RULE 16). */
.sheet-card .sheet-actions .ghost{
  padding:15px; font-size:.93rem; font-weight:600; letter-spacing:.15em;
  background:transparent;
}
@media (prefers-reduced-motion:no-preference){
  .sheet-card{ animation:sheetIn .18s ease-out both }
  @keyframes sheetIn{ from{opacity:0; transform:translateY(8px) scale(.98)} to{opacity:1} }
}

/* ── 15 · the copyright line sits apart from the links ──────────────────*/
.site-foot .foot-c{ opacity:.72; }

/* ── 16 · one red button per screen, and it is the one you came for ─────
   Red is the call to action and nothing else: Take a seat, Deal again, See the
   full report. Everything else is a ghost button on the room's own ground.
   Two reds on one screen is two primary actions, which is none. */
.act-row .btn.on{ background:linear-gradient(180deg,var(--blood),var(--blood-2)); color:#fff; border-color:var(--blood-2); }

/* ── 21 · the action row on a phone ─────────────────────────────────────
   The buttons wrapped into a ragged two-and-a-half rows and the microphone
   ended up alone under them. Below 560px they are a proper grid with a
   44px minimum target — the size a thumb actually hits. */
@media (max-width:560px){
  .act-row{ display:grid; grid-template-columns:1fr 1fr; gap:8px; min-height:0; }
  .act-row .btn{ min-height:46px; padding:10px 8px; font-size:.78rem; }
  .act-row .spacer{ display:none; }
  .act-row .mic{ grid-column:1 / -1; min-height:48px; }
  .act{ padding-bottom:calc(12px + env(safe-area-inset-bottom)); }
  .site-foot{ gap:10px 12px; font-size:.68rem; padding:8px 14px 10px; }
  .sheet-card{ padding:22px 20px 20px; }
}
/* ⚠️ This was max-width:400px, which caught a 390px iPhone and stacked three
   buttons into three rows. A 390px screen fits two columns comfortably; one
   column is for the genuinely tiny, and there is almost nothing below 340. */
@media (max-width:340px){
  .act-row{ grid-template-columns:1fr; }
}

/* ── 22 · accessibility ─────────────────────────────────────────────────
   Every interactive thing gets a visible focus ring on the keyboard, and a
   44px target where a finger is expected. The seat list is a listbox, so its
   focus ring must not be clipped by the scrolling column. */
.seats{ scroll-padding:8px; }
.seat:focus-visible,.face-grid button:focus-visible,.seg button:focus-visible,
.engine button:focus-visible,.fold summary:focus-visible{
  outline:2px solid var(--brass); outline-offset:2px; border-radius:3px;
}
/* A skip link, for someone arriving on the keyboard. */
.skip{
  position:absolute; left:-9999px; top:0; z-index:100;
  padding:10px 16px; background:var(--wood-2); color:var(--brass);
  border:1px solid var(--brass); border-radius:0 0 3px 0;
}
.skip:focus{ left:0; }


/* ── 4 · the download bar says what it is doing ─────────────────────────*/
.dl-row{ display:flex; align-items:center; gap:10px; margin-top:12px; }
.dl-row .dl-label{ font-size:.78rem; color:var(--bone-3); white-space:nowrap; }
.dl-row .dl{ flex:1 1 auto; margin-top:0; }
.dl-row .dl-pct{
  font-family:'JetBrains Mono',monospace; font-size:.74rem; color:var(--brass);
  min-width:3.4em; text-align:right; font-variant-numeric:tabular-nums;
}

/* ── 11 · the two figures ───────────────────────────────────────────────
   A menu flanked by portraits is a poster composition, and a poster puts the
   eye-line in the upper third — level with the title, not with the middle of a
   form. They sat at top:50% of the WINDOW while the panel starts at the top and
   is taller than it, so on a tall screen their heads landed beside the email
   field. Raised, widened a little, and pushed further out so nothing crowds the
   controls. Still symmetric, still facing inward, still gone below 1180px. */
.hero{ top:44%; width:clamp(250px, 27vw, 430px); }
/* BUG-47 (Женя, 16.09): 585 → 645, a step further from the panel. */
.hero-l{ left:max(8px, calc(50% - 645px)) }
.hero-r{ right:max(8px, calc(50% - 645px)) }
@media (max-height:760px){ .hero{ top:47%; width:clamp(220px, 22vw, 340px); } }


/* ── 22 · contrast, and the targets a finger can hit ────────────────────── */

/* The tagline's red was #B4262C at 14.7px bold — 3.25:1, and bold only earns
   the relaxed 3.0 threshold from 18.66px. Red stays the accent; the TEXT red is
   lighter. The buttons keep the deep red, because white on it is 5.9:1. */
.tagline b{ color:#E0656C; }

/* A 23px checkbox row and a 20px footer link are below WCAG 2.2's 24px minimum
   target (2.5.8). On a phone they get the comfortable 44px instead. */
.check{ min-height:24px; }
.site-foot a{ display:inline-block; padding:2px 0; }
@media (max-width:560px){
  .check{ min-height:44px; }
  .site-foot{ gap:6px 14px; }
  .site-foot a{ padding:12px 4px; }
  .legal-nav-in a{ padding:12px 2px; display:inline-block; }
}

/* The legal page's own footer, so the document has a contentinfo landmark like
   every other page — a screen reader user should not lose the way out. */
/* The document page uses the SAME footer as the game — `.site-foot`. It only
   needs the width of the reading column and a little air under a long page. */
/* ⚠️ REMOVED, AND THAT IS THE FIX. This gave every page except the front door
   a DIFFERENT footer — 680px wide instead of 900, 38px of margin above instead
   of 26, and 40px of padding below instead of 11. The markup was byte-identical
   on all eight pages (footer-test.mjs holds that) and it still rendered two
   different ways, because "the same component" is about what the browser
   computes, not about what the file says.
   One rule owns the footer now. A page that genuinely needs a narrower one can
   ask for it, out loud, with a reason — nothing here needed it. */
/* (was: body.doc .site-foot{ max-width:680px; margin-top:38px; padding-bottom:40px }) */

/* Inline links in the terms are exempt from the 24px target rule, but a comfortable
   line is still worth having on a phone — the text is read once, carefully, by
   someone deciding whether to trust us with a card. */
@media (max-width:560px){ .legal p, .legal li{ line-height:1.85; } }

/* A paragraph whose whole content is one link is a button in a link's clothes:
   it needs a real target, on every screen, not only on a phone. */
/* BUG-77 (17.09): `.write` is excluded — `:only-child` counts as a class, so
   this rule (0,2,2) out-ranked `.legal .write` (0,2,0) in legal.html and the
   owner's 16.09 outlined button rendered inline-block, left-aligned, on all
   five languages. The button carries its own 16px padding, so the target
   rule below has nothing to add to it. */
/* BUG-81 (owner, 17.09): `:only-child` cannot see the TEXT around a link, so
   "write to <a>the contact form</a> and ask" got the button treatment too
   (inline-block, padding) and looked unlike every other link. Vertical padding
   on an INLINE box widens the tap target (the box grows to ≥24px) without
   moving a single glyph. */
.legal p > a:only-child:not(.write){ padding:4px 0; }


/* ═══ FIXES round 4 ══════════════════════════════════════════════════════ */

/* ── 18 · the two chips at the table were different sizes ────────────────
   CIVILIAN is .role-chip and TEACH ME was .coach-chip, written separately and
   drifting apart. One size, one shape, one baseline. */
.role-chip,.coach-chip{
  font-family:'JetBrains Mono',monospace; font-size:.7rem; letter-spacing:.09em;
  text-transform:uppercase; padding:6px 6px; border-radius:0;
  line-height:1; display:inline-flex; align-items:center; min-height:28px;
}

/* ── 19 · a button pressed against the paragraph above it ────────────────
   "SEE WHAT IT COSTS прямо под текстом" — no air, so it read as part of the
   sentence rather than as the thing to press. */
.fold-body .act-row{ margin-top:16px; }

/* ── 29 · the table is read at a glance, so it has to be legible ─────────
   Names at .97rem, statuses at .62rem and the day header at .68 were sized for
   a dense dashboard, not for a game you look up at between lines. Everything
   on the board goes up one step. The design does not change — only the size,
   the weight and the room between them. */
.seat .nm{ font-size:1.05rem; font-weight:500; }
.seat .pill{ font-size:.72rem; letter-spacing:.09em; }
.seats-head .label{ font-size:.78rem; }
.bar .day{ font-size:1.15rem; }
.bar-l{ font-size:.92rem; }
.transcript .line .who{ font-size:.8rem; letter-spacing:.12em; }
.transcript .line .txt,.transcript .line p{ font-size:1.02rem; line-height:1.62; }
.act-head .label{ font-size:.82rem; }
.act-head .sub{ font-size:.86rem; }
.act-row .btn{ font-size:.84rem; }
.heard{ font-size:.95rem; }
.hint-bar{ font-size:.98rem; }
@media (max-width:560px){
  .seat .nm{ font-size:1rem; }
  .transcript .line .txt,.transcript .line p{ font-size:1rem; }
  .hint-bar{ font-size:.94rem; }
}


/* ═══ FIXES round 6 ══════════════════════════════════════════════════════ */

/* ── 19 · the save note sits at the far end of the row it belongs to ─────*/
.saved-note{ margin:0 0 0 auto; text-align:right; }

/* ── 17 · the scrollbars ─────────────────────────────────────────────────
   Two bright grey system bars down the middle of a sepia room — the same
   mistake as the white checkbox and the blue dropdown, and the most visible
   one because they sit against the darkest part of the page. Drawn in brass,
   thin, and only while there is something to scroll. Firefox gets the two
   properties it understands; WebKit and Chromium get the full treatment. */
.seats, .transcript, .fold-body, .legal, .screen{
  scrollbar-width: thin;
  scrollbar-color: var(--edge) transparent;
}
.seats::-webkit-scrollbar,
.transcript::-webkit-scrollbar,
.screen::-webkit-scrollbar,
.fold-body::-webkit-scrollbar{ width:8px; height:8px; }
.seats::-webkit-scrollbar-track,
.transcript::-webkit-scrollbar-track,
.screen::-webkit-scrollbar-track,
.fold-body::-webkit-scrollbar-track{ background:transparent; }
.seats::-webkit-scrollbar-thumb,
.transcript::-webkit-scrollbar-thumb,
.screen::-webkit-scrollbar-thumb,
.fold-body::-webkit-scrollbar-thumb{
  background:var(--edge); border-radius:4px;
  border:2px solid transparent; background-clip:content-box;
}
.seats::-webkit-scrollbar-thumb:hover,
.transcript::-webkit-scrollbar-thumb:hover,
.screen::-webkit-scrollbar-thumb:hover{ background:var(--brass-2); background-clip:content-box; }
.seats::-webkit-scrollbar-corner,
.transcript::-webkit-scrollbar-corner{ background:transparent; }


/* ═══ FIXES round 7 ══════════════════════════════════════════════════════ */

/* ── 2 · the move, beside the name that made it ──────────────────────────*/
.line .names{
  /* BUG-68 (owner, 17.09): the name of the one who nominates and the name of
     the one nominated are ONE size — the arrow and its red colour are the
     only difference; the "→ who" column inherits the speaker line's face,
     size, spacing and case. */
  margin-left:10px; color:var(--blood-text);
  font-family:inherit; font-size:inherit; letter-spacing:inherit; text-transform:inherit;
}
.line.you .names{ color:#E0656C }

/* ── 1 · the reading column is CENTRED, not left-aligned in a wide pane ──
   `.line{max-width:66ch}` is the right measure — 66 characters is what an eye
   reads without losing the line. But left-aligned inside a 1600px pane it
   looks like text breaking in the middle of the screen for no reason, which
   is exactly how it was reported. Centring the column makes the whitespace
   symmetrical, and a symmetrical margin reads as a decision. */
.transcript{ padding-inline: max(34px, calc((100% - 74ch) / 2)); }
@media (max-width:900px){ .transcript{ padding-inline:20px } }

/* Who is signed in, and the way out. Quiet by design — it is a safety valve, not
   a call to action — but it must be findable, because on a shared machine the
   second person cannot start without it. */
/* Below the button and visibly separate from it: a quiet account line, not a
   second call to action. */
.whoami{
  margin:16px 0 0; padding-top:14px; border-top:1px solid var(--edge-2);
  font-size:.84rem; color:var(--bone-3);
  display:flex; gap:12px; align-items:baseline; flex-wrap:wrap; justify-content:center;
}
/* ⚠️ 196x21 — under WCAG 2.5.8's 24x24 minimum, caught by a11y-test the first
   time it ran after this button existed. The inline-link exception does not
   apply: it stands on its own line, it is not a link inside a sentence. */
.linkish{
  background:none; border:0; padding:4px 2px; min-height:24px; font:inherit;
  color:var(--brass); text-decoration:underline; text-underline-offset:3px; cursor:pointer;
}
.linkish:hover{ color:var(--bone); }
/* ⚠️ w16-pre — same WCAG 2.5.8 floor as `.linkish` just above, caught by
   a11y-test on clubs.html: a bare <button> with no class here rendered at
   browser-default metrics (114×18), under the 24px minimum. RULE 15 still
   applies (clubs.js's own comment on this row) — this must never look like
   a second `.btn`/`.ghost`, so no border or background, just enough padding
   and a min-height to be a legal target. */
.not-mine{
  background:none; border:0; padding:4px 2px; min-height:24px; margin-left:10px;
  font:inherit; color:var(--bone-3); text-decoration:underline; text-underline-offset:3px;
  cursor:pointer;
}
.not-mine:hover{ color:var(--bone-2); }

/* ── the country list, drawn by the page instead of by the operating system ──
   The native <select> stays as the value; this is what a person sees. */
.combo{ position:relative; }
.combo-native{ position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; }
.combo-btn{
  width:100%; text-align:left; cursor:pointer;
  font:inherit; color:var(--bone); background:linear-gradient(180deg,#241A13,#1B140F);
  border:1px solid var(--edge); border-radius:4px; padding:11px 34px 11px 12px;
  min-height:44px;
}
.combo-btn:hover{ border-color:var(--brass-2); }
.combo.open .combo-btn{ border-color:var(--brass); }
.combo-btn::after{
  content:''; position:absolute; right:14px; top:50%; width:7px; height:7px;
  border-right:1.5px solid var(--bone-3); border-bottom:1.5px solid var(--bone-3);
  transform:translateY(-70%) rotate(45deg); pointer-events:none;
}
.combo-list{
  position:absolute; z-index:40; left:0; right:0; top:calc(100% + 5px);
  margin:0; padding:5px; list-style:none; max-height:280px; overflow-y:auto;
  background:#1B140F; border:1px solid var(--brass-2); border-radius:4px;
  box-shadow:0 14px 40px rgba(0,0,0,.6);
}
.combo-opt{
  padding:9px 11px; border-radius:3px; cursor:pointer; color:var(--bone-2);
  min-height:24px; display:flex; align-items:center;
}
.combo-opt.active{ background:#3A2C1E; color:var(--bone); }
.combo-opt.on{ color:var(--brass); }
.combo-opt.on::after{ content:'✓'; margin-left:auto; }

/* One week is not a trend, so it is not drawn as one: three numbers, read left
   to right, instead of a chart with a single bar floating in an empty box. */
.prog-now{
  display:flex; gap:26px; flex-wrap:wrap; align-items:flex-end;
  margin:16px 0 4px; padding:14px 0 0; border-top:1px solid var(--edge-2);
}
.prog-now > div{ display:flex; flex-direction:column; gap:3px; min-height:2.4rem; justify-content:flex-end; }
/* BUG-129 — tabular-nums: "you 8%" growing to "you 100%" must not nudge the
   two dim columns beside it sideways. */
.prog-n{ font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:1.6rem; line-height:1; color:var(--bone); font-variant-numeric:tabular-nums; }
.prog-n.dim{ color:var(--bone-3); }
.prog-now .label{ font-size:.66rem; letter-spacing:.12em; }

/* ── the turning point ──────────────────────────────────────────────────────
   The first thing in the report and the only thing a player has to read. It is
   deliberately typographic rather than a chart: this is a sentence about one
   move, and a chart would make it look like more data. */
.turning{
  margin:2px 0 18px; padding:14px 16px; border-radius:var(--card);
  background:linear-gradient(180deg,#241A13,#1B140F);
  border:1px solid var(--brass-2);
}
.turning .label{ color:var(--brass); margin:0 0 8px; }
.turn-ev{ margin:0 0 6px; font-size:1.02rem; color:var(--bone); line-height:1.5; }
.turn-you,.turn-cost{ margin:0 0 4px; font-size:.9rem; color:var(--bone-2); line-height:1.5; }
.turn-lesson{
  margin-top:12px; padding-top:11px; border-top:1px solid var(--edge-2);
  display:flex; gap:11px; align-items:baseline; flex-wrap:wrap;
}
.turn-l-lab{
  flex:0 0 auto; font-family:"JetBrains Mono",monospace; font-size:.62rem;
  letter-spacing:.14em; text-transform:uppercase; color:var(--brass);
}
.turn-l-txt{ margin:0; flex:1 1 260px; min-width:0; font-size:.93rem; color:var(--bone); line-height:1.55; }
.turning.locked{ border-style:dashed; }
.turn-cost.blur{ color:var(--bone-3); font-style:italic; }

/* ── who they were, and what that kind of player normally does ───────────── */
.turn-who{
  margin:10px 0; padding:10px 12px; border-left:2px solid var(--brass-2);
  background:rgba(201,162,39,.06); border-radius:0 3px 3px 0;
}
.who-h{ margin:0 0 5px; }
.who-name{ font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.98rem; color:var(--brass); letter-spacing:.02em; }
.who-short{ color:var(--bone-3); font-size:.9rem; }
.who-tell{ margin:0 0 5px; font-size:.9rem; color:var(--bone-2); line-height:1.5; }
.who-stat{ margin:0 0 3px; font-size:.88rem; color:var(--bone); line-height:1.5; }

.cast-list{ display:flex; flex-direction:column; gap:8px; margin:10px 0 6px; }
.cast-p{ padding:9px 12px; border:1px solid var(--edge); border-radius:var(--card); background:var(--wood-2); }
.cast-p.was-black{ border-color:var(--blood-2); }
.cast-top{ margin:0 0 3px; display:flex; gap:10px; align-items:baseline; flex-wrap:wrap; }
.cast-nm{ color:var(--bone); font-size:.98rem; }
.cast-ch{ font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; color:var(--brass); font-size:.86rem; }
.cast-role{
  margin-left:auto; font-family:"JetBrains Mono",monospace; font-size:.6rem;
  letter-spacing:.12em; text-transform:uppercase; color:var(--bone-3);
}
.cast-p.was-black .cast-role{ color:var(--blood-lit); }
.cast-tell{ margin:0; font-size:.85rem; color:var(--bone-3); line-height:1.5; }

/* The daily allowance, said in words at the moment it applies. */
.quota{
  margin:0 0 12px; padding:8px 12px; border-radius:4px;
  font-size:.86rem; line-height:1.5; color:var(--bone-2);
  background:rgba(201,162,39,.08); border-left:2px solid var(--brass-2);
}
.quota.spent{ color:var(--bone); background:rgba(201,162,39,.13); border-left-color:var(--brass); }

/* The half of the progress panel a free player can see the shape of. */
.spots-locked{
  margin-top:8px; padding:12px 14px; border:1px dashed var(--brass-2);
  border-radius:var(--card); background:rgba(201,162,39,.05);
}
.spots-locked p{ margin:0 0 6px; font-size:.92rem; line-height:1.5; color:var(--bone-2); }
.spots-locked p b{ color:var(--bone); }



/* ── the eight kinds of player ───────────────────────────────────────────────
   A gallery, not a document. The portraits are the reason a stranger reads it,
   so they are the largest thing on the page and the type is arranged around
   them — one column of reading beside one face, at a size where the face is a
   person rather than a thumbnail. */
/* 16.09 (owner): "the background colour differs from page to page" — this
   gallery used to paint a FLAT wood behind the portraits while every other
   page carries the lamp gradient set on `body`. One background, everywhere;
   the portraits sit on the same room as the rest of the site. */
.ch-wrap{ max-width:var(--w-page); margin:0 auto; padding:var(--doc-top) 22px 80px; }
/* The sticky bar: the way out on the left, the way around on the right. */
.ch-nav{
  position:sticky; top:0; z-index:5;
  background:rgba(26,19,16,.92); backdrop-filter:blur(8px);
  border-bottom:1px solid var(--edge);
}
@supports not (backdrop-filter: blur(1px)){ .ch-nav{ background:var(--wood); } }
.ch-nav-in{
  max-width:var(--w-page); margin:0 auto; padding:10px 22px;
  display:flex; gap:18px; align-items:center; justify-content:space-between; flex-wrap:wrap;
}
.ch-out{
  flex:0 0 auto; color:var(--brass); text-decoration:none; font-size:.86rem;
  padding:6px 0; min-height:24px; display:inline-flex; align-items:center;
}
.ch-out:hover{ color:var(--brass); }
.ch-jump{ display:flex; gap:2px; flex-wrap:wrap; justify-content:flex-end; min-width:0; }
.ch-jump a{
  color:var(--bone-3); text-decoration:none; font-size:.76rem; letter-spacing:.02em;
  padding:6px 8px; border-radius:3px; min-height:24px; display:inline-flex; align-items:center;
}
.ch-jump a:hover{ color:var(--brass); background:rgba(201,162,39,.08); }
@media (max-width:700px){ .ch-jump{ display:none; } }

.ch-hero{ padding:40px 0 28px; max-width:64ch; }
/* The chapter titles break where the design wants them to, without a <br>
   eating the space between two words. See the note in characters.html. */
.ch-hero h1 .l{ display:block }

/* ── the school's front page, before the school has answered ──────────────
   ⚠️ THE SELL IS IN THE FILE AND MUST NOT BE THE FIRST THING A MEMBER SEES.
   `/learn.html` ships the pitch — the course's own size, and the proof below
   it — because a crawler and a reader with JavaScript off need a page that
   says something. Everyone else is about to be handed their own next lesson
   instead, and painting the pitch first meant a visible swap about half a
   second in: two different pages in one load, reported as exactly that.
   ⛔ THE STYLESHEET ALONE CANNOT HIDE IT. With JavaScript off nothing would
   ever take the rule off and the sell would be invisible to the only readers it
   is for. So `.waiting` is put on by lesson.js — it exists only while the
   script is running — and this delayed reveal is the second line: if that file
   never loads, or throws, the sell appears by itself a third of a second in.
   ⚠️ 320ms, NOT LONGER. Below about a third of a second a blank content area
   reads as the page still arriving; past it, it reads as broken.
   ⚠️ `#doorProof` GETS THE SAME TREATMENT AS `#doorSell`. It is the section
   below the fold carrying the numbers the headline does not, a sibling of
   #doorSell inside the same container, removed by the same `root.innerHTML`
   the moment the wall or the finished screen answers. Reveal it on its own
   timer and it flashes independently of the headline above it.
   🔁 17.09.2026 — `#doorSample` IS GONE, and so is the five-lesson taste it
   named: block 6 of the landing prints the WHOLE programme, folded by level,
   which is the same promise done properly. See tools/build-learn.mjs. */
#doorSell,#doorProof{ animation:door-sell-in 1ms linear 320ms both }
@keyframes door-sell-in{ from{ opacity:0 } to{ opacity:1 } }
/* ⛔ THIS WAS `#lesson.lesson-loading #doorSell`, a descendant selector — and
   the sell is a SIBLING of #lesson now, not a child of it, so the rule stopped
   matching and the school would paint its sell and then swap it for a lesson:
   two pages in one load, which is the thing fixes-test exists to prevent.
   ⚠️ A CLASS ON <body>, NOT `:has()`. Firefox only learned `:has()` in 121 and
   this is the one rule standing between a reader and a visible flash. */
body.school-loading #doorSell,body.school-loading #doorProof{ animation:none; opacity:0 }
/* Nothing moves, so there is nothing to turn off for a reader who asked for
   less motion — but an animation-driven reveal is still an animation, and this
   says out loud that the reveal is a step, not a fade. */
@media (prefers-reduced-motion:reduce){ #doorSell,#doorProof{ animation-duration:1ms } }
/* The proof lives a scroll below the button — a hairline rule says so, rather
   than the reader discovering it by noticing more text kept coming. */
.ch-proof-below{ margin-top:18px; padding-top:22px; border-top:1px solid var(--edge-2); max-width:64ch; }
.ch-proof-below h2,.ch-sample-below h2{
  margin:0 0 12px; font-family:Bitter,Georgia,serif; font-weight:600;
  font-size:1.3rem; line-height:1.2; color:var(--bone);
}
/* ⚠️ KEPT AFTER #doorSample ITSELF WENT (17.09.2026). The rules below cost
   nothing and are what a read-only list of lesson titles should look like —
   no cursor, no hover state, no link — if one is ever wanted again. */
.ch-sample-below{ margin-top:26px; padding-top:22px; border-top:1px solid var(--edge-2); max-width:64ch; }
.ch-sample-item{ margin:0 0 14px; font-size:.96rem; line-height:1.55; color:var(--bone-3); }
.ch-sample-item b{ display:block; margin-bottom:2px; color:var(--bone-2); font-weight:600; }
.ch-eyebrow{
  margin:0 0 12px; font-family:"JetBrains Mono",monospace; font-size:.66rem;
  letter-spacing:.2em; text-transform:uppercase; color:var(--brass);
}
.ch-hero h1{
  margin:0 0 18px; font-family:Bitter,Georgia,serif; font-weight:600;
  font-size:clamp(2rem,5.5vw,3.2rem); line-height:1.05; color:var(--bone);
  letter-spacing:.01em;
}
.ch-dek{ margin:0 0 14px; font-size:1.1rem; line-height:1.6; color:var(--bone-2); }
.ch-dek strong{ color:var(--bone); }
.ch-note{ margin:0; font-size:.92rem; line-height:1.6; color:var(--bone-3); }
/* ⚠️ TWO NOTES IN A ROW NEED A GAP — 2026-09-15. `.ch-note` is margin:0
   because most of them sit inside a card, where the card's own padding does the
   spacing. At the foot of gestures.html two of them are now SIBLINGS (the
   poster, and the pointer to the lessons that replaced the removed chapter
   nav), and zero margins made the two read as one wrapped paragraph — two
   separate offers, printed as though they were one sentence. Scoped to
   siblings that are direct children of a page wrapper, so no note inside a
   card is touched. */
.ch-wrap > .ch-note + .ch-note{ margin-top:13px }
.ch-note b{ color:var(--bone-2); }

/* The three verdicts, shown once at the top so the tags on the cards mean
   something before the first card is read. */
.ch-key{ display:flex; gap:10px; flex-wrap:wrap; padding:0 0 26px; border-bottom:1px solid var(--edge-2); }
.ch-verdict{
  display:inline-block; padding:3px 10px; border-radius:20px;
  font-family:"JetBrains Mono",monospace; font-size:.62rem; letter-spacing:.11em;
  text-transform:uppercase; white-space:nowrap; border:1px solid transparent;
}
.ch-verdict.read{ color:var(--brass); background:rgba(201,162,39,.13); border-color:var(--brass-2); }
/* Four bands, four weights — the tag is the first thing read and it has to
   rank at a glance without colour alone carrying the meaning. */
.ch-verdict.loud{ color:var(--bone); background:rgba(201,162,39,.09); border-color:var(--edge); }
.ch-verdict.soft{ color:var(--bone-2); background:rgba(178,166,146,.07); border-color:var(--edge-2); }
.ch-verdict.flat{ color:var(--blood-lit); background:rgba(180,38,44,.11); border-color:var(--blood-2); }

/* BUG-46 (owner, 16.09): the grid ends the page now (its closing block went
   with BUG-29), so no 44px tail under it and no rule under the last card —
   the page's own bottom padding is the whole distance to the footer. */
.ch-grid{ display:flex; flex-direction:column; gap:0; margin:0; }
.ch-grid > .ch:last-child{ border-bottom:0; }
.ch{
  display:grid; grid-template-columns:1fr; gap:0;
  padding:30px 0; border-bottom:1px solid var(--edge-2); scroll-margin-top:64px;
}
@media (min-width:720px){
  .ch{ grid-template-columns:220px 1fr; gap:30px; align-items:start; }
}
@media (min-width:1000px){
  .ch{ grid-template-columns:260px 1fr; gap:38px; }
}
.ch-face{ margin:0 0 16px; }
@media (min-width:720px){ .ch-face{ margin:0; position:sticky; top:22px; } }
.ch-face img{
  display:block; width:100%; max-width:260px; height:auto; border-radius:50%;
  box-shadow:0 14px 42px -18px #000;
}
.ch-body{ min-width:0; max-width:60ch; }
/* ⚠️ A ROLE CARD CAN LOSE ITS PHOTO AND KEEP ITS COLUMN — second UI/UX pass,
   2026-09-13: don.jpg showed the wrong gesture and was pulled rather than
   shipped wrong (README's own rule — no suitable photo, no photo, text
   only). Without a `.ch-face` sibling, `.ch-body` is the ONLY item in `.ch`'s
   two-column grid, and CSS auto-placement would drop it into the narrow
   220/260px photo column instead of the wide one. `:only-child` catches
   exactly that one case; every other `.ch` article still carries a
   `.ch-face` and is untouched. */
.ch-body:only-child{ grid-column:1 / -1; }
.ch-body .ch-verdict{ margin-bottom:10px; }
.ch h3{
  margin:0 0 3px; font-family:Bitter,Georgia,serif; font-size:1.5rem; line-height:1.15;
  color:var(--bone); letter-spacing:.01em;
}
.ch-short{ margin:0 0 12px; font-size:.9rem; color:var(--bone-3); font-style:italic; }
.ch .tell{ margin:0 0 10px; font-size:1rem; color:var(--bone-2); line-height:1.6; }
.ch .watch{ margin:0 0 14px; font-size:.96rem; color:var(--bone-2); line-height:1.6; }
.ch .watch b{
  color:var(--brass); font-family:"JetBrains Mono",monospace; font-size:.64rem;
  letter-spacing:.13em; text-transform:uppercase; margin-right:6px;
}
/* ⚠️ NOTHING HIGHLIGHTED BY COLOUR, NOTHING BELOW 16px — owner's review,
   2026-09-13: "highlighted text — don't highlight with colour, highlight
   with weight" and "nothing smaller than 15px at 390 / 16px at 1440 in the
   page body." This site's rem never rescales with the viewport (no root
   font-size media query), so one fixed rem value is the same px at both
   widths — 1rem (16px) clears both floors in one move. Scoped to
   `body.gestures` (added to this page's <body> only) so characters.html,
   which shares every one of these classes, is untouched — its own review
   has not happened yet. Left alone on purpose: `.ch-index-links a` — a real
   link, and "colour stays only for links" is the owner's own carve-out.
   `.ch-eyebrow` (the kicker above the h1) got the same treatment as
   everything else here — it was brass and 10.5px too.
   ⚠️ `.ch-next a` DOES NOT get that carve-out — second UI/UX pass, 2026-09-13.
   It was a coloured inline link when this comment was first written; the
   same pass that added the `.ghost` class to it turned it into a bordered
   BUTTON, and a button's own label is not the kind of link the carve-out was
   for. Measured at 14.72px at both 390 and 1440 (this site's rem does not
   rescale with the viewport) — same floor-miss as everything else below. */
body.gestures .ch .watch b{ color:var(--bone-2); font-size:1rem; font-weight:600; }
body.gestures .ch-eyebrow{ color:var(--bone-2); font-size:1rem; font-weight:600; }
body.gestures .ch-short,
body.gestures .ch-note,
body.gestures .g-cite,
body.gestures .g-pair figcaption,
body.gestures .ch .watch,
body.gestures .g-wrap p,
body.gestures .ch-next a,
body.gestures .g-law > p{ font-size:1rem; }
.ch .stat{
  margin:0 0 7px; padding:10px 13px; font-size:.94rem; line-height:1.55;
  color:var(--bone); background:linear-gradient(180deg,#241A13,#1D160F);
  border:1px solid var(--edge); border-left:2px solid var(--brass-2);
  border-radius:0 4px 4px 0;
}
.ch .stat.terse{ color:var(--bone-3); font-size:.88rem; border-left-color:var(--edge); }

/* ⚠️ CENTRED, BECAUSE LEFT-ALIGNED LEFT 453 PIXELS OF NOTHING BESIDE IT. The
   card grid above this section runs the full 1076px of the container; this one
   is a 64-character reading column pinned to the left edge, so on a laptop the
   page ends with a wide band of empty page to the right of the last paragraph
   and the reader assumes something failed to load.
   The 64ch limit itself is right and stays — a line of prose longer than about
   75 characters is measurably harder to read, and this is the only part of the
   page that is prose rather than cards. What was wrong was where the column
   sat, not how wide it was. */
.ch-how{ max-width:64ch; margin-inline:auto; padding-top:8px; }
.ch-how h2{ margin:0 0 14px; font-family:Bitter,Georgia,serif; font-size:1.5rem; color:var(--bone); }
.ch-how p{ margin:0 0 14px; font-size:1rem; line-height:1.65; color:var(--bone-2); }
.ch-how p strong{ color:var(--bone); }
.ch-cta{ margin-top:26px; display:flex; gap:14px; align-items:baseline; flex-wrap:wrap; }
.ch-cta a{
  display:inline-block; padding:6px 0; min-height:24px;
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:1.02rem; letter-spacing:.03em;
  color:var(--brass); text-decoration:none; border-bottom:1px solid var(--brass-2);
}
.ch-cta a:hover{ color:var(--bone); border-color:var(--bone-3); }
.ch-cta-note{ font-size:.86rem; color:var(--bone-3); }
a.cta{
  display:inline-block; padding:13px 26px; border-radius:3px; text-decoration:none;
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; letter-spacing:.09em; text-transform:uppercase; font-size:.88rem;
  color:#fff; background:linear-gradient(180deg,var(--blood),var(--blood-2)); border:1px solid var(--blood-2);
}
a.cta:hover{ filter:brightness(1.12); }

/* The one contextual link out of the lobby — placed where the question it
   answers is already in the player's head, not buried in the footer. */
.learn-link{ margin-top:8px; }
/* ⚠️ 263x19 — under WCAG 2.5.8's 24x24 minimum. It stands on its own line, so
   the inline-link exception does not apply to it. */
.learn-link a{
  display:inline-block; padding:4px 0; min-height:24px;
  color:var(--brass); text-decoration:none; font-size:.88rem;
  border-bottom:1px solid var(--brass-2);
}
.learn-link a:hover{ color:var(--bone); border-color:var(--bone-3); }


/* ── getting back in ───────────────────────────────────────────────────────
   An organiser arrives once through a one-time link and then needs a way back
   in — to their club, or to a fresh link. Both live beside the identity line,
   which is where a returning person looks, and neither is in the way of a new
   player: the sign-in box is closed until asked for. */
.signin-ask{margin:10px 0 0; text-align:center}
.signin{
  margin:12px 0 0; padding:16px 18px;
  border:1px solid var(--edge); border-radius:6px; background:#1A120D;
  display:grid; gap:10px; text-align:left;
}
.signin label{font-size:.76rem; color:var(--bone-3); text-transform:uppercase; letter-spacing:.09em}
.signin input{
  width:100%; background:#160F0B; border:1px solid var(--edge); border-radius:4px;
  color:var(--bone); padding:11px 13px; font:inherit; font-size:.95rem;
}
.signin input:focus{outline:none; border-color:var(--brass-2); box-shadow:0 0 0 3px rgba(190,150,90,.13)}
.signin .sub-hint{margin:0}
#myClub{color:var(--brass)}

/* The public-list switch reports what the SERVER accepted, not what the box
   looks like — a toggle that controls other people's visibility must never
   show a state it failed to save. */
.optsay{ font-size:.86em; margin-left:.5em }
.optsay.bad{ color:#c0392b }

/* The club a player belongs to, named on the start screen. It was stored,
   sent to the server and used for the trial and the commission — and never
   once shown to the person it belonged to. */
.myclub{
  margin:8px 0 0; font-size:.84rem; color:var(--brass);
  letter-spacing:.01em;
}

/* The tag row reads as a legend, not as four buttons that do nothing. */
.ch-key-lead{
  display:block; width:100%; margin:0 0 8px;
  font-size:.84rem; color:var(--bone-3);
}

/* ═══ the fourteen reads — the only number here that goes up ═══════════ */
.reads-lead{margin:0 0 10px; font-size:.9rem; color:var(--bone-2)}
.reads-lead b{color:var(--brass)}
/* BUG-129 — A FLOOR UNDER THE LEAD SENTENCE, same idea as `.badge-name`'s
   `min-height:2.5em` a few screens up: a short sentence and a long one get
   the same box, so the loading skeleton (which carries no text of its own —
   see web/src/screens/Progress.jsx) reserves the exact same space the real
   one lands in, in every language. Sized to the longest real sentence each
   panel prints in English at each width, plus a line of headroom for a
   translation that runs longer — `#readsBody`'s zero-state sentence is one
   line at 1280px / three at 390px; `#badgesBody`'s is three / six. The `em`
   value is the fallback for a browser with no `lh` unit (Firefox < 129,
   Safari < 16.4); the `lh` line after it wins everywhere modern. */
#readsBody .reads-lead{ min-height:3em; min-height:2lh }
#badgesBody .reads-lead{ min-height:6em; min-height:4lh }
@media (max-width:479px){
  #readsBody .reads-lead{ min-height:6em; min-height:4lh }
  #badgesBody .reads-lead{ min-height:10.5em; min-height:7lh }
}
/* The nearest unmade read is the reason to play the next game, so it is the
   one thing in this panel that is not a list item. */
.reads-next{
  margin:0 0 12px; padding:9px 12px; font-size:.86rem; line-height:1.5;
  border-left:2px solid var(--brass); background:var(--brass-dim); color:var(--bone);
}
.reads-next span{
  display:block; font-size:.66rem; letter-spacing:.13em; text-transform:uppercase;
  color:var(--brass); margin-bottom:3px;
}
.reads-list{list-style:none; margin:0; padding:0; display:grid; gap:5px}
.reads-list li{
  display:flex; gap:9px;
  font-size:.86rem; color:var(--bone-3); line-height:1.4;
  /* BUG-129 — one line is the common case, two is the occasional wrap on a
     narrow phone or a longer translation; reserving both means the row a
     reader sees while the skeleton is up (`.skel-bar` below) never resizes
     once the real label lands, wrapped or not. `center`, not `baseline`,
     because a one-line row is now sometimes taller than its text — baseline
     would pin the bullet and the label to the top of that space instead of
     the middle. */
  align-items:center; min-height:2.8em; min-height:2lh;
}
/* ⚠️ --edge IS A BORDER COLOUR, NOT A TEXT COLOUR. Using it for the bullet
   gave 1.35:1 against the card — the a11y audit failed on fourteen nodes at
   once. A marker that says "you have not made this read yet" has to be READABLE
   for that to mean anything; invisible is not the same as understated.
   ⚠️ And no opacity on it either — .75 dropped it straight back to 3.37:1,
   which is a fix that looks like a fix and fails the same audit. */
.reads-list li b{color:var(--bone-3); font-weight:700; flex:0 0 auto}
.reads-list li.got{color:var(--bone)}
.reads-list li.got b{color:var(--brass)}
.reads-list .n{margin-left:auto; font-size:.76rem; color:var(--bone-3); white-space:nowrap}

/* Badges reuse the reads list, with one difference that matters: the trailing
   text is a whole sentence rather than a count, so it must be allowed to wrap
   instead of being shoved onto one nowrap line and clipped on a phone.
   ⚠️ Colour uses --bone-3 like everything else here, never --edge — the same
   1.35:1 trap the note above describes, and a badge nobody can read is worse
   than no badge, because it looks like the feature is broken. */
.badges-list li{flex-wrap:wrap}
.badges-list .n{
  margin-left:0; flex:1 1 100%; white-space:normal;
  padding-left:18px; font-size:.74rem; color:var(--bone-3);
}
.badges-list li.got .n{color:var(--bone-3)}
/* The streak line. Deliberately the same weight as everything else in the
   panel — no flame, no counter animation, no red. See the note in report.js:
   the moment this shouts, it is a chore instead of a trainer. */
.streak-line{
  margin:0 0 8px; font-size:.86rem; color:var(--bone-3); line-height:1.4;
}
.streak-line b{color:var(--brass); font-weight:700}

/* ═══ the first-run strip ══════════════════════════════════════════════════
   Deliberately quiet and deliberately in the flow — it pushes the button down
   rather than sitting on top of it, so nothing is ever unreachable and no
   automated check has to dismiss it before it can click. */
.firstrun{
  margin:14px 0 0; padding:11px 13px;
  border:1px solid var(--edge); border-radius:8px;
  background:rgba(200,164,74,.06);
}
.firstrun p{margin:0 0 9px; font-size:.88rem; line-height:1.45; color:var(--bone)}
.firstrun-row{display:flex; gap:12px; align-items:center; flex-wrap:wrap}
.firstrun .btn{padding:5px 13px; font-size:.82rem}
/* --bone-3, never --edge: an escape hatch nobody can read is not an escape
   hatch. Same 1.35:1 trap the reads list documents. */
.firstrun .linkish{font-size:.8rem; color:var(--bone-3)}

/* The button somebody just chose on the pricing page. Two slow pulses, then it
   stops — an animation that keeps going is a distraction, not a signpost.
   ⚠️ Respects reduced-motion: for some people a pulsing control is not a nudge
   but a reason to close the tab. */
@keyframes pricePulse{0%,100%{box-shadow:0 0 0 0 rgba(200,164,74,0)}50%{box-shadow:0 0 0 6px rgba(200,164,74,.28)}}
.pulse{animation:pricePulse 1.3s ease-in-out 2}
@media (prefers-reduced-motion:reduce){ .pulse{animation:none; outline:2px solid var(--brass); outline-offset:3px} }

/* The club-code row. The field takes the space; the button takes what it needs. */
.joincode-row{display:flex; gap:9px; align-items:stretch}
.joincode-row input{
  flex:1 1 auto; min-width:0; text-transform:uppercase; letter-spacing:.06em;
}
.joincode-row .btn{flex:0 0 auto}

/* ⚠️ VISUALLY HIDDEN, NOT display:none. A label with display:none or
   visibility:hidden is removed from the accessibility tree too, so it labels
   nothing — the field stays unnamed and the audit still fails. This is the
   clip-rect form, which keeps the text available to a screen reader while
   taking no space. A placeholder is not a substitute: it vanishes the moment
   somebody types, and is not announced as the field's name. */
.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}

/* ═══ every vote, named ═══════════════════════════════════════════════
   Chess.com's game review is worth paying for because every move gets a word.
   Colour carries the same meaning as the word, never instead of it — the label
   is always present, so nobody has to see colour to read the table. */
.rnd-verdict{
  font-size:.72rem; letter-spacing:.08em; text-transform:uppercase;
  font-weight:700; white-space:nowrap; text-align:right;
}
.rnd.v-brilliant .rnd-verdict{color:#7FD1A6}
.rnd.v-best      .rnd-verdict{color:var(--brass)}
.rnd.v-fair      .rnd-verdict{color:var(--bone-2)}
.rnd.v-missed    .rnd-verdict{color:#D98A80}
.rnd.v-noread    .rnd-verdict{color:var(--bone-3)}


/* ═══ the front door ═══════════════════════════════════════════════════════
   ⚠️ SIZED FROM A MEASURED FAILURE. The old first screen carried 18 inputs and
   20 buttons, and 71% of arrivals left at its registration form without ever
   playing. What is left is a name, a sentence, a button and one row — so the
   button is allowed to be the biggest thing on the page, which is the only
   layout instruction that actually matters here. */
.big-play{
  display:block; width:100%; max-width:320px; margin:22px auto 14px;
  padding:16px 22px; font-size:1.15rem; letter-spacing:.02em;
}
/* The difficulty row is a quiet control, not a labelled form field: no border
   box, no caption above it. Somebody who does not care never notices it. */
/* ⚠️ ONE MEASURE FOR EVERYTHING UNDER PLAY. The segment was 360px, the hint
   under it ran the full card and the table line ran wider still, so the front
   door stepped outward line by line — a pyramid, and it read as three unrelated
   things rather than one control with two notes about it.
   They share a width now, and the note is TIED to the control it describes:
   4px under the buttons, then real air before the next idea starts. */
.quiet-seg{ max-width:400px; margin:26px auto 0 }
#diffHint{ max-width:400px; margin:8px auto 0; text-align:center }
#tableHint{ max-width:400px; margin:20px auto 0; text-align:center }
.front-note{margin:10px 0 0; text-align:center; font-size:.86rem; color:var(--bone-3); min-height:1.2em}
.front-terms{margin:16px auto 0; max-width:44ch; text-align:center}
.front-links{
  display:flex; flex-wrap:wrap; gap:6px 18px; justify-content:center;
  margin:20px 0 0; font-size:.86rem;
}
.front-links a{color:var(--bone-3)}
.front-links a:hover{color:var(--brass)}

/* ⚠️ TWO WORDS EACH AND THEY LOOKED LIKE THE TERMS. "Your progress" and
   "Settings" sat as plain underlined text under a paragraph of legal small
   print, in the same size and the same grey — three links in a row, two of
   which go to a screen and one to a document. An icon and a word tells a
   reader at a glance which kind of thing each one is, and gives the pair a
   shape the eye can find without reading. */
.icon-link{ display:inline-flex; align-items:center; gap:7px; padding:8px 12px;
            border:1px solid var(--edge); border-radius:3px; color:var(--bone-3);
            text-decoration:none; min-height:36px; transition:.14s }
.icon-link:hover{ border-color:var(--brass-2); color:var(--bone) }
.icon-link svg{ width:15px; height:15px; flex:none; opacity:.85 }

/* Settings and Progress are screens; these give them a page's furniture. */
.screen-back{margin:0 0 6px}
.screen-title{margin:0 0 18px; font-size:1.25rem}

/* ═══ keep it, after the first game ════════════════════════════════════════
   The one place an email is worth asking for: the coach report is on screen,
   the player has just been shown something about their own game, and there is
   finally an answer to "why would I". Never on arrival — see the front door. */
.keepit{
  /* BUG-42 (owner, 16.09): air under the box too — the buttons used to sit
     straight on its border. */
  margin:16px 0 22px; padding:14px 15px;
  border:1px solid var(--brass); border-radius:9px;
  background:rgba(200,164,74,.07);
}
.keepit p{margin:0 0 10px; font-size:.9rem; line-height:1.45; color:var(--bone)}
.keepit-row{display:flex; gap:9px; align-items:stretch; flex-wrap:wrap}
.keepit-row input{flex:1 1 220px; min-width:0}
.keepit-row .btn{flex:0 0 auto}
.keepit .said{margin:9px 0 0; font-size:.82rem}


/* The consent tick on the settings screen. Deliberately ordinary: a checkbox
   people recognise, with the sentence as its label, and nothing that makes it
   look pre-decided. */
.consent-check{display:flex; gap:9px; align-items:flex-start; margin:12px 0 0; font-size:.86rem; line-height:1.45}
.consent-check input{margin-top:2px; flex:0 0 auto}
.consent-check span{color:var(--bone-3)}


/* Deletion sits beside the pause now, and it must not be the eye-catching one:
   the irreversible action should never look like the default. */
.danger-quiet{opacity:.85}
.danger-quiet:hover{opacity:1}


/* ═══ badge icons ══════════════════════════════════════════════════════════
   Cut from one sheet, keyed by flooding the checkerboard from the border —
   a luminance threshold alone would have eaten the cream inside every icon,
   which is the same colour as the background it sits on. */
.badge-icon{
  width:34px; height:34px; flex:0 0 auto; object-fit:contain;
  margin-right:2px;
}
/* ⚠️ DIMMED, NOT HIDDEN. An unearned badge people cannot see is a list of
   things they already have, which is a scoreboard rather than an invitation.
   Greyscale keeps the shape readable while saying plainly it is not yours yet. */
.badge-icon.locked{filter:grayscale(1) opacity(.38)}
.badges-list li{align-items:center}
@media (prefers-reduced-motion:no-preference){
  .badges-list li.got .badge-icon{transition:transform .2s ease}
  .badges-list li.got:hover .badge-icon{transform:scale(1.08)}
}


/* ═══ waiting to be let in ═════════════════════════════════════════════════
   One row per person, with both answers side by side. "Let in" is the ordinary
   button and "Not now" is a plain link — declining somebody should never look
   like the thing the page wants you to do. */
.waiting{margin:16px 0 0; padding:14px 15px; border:1px solid var(--edge); border-radius:9px}
.waiting h3{margin:0 0 10px; font-size:.92rem; display:flex; align-items:center; gap:8px}
.ask-row{display:flex; align-items:center; gap:10px; padding:6px 0; font-size:.9rem}
.ask-row span{flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis}
.ask-row .btn{flex:0 0 auto; padding:4px 12px; font-size:.8rem}
.ask-row .linkish{flex:0 0 auto; font-size:.8rem; color:var(--bone-3)}


/* ═══ the identity bar ═════════════════════════════════════════════════════
   ⚠️ NOTHING ON THE PAGE ANSWERED "AM I SIGNED IN?". Top right is where every
   product puts it and where people already look, so it costs no learning. */
.topbar{
  position:absolute; top:0; right:0; left:0; z-index:5;
  display:flex; justify-content:flex-end; padding:14px 18px;
  pointer-events:none;
}
.topbar a{pointer-events:auto}
.whoami-link{
  font-size:.82rem; color:var(--bone-3); max-width:44vw;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  /* ⚠️ 38×20 FAILED THE TARGET AUDIT, and it was right to. A paragraph whose
     entire content is one link is not a sentence with a link in it — it is a
     button in a link's clothes, and WCAG's inline exception does not cover it.
     Padding to 24px tall costs nothing and is the difference between a control
     a thumb can hit and one it cannot. */
  display:inline-flex; align-items:center; min-height:24px; padding:2px 8px;
  border-radius:6px;
}
.whoami-link:hover{color:var(--brass)}
/* An address is quieter than an invitation: somebody signed in does not need
   their own email shouted at them, they need to be able to find it. */
.whoami-link.signed{opacity:.8; font-family:var(--mono, ui-monospace, monospace); font-size:.76rem}
body.playing .topbar{display:none}   /* the board is a full-height instrument */

/* ═══ the card sits in the middle of the window ════════════════════════════
   ⚠️ IT WAS PINNED TO THE TOP and the page ended in a long stretch of nothing,
   which reads as a page that failed to load the rest of itself. */
body.app > #start{display:flex; align-items:center; justify-content:center}
body.app > #start > .start-inner{width:100%; margin-block:auto}

/* A form column is narrower than a reading column on purpose — an input wider
   than about 30 characters is harder to scan, not easier. */
.start-inner.narrow{max-width:var(--w-card)}
.screen-lede{margin:0 0 18px; line-height:1.5}
#makeAccount{width:100%; margin-top:14px}
#makeAccount[disabled]{opacity:.45; cursor:not-allowed}
#makeHint{margin:8px 0 0; text-align:center}

/* ── the club table ─────────────────────────────────────────────────────────
   Your own club only. Numbers right-aligned so a column of them can be read
   down; your own row lit so you can find yourself without counting. */
.club-table{ width:100%; border-collapse:collapse; margin:10px 0 0; font-size:.93rem }
.club-table th{ text-align:left; font-weight:600; color:var(--bone-3); font-size:.8rem;
  text-transform:uppercase; letter-spacing:.04em; padding:0 8px 6px; border-bottom:1px solid var(--edge) }
.club-table td{ padding:7px 8px; border-bottom:1px solid var(--edge) }
.club-table .n{ text-align:right; font-variant-numeric:tabular-nums }
.club-table tr.you td{ background:var(--lift); font-weight:600 }
.club-table tr:last-child td{ border-bottom:0 }

/* ⚠️ A LINK IS A TARGET, AND A TARGET HAS A SIZE. "← the game" was 86×18 — the
   only way off the pricing page on a phone, and under the 24×24 floor WCAG 2.2
   sets for anything you have to hit with a thumb. Text links inside running
   prose are exempt from that rule and stay as they are; a link sitting alone on
   its own line is not prose, it is a button that forgot to say so. */
.back-link{ text-align:center; margin:28px 0 0 }
.back-link a{ display:inline-block; padding:12px 16px; min-height:24px; border-bottom:0 }
.back-link a:hover{ border-bottom:0 }

/* ── the account card ───────────────────────────────────────────────────────
   ⚠️ IT WAS PINNED TO THE TOP-LEFT CORNER OF THE SCREEN. On a wide display the
   form sat in the corner with two thirds of the page empty beside it, which
   reads as a layout that failed to load rather than a page. Every other screen
   here is a centred card; this one had simply never been given the same
   treatment. Centred both ways, with the same max width as the lobby, so the
   whole site stops looking like three products. */
.screen.hidden > .account-card{ display:none }
/* ⚠️ `align-items:center` CUTS THE TOP OFF WHEN THE CONTENT IS TALLER THAN THE
   BOX. It is the oldest flexbox trap there is: centring an overflowing child
   pushes its start ABOVE the container, and there is no scrollbar to reach it —
   measured at -57px, so the heading and the way back were off the top of the
   screen entirely.
   `margin:auto` on the child does what centring was meant to do: it takes the
   leftover space when there is any and takes none when there is not, so a short
   card is centred and a tall one starts at the top and scrolls. */
/* ⚠️ `flex:1 1 auto` LETS A TALL CHILD GROW THE ROW; only `flex:1 1 0%` makes
   it take the space that is left and no more. With `auto` the account card ran
   134 pixels PAST the top of the footer — so the footer's edge looked like a
   band drawn across the form, and the fields underneath it were unreachable
   and, worse, unguessable: nothing suggested there was more below.
   `min-height:0` is the other half — without it a flex item refuses to shrink
   below its content and overflow:auto never gets anything to scroll. */
#account, #settings, #progress{ flex:1 1 0%; min-height:0 }
#account{ display:flex; align-items:flex-start; justify-content:center;
  padding:24px 16px; box-sizing:border-box; overflow-y:auto }
/* ⚠️ REMOVED: `margin-block:auto` CENTRES BY PUSHING, and inside a scroll
   container it pushes content out of reach. It is why the card still ran 375
   pixels past the footer even after the flex sizing was fixed — the section
   scrolled, and the card was drawn over the footer anyway.
   Top-aligned with padding instead. Centring a card that fits is worth having;
   it is not worth a form whose bottom is behind a band the reader cannot see
   past and cannot guess is hiding anything. */
#account.hidden{ display:none }
.account-card{ width:100%; max-width:var(--w-card); margin:0 auto; text-align:left }

/* ⚠️ THE TICK SAT LOW AND LEFT OF ITS OWN BOX. `align-items` was never set, so
   a two-line label pushed the box to the top of a tall row and the glyph drifted
   out of its frame — small, and the kind of thing that makes a form feel
   untended on exactly the screen where somebody is deciding whether to trust
   this with their address. */
/* ⚠️ THE BOX AND THE TICK ARE TWO PSEUDO-ELEMENTS AND THEY HAVE TO MOVE
   TOGETHER. The real <input> is opacity:0 and absolutely positioned; what a
   person sees is `.check::before` (a flex item — the square) and
   `.check::after` (the tick, positioned against the ROW at top:50%).
   An earlier attempt at this set `align-items:flex-start` and nudged the input.
   Both were wrong: the input is absolute, so flex and margin do nothing to it —
   and moving the row's alignment took the SQUARE to the top of a two-line label
   while the TICK stayed at the middle of the row. The box showed empty and a
   stray tick floated below it, on the one control somebody has to trust before
   agreeing to anything. Exactly the failure the clubs page had already had.
   So: the row is top-aligned, the square is nudged to the first line's cap
   height, and the tick is pinned to the SAME distance from the top and centred
   inside the square — one number, used twice, instead of two systems. */
.check{ display:flex; align-items:flex-start; gap:10px; line-height:1.45; position:relative }
.check::before{ margin-top:2px }
/* ⛔ THE FOUR NUDGES THAT USED TO LIVE HERE ARE GONE. They positioned a
   separately-anchored tick against three different label shapes, and they were
   the reason it was never centred in any of them. The tick is a background of
   the box now — see the note beside `.check::before` — so there is nothing left
   to keep in step. */
.check.consent{ align-items:center }
.check.consent::before{ margin-top:0 }
.consent-block{ margin:14px 0 0; display:flex; flex-direction:column; gap:10px }
/* ⚠️ NEVER TOUCH THE OPACITY OF THE REAL INPUT. It is `opacity:0` on purpose —
   the visible control is `.check::before`, drawn by CSS — and this rule set it
   to .4 for the disabled state, which made the BROWSER'S OWN CHECKBOX show
   through at 40% beside the drawn one. Two squares, one of them half-visible
   and slightly lower, on the control somebody has to trust before agreeing to
   anything. It looked exactly like a rendering bug because it was one.
   A disabled box is not broken — it is waiting for the address above it — so
   the DRAWN box is what dims, and the real input stays invisible always. */
.check:has(input:disabled)::before{ opacity:.45 }
.check input[type=checkbox]:disabled{ cursor:not-allowed }
.check input[type=checkbox]:disabled + span{ opacity:.55 }
#makeAccount{ width:100%; margin:16px 0 0 }
#joinClubFold{ margin:22px 0 0 }

/* ── one way out, in one place, with one name ────────────────────────────────
   ⚠️ THE SITE HAD FOUR DIFFERENT ANSWERS TO "how do I get back". "← Back" at
   the top of the terms and the characters page, "← the game" at the BOTTOM of
   pricing and clubs, and nothing at all on the club and sign-in pages. The
   labels disagreed, the positions disagreed, and two pages were dead ends.
   Top-left, on every standalone page, always the same three words. Top rather
   than bottom because nobody should have to read to the end of the terms to
   find the way out — and the footer already carries a second copy for anybody
   who does reach the bottom. */
.back-row{ margin:0; padding:18px 20px 0; }
.back-to-game{ font-size:.92rem; border-bottom:0 }
.back-to-game:hover{ border-bottom:0; text-decoration:underline; text-underline-offset:3px }
/* A target, not a word: 24px is the WCAG 2.2 floor for anything a thumb hits. */
.back-row .back-to-game{ display:inline-block; padding:8px 4px; min-height:24px }

/* ── the badge wall ─────────────────────────────────────────────────────────
   ⚠️ SEVENTEEN ROWS OF NAME-PLUS-SENTENCE IS A WALL OF TEXT, and nobody reads
   the twelfth line of one. What a player wants from this panel is the SHAPE of
   the collection — how much is filled in, where the gaps are — which is a grid,
   not a list. The sentence moves to hover and to `title`, so a mouse gets it on
   demand, and a screen reader and a touch device get it from the attribute. */
.badges-grid{ list-style:none; margin:12px 0 0; padding:0;
  display:grid; gap:10px; grid-template-columns:repeat(auto-fill, minmax(96px, 1fr)) }
.badge-cell{ position:relative; text-align:center; padding:10px 6px 8px;
  border:1px solid var(--edge); border-radius:10px; background:rgba(255,255,255,.015) }
.badge-cell .badge-icon{ width:56px; height:56px; display:block; margin:0 auto 6px }
/* Unearned is dimmed, never hidden: seeing what is still out there is the whole
   reason to open this panel a second time.
   ⚠️ DIMMED ENOUGH TO READ AS "NOT YET", NOT SO FAR THAT IT READS AS DEAD.
   This was `grayscale(1) opacity(.32)` — full desaturation at a third of the
   ink, on a #1A1310 room. Seventeen of those in a grid is seventeen grey
   smudges, and it was reported as exactly that: the badges do not look alive.
   The point of showing a locked badge is that the player can SEE the thing they
   have not got and want it, and that requires it to be legible.
   Measured against the same art: 0.55 opacity with the grayscale held back to
   0.85 and a touch of brightness leaves the shape and the brass readable while
   still sitting plainly behind an earned one.
   ⚠️ AND THE EARNED ONE NOW LOOKS EARNED. A 5%-alpha background was the only
   difference between got and not-got, which at a glance is no difference at
   all. A lit rim and a brass wash cost nothing and make the wall a record of
   what happened rather than a chart of what did not. */
.badge-cell .badge-icon.locked{ filter:grayscale(.85) brightness(1.1) opacity(.55) }
.badge-cell.got{
  border-color:rgba(201,162,39,.5);
  background:radial-gradient(circle at 50% 30%, rgba(201,162,39,.14), rgba(201,162,39,.04) 70%);
  box-shadow:inset 0 0 0 1px rgba(201,162,39,.12), 0 6px 18px -12px rgba(201,162,39,.55);
}
/* ⚠️ TWO LINES OF ROOM, ALWAYS, EVEN FOR A ONE-LINE NAME. "Level with the
   coach" wraps and "Wore it well" does not, so a row containing both was 110px
   tall while the row above it was 95 — measured. Grid stretches the cells to
   match, so the medals in the taller row sat higher inside their boxes than the
   ones above, and the wall read as though pieces of it had slipped. Reserving
   the second line costs 15px and makes every cell the same shape.
   ⚠️ THE OTHER HALF OF THAT FIX IS IN THE ART, not here: the drawing inside
   each PNG was a different size in each file, from 61px of ink to 96px on the
   same 96px page. `tools/normalise-badges.mjs` made them one size and
   badges-test.mjs holds them there. */
.badge-name{ display:block; font-size:.76rem; line-height:1.25; color:var(--bone-2);
  min-height:2.5em }
.badge-cell.got .badge-name{ color:var(--bone); font-weight:600 }

/* The hint, on demand. Positioned over the grid rather than inside the cell so
   it never changes the layout — a tooltip that reflows the wall it belongs to
   makes every other medal jump while you are reading one. */
.badge-tip{ position:absolute; left:50%; bottom:calc(100% + 6px); transform:translateX(-50%);
  width:min(230px, 70vw); padding:8px 10px; border-radius:8px; z-index:5;
  background:#1A1310; border:1px solid var(--edge); color:var(--bone-2);
  font-size:.78rem; line-height:1.4; text-align:left;
  opacity:0; visibility:hidden; transition:opacity .12s }
.badge-cell:hover .badge-tip, .badge-cell:focus-within .badge-tip{ opacity:1; visibility:visible }
/* ⚠️ NEVER ON A PHONE. There is no hover on a touch screen, so the tooltip
   would either never appear or appear stuck; the `title` attribute is what
   those devices and screen readers use, and it is on every cell. */
@media (hover: none){ .badge-tip{ display:none } }
@media (max-width:420px){ .badges-grid{ grid-template-columns:repeat(auto-fill, minmax(80px, 1fr)) } }

/* ⚠️ A BADGE WITHOUT ART IS STILL A BADGE. Five were added before their sprites
   were cut, and a missing PNG renders as a broken-image glyph — the one thing
   on a page that says "this product is half-built" louder than an empty space.
   The cell falls back to a drawn medallion so the wall stays whole, and the
   name and the hint were always the part that carried the meaning anyway. */
.badge-icon.no-art{ background:radial-gradient(circle at 50% 38%, rgba(201,162,39,.30), rgba(201,162,39,.05) 62%);
  border:1px solid rgba(201,162,39,.35); border-radius:50%; }
.badge-cell:not(.got) .badge-icon.no-art{ background:radial-gradient(circle at 50% 38%, rgba(255,255,255,.07), transparent 62%);
  border-color:var(--edge) }

/* ── loading placeholders (BUG-129, owner, 17.09) ────────────────────────────
   ⚠️ THE EMPTY FOLD-BODY WAS THE BUG. `#readsBody` and `#badgesBody` used to
   render nothing until their own `/api/reads`/`/api/badges` round trip
   answered, so the footer sat 0.02–0.09 CLS lower for that one request and
   then jumped up — measured, and named in progress-shape-test.mjs's own §6
   note. The fix is the SAME markup the real content uses
   (`.badges-grid`/`.badge-cell`/`.reads-list li`/`.reads-lead`), carrying no
   text of its own, so the browser's own grid/flow layout — plus the
   min-height floors on `.badge-name`, `.reads-list li` and `.reads-lead`
   above, which apply to the real content too — gives the placeholder and the
   real content the identical box. Swapping the HTML in report.js then
   changes nothing about the height. Every skeleton node is empty (no text
   nodes) so `#readsBody`/`#badgesBody`'s `.textContent` stays '' until the
   real, translated content lands — the exact signal progress-ru-i18n-test.mjs
   and progress-uk-i18n-test.mjs already wait on. `aria-hidden` keeps a
   screen reader from being told about seventeen nameless cells; `aria-busy`
   on the fold-body itself (set in the JSX, cleared by report.js once real
   content is written) is the one signal a screen reader gets while this is
   up. */
.skel-block, .skel-bar, .skel-line{
  background:rgba(255,255,255,.07); border-radius:6px;
  animation:skel-pulse 1.6s ease-in-out infinite;
}
.badge-name.skel-bar{ width:60%; height:.9em; margin:.2em auto 0 }
.reads-list .skel-bar{ display:block; width:55%; height:.86em }
/* The lead paragraph's own placeholder: two or three short bars, purely
   decorative — the paragraph's real height floor comes from `.reads-lead`'s
   own `min-height` above, identically for this and for the real sentence, so
   these do not need to be measured against anything. */
.skel-lead{ display:flex; flex-direction:column; gap:8px; padding-top:2px }
.skel-line{ display:block; height:.8em; width:92%; }
.skel-line:last-child{ width:58% }
@keyframes skel-pulse{ 0%,100%{ opacity:.5 } 50%{ opacity:.95 } }
@media (prefers-reduced-motion: reduce){ .skel-block, .skel-bar, .skel-line{ animation:none; opacity:.7 } }

/* ── the badge a game just earned ───────────────────────────────────────────
   ⚠️ ABOVE THE REPORT AND OUTSIDE THE PAYWALL. Badges were computed from the
   record and shown only on a panel somebody had to go and open, so a player won
   one and found out days later, if ever. This is the one line in the report
   that is pure good news, and a free player sees it too — it is a record of
   what they did, not part of what is sold. */
.earned-now{ margin:0 0 16px; padding:12px 14px; border-radius:10px;
  border:1px solid rgba(201,162,39,.34); background:rgba(201,162,39,.06) }
.earned-head{ margin:0 0 8px; font-weight:700; color:var(--brass); font-size:.95rem }
.earned-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:8px }
.earned-list li{ display:flex; align-items:center; gap:10px; flex-wrap:wrap }
.earned-list .badge-icon{ width:40px; height:40px; flex:0 0 auto }
.earned-list b{ color:var(--bone) }
.earned-list .n{ color:var(--bone-3); font-size:.84rem; flex:1 1 100% }

/* ── the offer, at the size of the moment it appears in ──────────────────────
   ⚠️ IT USED TO BE A PRICING PAGE WEDGED INTO A PANEL: two columns of unequal
   length, eleven bullets, two buttons and a terms line, inside a narrow box.
   The lopsidedness was not a styling bug — a six-item column beside a five-item
   one IS lopsided, and no amount of alignment fixes a thing that should not
   have been two columns. Three lines and one button now; the comparison lives
   on /pricing.html, which is a page and can afford it. */
.offer-points{ list-style:none; margin:14px 0 0; padding:0; display:flex; flex-direction:column; gap:8px }
.offer-points li{ position:relative; padding-left:20px; color:var(--bone-2);
  font-size:.92rem; line-height:1.45 }
.offer-points li::before{ content:'✓'; position:absolute; left:0; color:var(--brass); font-weight:700 }
.offer-points strong{ color:var(--bone) }
.offer-terms{ margin:12px 0 0 }

/* ── the front door says which of two things it is ──────────────────────────
   The account button and the play button are the same size and the same weight
   because they occupy the same moment: whichever one you are looking at is the
   only thing on the page to press. */
#goSignup{ display:block; text-align:center; text-decoration:none; border-bottom:0 }
#goSignup:hover{ border-bottom:0 }
.signed-as{ margin:10px 0 0; text-align:center; font-size:.84rem; color:var(--bone-3);
  font-family:"JetBrains Mono",ui-monospace,monospace }

/* ⚠️ THE DIFFICULTY ROW SAT ALMOST AGAINST THE PLAY BUTTON, so the two read as
   one control and the eye could not tell where the primary action ended. They
   are different decisions — one is "start", the other is "how hard" — and the
   space between them has to say so. */
.big-play{ margin-bottom:22px }
#diff{ margin-top:0 }

/* ── the distance between the last word and the footer ──────────────────────
   ⚠️ IT WAS ABOUT 128 PIXELS ON EVERY PAGE, and nobody chose that number: it
   was ~90px of padding INSIDE the content column plus another ~38px of margin
   outside it, two separate rules neither of which knew about the other.
   On a short page — the clubs form, the contact form — that reads as the page
   having ended and then not ending, which is exactly what a reader takes as
   "something failed to load below this".
   One rule owns it now. The padding inside each column was cut to 34px, and
   this sets the outside distance, so the total is one number in one place. */
/* ⚠️ THE MARGIN WAS THE BROWN STRIP. A 26px gap above the footer is 26 pixels
   of PAGE showing through — and the page carries a warm radial gradient, so
   against the darker screen above it read as a lighter band drawn across the
   bottom of the form. It looked like a bar covering the last fields, which is
   worse than a cosmetic problem: it suggested there was nothing below it.
   The air belongs INSIDE the footer, as padding, where nothing shows through
   and the separator line stays exactly one pixel wide. */
/* ⚠️ AND THE PADDING IS SYMMETRIC. Moving the air inside the footer left 22px
   above the links and 11 below — inherited from the old rule, where the space
   above came from a margin nobody counted. The row sat against the bottom edge
   of the page and read as cut off. Equal on both sides: the links are centred
   in their own band, not resting on the floor. */
/* ⚠️ BALANCED *AND* THIN, WHICH ARE TWO REQUIREMENTS THAT PULLED APART. Equal
   padding fixed the row sitting on the floor; 22px of it grew the footer to
   67px, and a check written when it was slimmed from 73 wants it under 48 —
   because a footer is chrome, and chrome that takes a fifth of a phone screen
   is a section. 14px each side: symmetric, and 45px tall. */
.site-foot{ margin-top:0; padding:14px 18px }
body.doc main{ padding-bottom:34px }
/* The legal page is a long document and earns a little more air before the
   footer — a reader arriving at the bottom of 11,000 pixels of terms has
   finished something, and the break should say so. */
body.doc main.legal-page{ padding-bottom:48px }

/* ── settings and progress are cards too ────────────────────────────────────
   ⚠️ BOTH RENDERED IN THE TOP-LEFT CORNER OF AN EMPTY SCREEN. The account
   screen was centred when the same thing was reported about it; these two were
   left behind, so the site had one centred screen and two that looked like a
   layout that had failed to load. Same rule, same width, all three. */
/* ⚠️ NO min-height HERE, AND THAT WAS A REGRESSION I INTRODUCED. Centring these
   screens with `min-height:100dvh` made each of them exactly one viewport tall
   — inside a body that is already a flex column with the footer as its last
   row. So the screen filled the window and the footer was pushed to 926px in a
   900px window: present in the DOM, off the bottom of the screen, on three of
   the four screens. The front door was fine, which is why it went unnoticed.
   `body.app > .screen` already says `flex:1 1 auto; min-height:0` — the screen
   takes the space that is left. All these rules have to do is centre what is
   inside it. */
#settings, #progress{ display:flex; align-items:flex-start; justify-content:center;
  padding:24px 16px 32px; box-sizing:border-box; overflow-y:auto }
#settings.hidden, #progress.hidden{ display:none }
/* ⛔ A RAW 460px, WHICH IS THE FOURTH ARBITRARY WIDTH ARRIVING AGAIN — and it
   slipped past page-frame-test because that check only looks at containers
   above 600px. It is now the reading measure, and that is not only tidiness:
   settings and progress are rows of label-and-value, and at 460px on a laptop
   they were a narrow ribbon in the middle of an empty screen while the school
   next door read at 760. Same measure now, so moving between them stops
   feeling like moving between two sites. The front door keeps --w-card: it is
   one decision and one button, and a focused card is right for that. */
#settings > .start-inner, #progress > .start-inner{ width:100%; max-width:var(--w-read); margin:0 auto }

/* ── it is your turn ────────────────────────────────────────────────────────
   ⚠️ THE TABLE GOING QUIET WHILE IT WAITS FOR YOU LOOKS EXACTLY LIKE A GAME
   THAT HAS FROZEN. Eight seats, a seating order you did not choose, and no name
   of your own unless you set one — so a person waits, decides it is broken, and
   reloads. The bottom bar does say whose turn it is, in words, at the opposite
   end of the screen from the seat they are looking at.
   A slow pulse on your own seat, not a flash: this is an orientation cue, not
   an alarm, and it has to be readable by somebody who is thinking.
   ⚠️ AND IT STOPS COMPLETELY UNDER prefers-reduced-motion. A pulsing element is
   one of the named triggers for vestibular disorders, and a game somebody
   cannot look at is not a game they will play. The border stays lit; only the
   movement goes. */
/* ⚠️ LOUDER, BECAUSE IT WAS MEASURED WORKING AND STILL WENT UNNOTICED. The
   class fired, the animation ran, the border was brass — and the owner, who
   knew the feature existed, played a whole game without seeing it. A ring at
   42% opacity fading to nothing, on a dark wooden table, is a rumour.
   Three channels now instead of one, because any single channel fails somebody:
   the ring is brighter and travels further, the plate itself lifts, and the
   word "your turn" appears on it — colour alone is unreadable to roughly one
   man in twelve, and a pulse alone is invisible to anybody who has looked away.
   Reduced motion still gets a solid ring and keeps the word. */
@keyframes your-turn-pulse{
  0%,100%{ box-shadow:0 0 0 0 rgba(201,162,39,.85), 0 0 12px 0 rgba(201,162,39,.30) }
  50%    { box-shadow:0 0 0 9px rgba(201,162,39,0),  0 0 22px 4px rgba(201,162,39,.16) }
}
.seat.your-turn{
  border-color:var(--brass);
  /* ⚠️ TWO LAYERS, NOT A REPLACEMENT. Setting `background` outright threw away
     the wood the whole table is made of and the seat went flat. The brass tint
     sits ON the wood. */
  background:linear-gradient(180deg,rgba(201,162,39,.18),rgba(201,162,39,0) 70%),
             linear-gradient(180deg,var(--wood-3),#241A14);
  animation:your-turn-pulse 1.5s ease-in-out infinite;
}
/* The word lives in the markup, inside the layout, so nothing can overlap it —
   an absolutely positioned caption sat on top of the name at ordinary widths. */
.seat.your-turn .turn-tag{
  display:block; font-size:.58rem; letter-spacing:.09em; text-transform:uppercase;
  color:var(--brass); margin-top:1px;
}
@media (prefers-reduced-motion: reduce){
  .seat.your-turn{ animation:none; box-shadow:0 0 0 4px rgba(201,162,39,.62) }
}

/* ── who you are, under the button ──────────────────────────────────────────
   ⚠️ IT WAS TOUCHING THE DIFFICULTY ROW — measured at ZERO pixels between them,
   so the address and the four room buttons read as one block and the eye could
   not tell that one is a statement and the other is a control.
   Sign-out lives here too. It used to exist only on the account screen, two
   clicks away behind a heading nobody opens unless something is wrong — so on
   a shared computer the second person could not become themselves. The line
   that says who you are is the line that lets you stop being. */
.signed-as{ margin:24px 0 26px; display:flex; gap:12px; justify-content:center;   /* BUG-62: air above the signed-in row */
  align-items:baseline; flex-wrap:wrap }
.signed-as #signOutFront{ font-size:.82rem }

/* ⚠️ THE ACCOUNT CARD SAT AGAINST THE TOP OF THE SCREEN. Centring it works only
   while it fits; with the first-run strip and the club fold open it is taller
   than the window, and a flex row that overflows pins its content to the start
   — so the heading ended up touching the ceiling with no air above it.
   Padding rather than a different alignment: on a short viewport it gives the
   card breathing room at the top, and on a tall one the centring still wins. */
#account{ padding-top:36px; padding-bottom:36px }
/* ⚠️ FIVE PIXELS between the way out and the title, so the two read as one
   two-line heading and the link stopped looking like a link. They are different
   things: one leaves, one names the page. */
.screen-back{ margin:0 0 18px }

/* ── the copyright is a note, not an eighth link ────────────────────────────
   ⚠️ IT SAT IN THE SAME ROW AT THE SAME SIZE AND THE SAME COLOUR AS THE SEVEN
   LINKS BESIDE IT, so the eye counted it as one of them and then failed to
   click it. Researched convention on the left/right question is that there is
   no rule — one centred row is a legitimate pattern for a small site — but
   every source agrees the legal line should be visually SEPARATED from the
   navigation, by a divider or a shade. A rule and a dimmer weight do that
   without adding a second footer row to an eight-page site. */
.site-foot .foot-c{ opacity:.6; font-size:.94em; margin-left:4px;
  padding-left:14px; border-left:1px solid var(--edge-2) }
@media (max-width:560px){
  /* On a phone the row wraps, and a left border on a wrapped line points at
     nothing. It becomes its own quiet line instead. */
  .site-foot .foot-c{ border-left:0; padding-left:0; margin-left:0; flex-basis:100%;
    text-align:center; margin-top:2px }
}

/* ── what this account is paying for ────────────────────────────────────────
   ⚠️ A SUBSCRIPTION NOBODY IS REMINDED OF IS ONE THAT GETS CANCELLED on the
   first bank statement somebody reads properly: the charge is remembered, the
   thing it buys is not. It is also the only honest place to say how many days
   of a club trial are left, which is the number that decides whether somebody
   subscribes at all.
   Brass for paid, quieter for a trial — a countdown in red would be the fear
   lever, and a trial ending is not an emergency. */
.tier-badge{ font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.66rem; letter-spacing:.12em;
  text-transform:uppercase; padding:3px 9px; border-radius:99px;
  border:1px solid var(--brass); color:var(--brass); background:rgba(201,162,39,.08) }
.tier-badge.trial{ border-color:var(--edge); color:var(--bone-3); background:rgba(255,255,255,.03) }

/* ⚠️ AUTOFILL PAINTS ITS OWN BACKGROUND AND IGNORES `background`. Chrome and
   Safari both draw a white-blue wash over a filled field using an internal
   style that only an inset box-shadow can cover — so a password the browser
   filled in came out as a white box in the middle of a dark form, on the one
   screen where somebody is deciding whether to trust this with a password.
   The colour has to be repeated here because the shadow, not the background,
   is what is actually visible. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active{
  -webkit-box-shadow:0 0 0 1000px var(--wood-2) inset !important;
  -webkit-text-fill-color:var(--bone) !important;
  caret-color:var(--bone);
  transition:background-color 9999s ease-out 0s;
}

/* ⚠️ THE ANCHOR PRIMARY NEEDS THE SAME FOCUS RING AS THE BUTTON ONE. The front
   door's main control is a <button> for a signed-in player and an <a> for
   everybody else — it navigates rather than acts, which is the correct tag —
   and the focus styles were written for buttons. So the one control a stranger
   sees had no visible focus at all: a keyboard user could reach it and could
   not tell they had. Caught by the accessibility suite, which is exactly what
   it is for. */
a.primary:focus-visible, a.big-play:focus-visible, .back-to-game:focus-visible{
  outline:2px solid var(--brass); outline-offset:3px; border-radius:3px;
}

/* ⚠️ THE "IN TESTING" BADGE — half the public club list is seeded, and every
   seeded row says so. It is deliberately readable rather than decorative: a
   whisper here is the same as lying, because the whole point is that somebody
   who does not recognise their own city learns why, instead of concluding the
   list is invented. Brass on wood at the sizes used elsewhere for state. */
/* ⚠️ THIS WAS A PILL OF WORDS, AND THE WORDS COULD NOT BE READ. Bone-white
   text on brass measures 1.9:1 — below every threshold there is — and at seven
   rows it turned the club list into a column of yellow smears with the club
   names competing underneath. A label nobody can read is worse than no label:
   it takes the space and gives back nothing.
   It is a dot now. The meaning lives in the title, which every browser and
   every screen reader already knows how to show, and the letter T is drawn in
   the page's own dark ink on brass — 9.4:1, and legible at 14 pixels. */
/* ⛔ `overflow:hidden` WAS HERE AND IT ATE THE TOOLTIP. The badge hid its own
   label — "in testing" — by clipping a 15×15 box, and the explanation below is
   drawn as a ::before on that same box. It was generated correctly, measured
   260px wide and 56px tall, and was clipped to nothing by the rule two lines
   above it. Reported twice as "I still do not see a popup", and both times the
   tooltip code was fine.
   The label is a visually-hidden child now, so the badge never needs to clip
   anything and the tooltip can leave the circle. */
.demo-tag{
  display:inline-flex; align-items:center; justify-content:center;
  width:24px; height:24px; border-radius:999px; margin-left:7px;
  vertical-align:-6px; cursor:help;
  background:var(--brass); color:var(--wood);
  font-size:.66rem; font-weight:700; line-height:1;
  position:relative; overflow:visible; flex:none;
}
.demo-tag::after{
  content:'T'; display:flex; align-items:center; justify-content:center;
  font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; letter-spacing:0;
}
.demo-tag:hover, .demo-tag:focus-visible{ outline:none; box-shadow:0 0 0 3px rgba(201,162,39,.28) }
/* ⚠️ A LETTER IN A CIRCLE EXPLAINS NOTHING, AND `title=` IS NOT AN EXPLANATION
   EITHER — the browser's native tooltip takes about a second to appear, is
   drawn in the OS font, and never appears at all on a touch screen. Reported
   as exactly that: "T для клубов не совсем понятно".
   So the page draws its own, in the page's own type, instantly, on hover AND
   on keyboard focus. `data-tip` carries the sentence, so one attribute is the
   whole API and there is no second element to keep in step. */
/* ⚠️ AND IT OPENS ON A TAP, NOT ONLY ON A HOVER. A touch screen has no hover at
   all, so on a phone this badge explained itself to nobody. `.on` is put there
   by clubs.js on a press and taken off by the next press, Escape, or a click
   anywhere else. Three ways in — pointer, keyboard, finger. */
/* ⛔ IT IS NOT A `::before` ON THE BADGE ANY MORE, and that is the whole fix.
   The badge lives inside `.wrap`, which carries `overflow-x:auto` so a wide
   table can scroll on a phone — and `overflow-x:auto` computes `overflow-y` to
   `auto` too, which makes `.wrap` a scroll container, which CLIPS everything
   inside it. Measured live at 1280px: badge top y=423, bubble 74px tall opening
   8px above it → wanted y=340; `.wrap` starts at y=360. The first line was cut
   off, the "Bring your club" button sits at that exact height, and it was
   reported as the button covering the popup. No z-index fixes a clip.
   So the bubble is a single element parked on <body> and positioned by
   clubs.js. `position:fixed` answers to the viewport, so no ancestor's overflow
   can reach it.
   ⚠️ z-index ABOVE THE STICKY BAR (60). A tooltip that opens under the
   navigation is the same bug wearing a different ancestor. */
.club-tip{
  position:fixed; z-index:70; left:0; top:0;
  width:max-content; max-width:min(280px, 72vw);
  text-align:left; text-transform:none; letter-spacing:0;
  font-family:inherit; font-size:.8rem; font-weight:400; line-height:1.45;
  background:#241A14; color:var(--bone-2);
  border:1px solid var(--brass-2); border-radius:4px; padding:8px 10px;
  box-shadow:0 12px 30px -12px rgba(0,0,0,.9);
  pointer-events:none;
}
/* ⚠️ AN AUTHOR `display` BEATS THE UA'S `[hidden]{display:none}`, and this
   stylesheet has now shipped that bug three times — on a form, on a chip and on
   a navigation link. `.club-tip` sets no display of its own, so `hidden` works;
   this rule is the belt, because the next person to add `display:block` here
   would ship a tooltip that never closes. */
.club-tip[hidden]{ display:none !important }
/* ⚠️ IT IS A TARGET NOW, SO IT IS SIZED LIKE ONE. It used to be a 15px mark
   with no click and no keyboard stop, and the note here said WCAG 2.2 SC 2.5.8
   did not apply because nothing was lost by not hovering it — the sentence
   under the table said the same thing in full. That sentence is gone (it was
   three lines of small print explaining one letter), so this badge is now the
   only place the explanation lives, it is pressable, and 24px with a 3px focus
   ring is the smallest honest size for a thing a finger has to find. */

/* ⚠️ THE SECOND DOOR ON THE FRONT PAGE. The site offers two things — play, and
   learn — and until 2026-09-06 only one of them was on the screen. It sits
   under the terms line rather than beside Play on purpose: a person who came to
   play should not have to choose between two buttons before they have seen a
   game. Quiet, always visible, and it goes somewhere real. */
/* ⚠️ THE SECOND DOOR WAS A GREY UNDERLINED LINE AND NOBODY WOULD EVER PRESS
   IT. It sat below the terms, in the same size and colour as "privacy notice",
   offering the only thing on this site anybody pays for. A person scanning the
   page saw three pieces of small print and one of them happened to be the
   product.
   It is a card now, and it carries the one fact that makes somebody curious —
   a measured number they can check — because "lessons" sells nothing and
   "84.7% of the time, and almost nobody looks" is an itch. Brass, not red:
   there is one red control per screen and it is Play. */
.learn-door{ display:block; max-width:400px; margin:26px auto 0; padding:14px 16px;
             border:1px solid var(--brass-2); border-radius:4px; text-align:left;
             background:linear-gradient(180deg, rgba(201,162,39,.09), rgba(201,162,39,.03));
             text-decoration:none; transition:.15s }
.learn-door:hover{ border-color:var(--brass); background:linear-gradient(180deg, rgba(201,162,39,.15), rgba(201,162,39,.05)) }
.learn-door .ld-eyebrow{ display:block; font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-size:.68rem; letter-spacing:.14em;
                         text-transform:uppercase; color:var(--brass); margin:0 0 5px }
.learn-door .ld-title{ display:block; color:var(--bone); font-size:.98rem; font-weight:600; line-height:1.35 }
.learn-door .ld-proof{ display:block; color:var(--bone-3); font-size:.84rem; line-height:1.5; margin:5px 0 0 }
.learn-door .ld-proof b{ color:var(--brass) }
.learn-door .ld-go{ display:block; color:var(--brass); font-size:.82rem; margin:8px 0 0 }

/* ⚠️ THE LESSON'S LINE, AND IT IS DELIBERATELY NOT THE HINT BAR. A hint is
   about this moment and changes as the game moves; this is what the reader
   came to watch and stays for the whole game. Same shape so it does not look
   bolted on, different colour so nobody confuses the two. */
.watch-bar{ display:flex; gap:8px; align-items:flex-start; padding:8px 12px; margin:0 0 8px;
            border-left:2px solid var(--brass-2,#c4a45a); background:rgba(196,164,90,.07);
            font-size:.92rem; line-height:1.45 }
.watch-bar .hint-i{ flex:none }
/* ⚠️ «СЕЙЧАС» ВЫГЛЯДИТ ИНАЧЕ, НО НЕ ПЕРЕБИВАЕТ. Тот же элемент, чуть плотнее фон и
   полная толщина полосы слева — момент видно, и при этом ничего не всплывает поверх
   игры. Шут 2008 прямо запрещает перебивать того, кто занят делом. */
.watch-bar.is-now{ border-left-width:4px; background:rgba(196,164,90,.16) }

/* w12-named-opponent — same shape as .watch-bar, and the SAME identity
   colour the seat's own "familiar face" chip uses (src/client/pure.js's
   `.known`/`.seat-known` — see the note there), so the banner reads as the
   same fact the badge on the table is already showing, not a third thing.
   One line, day one only (Game.jsx gates it on `view.day`). */
.named-bar{ display:flex; gap:8px; align-items:flex-start; padding:8px 12px; margin:0 0 8px;
            border-left:2px solid var(--known-2); background:rgba(201,162,39,.07);
            font-size:.92rem; line-height:1.45 }
.named-bar .hint-i{ flex:none }

/* ═══ the one navigation ═══════════════════════════════════════════════════
   ⚠️ STICKY, AND THE FOOTER IS NOT. Reported as "a mix of behaviour", and it
   was: the front door could not scroll at all so its footer looked pinned,
   three pages had a sticky bar, one had its own, four had a bare back link and
   two had nothing. A navigation is a tool you reach for mid-page; a footer is
   the end of a document. Fixing a footer costs a row of screen on every phone
   and buys nothing, because the links in it are the ones nobody needs until
   they are leaving.
   ⛔ z-index 60, NOT 5. The old bar sat at 5 and page content scrolled OVER it
   on the character page — reported exactly that way. Cards on these pages carry
   their own shadows and stacking, and 5 is inside the range they use. */
.site-nav{
  position:sticky; top:0; z-index:60;
  background:rgba(26,19,16,.94); backdrop-filter:blur(10px);
  border-bottom:1px solid var(--edge);
}
@supports not (backdrop-filter: blur(1px)){ .site-nav{ background:var(--wood) } }
/* ⚠️ pages-review 2026-09-16, finding #4 — a dark rectangular SEAM appeared
   next to the language switcher on gestures-poster.html
   and evening-kit.html (and, until it was removed, rules-sheet.html —
   BUG-112, 17.09.2026), and only on those. Root cause: `.nav-links`'s
   own scroll-edge cue (`.nav-links::after` below) paints a SECOND
   `rgba(26,19,16,.94)` layer over the already-translucent, blurred nav — two
   semi-transparent layers of the same colour compound to a slightly more
   opaque patch than the bar around it. Over a DARK page that difference is
   invisible (blurred dark content behind either way); these two pages are
   the only ones with a WHITE `body.poster`, where the plain bar reads
   slightly warmer (white bleeding through the blur) than the compounded
   patch beside it. Making the bar fully opaque on these three pages — no
   blur, nothing to bleed through — makes the two layers the same colour
   again, matching the existing no-`backdrop-filter` fallback two lines up. */
body.poster .site-nav{ background:var(--wood); backdrop-filter:none; }
.site-nav-in{
  max-width:var(--w-page); margin:0 auto; padding:0 18px;
  display:flex; align-items:center; gap:18px;
}
/* ⚠️ ROUND 8 BP (2026-09-13) — THE BAR'S MARK IS THE DOOR'S SIGN, SHRUNK, NOT A
   DIFFERENT ONE. `.nav-mark` carried its own lighter weight, tighter tracking
   and a serif fallback missing Georgia — three quiet mismatches against
   `#start h1` below — and, because it never overrode `border-bottom`, it fell
   back to the faint gold rule every plain `a` gets by default (line ~512),
   showing a permanent underline under the wordmark on every page and, on
   Clubs, a SECOND gold line right beside the active tab's own. font-family,
   font-weight and letter-spacing now match the door exactly; only font-size
   differs, sized to the bar. `a.nav-mark` (not `.nav-mark`) so this rule ties
   `a:hover`'s specificity and wins on hover too — the mark carries no
   underline of its own, ever; the brass rule under a tab belongs to that tab.
   tests/browser/brand-test.mjs proves both halves, --prove included.
   ⚠️ MERGED WITH BO (nav-stability-test.mjs, same wave): BO's original fix
   pinned `.nav-mark` to the system font stack so it would render identically
   whether or not Google Fonts had loaded — because before this merge the door
   font (Cinzel) was requested only by `index.html`, never by the ~60 static
   pages. Loading a serif wordmark on some pages and not others reproduces the
   exact "the bar jumps" defect BO measured, just with a different font pair.
   The merge keeps BP's design decision (the mark IS the door's sign) and
   restores BO's guarantee a different way: the SAME Cinzel `<link>` is now
   requested by every page — the ~60 static pages via page-frame/build-pages,
   the app via `index.html` (unchanged) — so the request, the cache, and the
   fallback timing are identical everywhere instead of split app/static.
   nav-stability-test.mjs checks this directly, before AND after
   `document.fonts.ready`, on every page. `.nav-links a` and `.nav-lang-btn`
   below are UNCHANGED — still pinned to the system stack per BO; only the
   wordmark itself carries the door's serif, and only because it can now do so
   consistently. ⚠️ CM (round 8, done) replaced the Google Fonts request
   with a self-hosted woff2 subset — same guarantee, zero third-party
   request. The family list here is UNCHANGED on purpose
   (Cinzel,'Playfair Display',Georgia,serif, byte-identical to `#start h1`'s
   own, which now carries the same declaration explicitly — it stopped
   inheriting it the moment `h1,h2,h3,.deco` moved to Bitter for body
   headings) so brand-test.mjs's exact-match check keeps passing without
   ever reading this comment. Only Cinzel 700, uppercase Latin only, is
   self-hosted (src/client/fonts/); 'Playfair Display' stays an unfetched
   fallback name, same as before. Barlow and Inter are retired entirely;
   every other heading is Bitter, everything else is Lato — sync-chrome.mjs
   / build-pages.mjs / index.html now preload one critical Lato file instead
   of linking fonts.googleapis.com. ⚠️ "JetBrains Mono" below is NOT
   self-hosted (out of this task's scope) — it was the same Google Fonts
   `<link>` that carried Cinzel/Barlow, so every `"JetBrains Mono",monospace`
   declaration now falls through to its own generic `monospace` fallback,
   silently and on purpose (numbers/badges/ticks render in the OS monospace
   face instead — the fallback this stack always declared for exactly this
   case, never a network error). Revisit only if that specific look is
   missed; nothing here relies on it.
   ⚠️ text-transform:uppercase ADDED HERE, not in BP's original patch — brand-
   test.mjs (BP's own file) checks the mark's transform against `#start h1`,
   and BN's door-2 patch (this same wave) gave the door `text-transform:
   uppercase` AFTER BP wrote the "matches the door exactly" comment above.
   Two true statements from two patches, one now-stale: restoring the actual
   parity BP's comment already promises. */
a.nav-mark{
  flex:0 0 auto; font-family:Cinzel,'Playfair Display',Georgia,serif; font-size:.95rem;
  font-weight:700; letter-spacing:.2em; text-transform:uppercase;
  color:var(--bone); text-decoration:none; border-bottom:2px solid transparent;
  padding:12px 0; min-height:44px;
  display:inline-flex; align-items:center;
}
a.nav-mark:hover{ color:var(--brass) }
/* ⚠️ THE LINKS SCROLL SIDEWAYS RATHER THAN WRAPPING. Five words plus a wordmark
   is about 330px and a small phone is 320 — a wrapping bar becomes two rows
   tall on exactly the screens with the least room. A strip that scrolls keeps
   the bar one row on every width, and the scrollbar is hidden because a
   navigation with a visible track under it looks broken. */
.nav-links{
  display:flex; gap:16px; align-items:center; margin-left:auto;
  overflow-x:auto; scrollbar-width:none; -webkit-overflow-scrolling:touch;
}
.nav-links::-webkit-scrollbar{ display:none }
/* ⚠️ GATE16-FIX (gate w16, first pass, finding #5) — A STRIP THAT SCROLLS
 * SIDEWAYS WITH NO EDGE CUE READS AS CUT OFF, NOT "KEEP SCROLLING". Reported
 * at 390px: the last visible link was a bare fragment ("icing", the tail of
 * "Pricing") with nothing on screen suggesting more sat past it.
 * ⚠️ PURE CSS, NO MARKUP CHANGE — `_nav.html` and `SiteNav.jsx` stay
 * byte-identical, which is the promise this bar's own comments make (see
 * `_nav.html`'s header and SiteNav.jsx's "ТА ЖЕ РАЗМЕТКА" note): the fade is
 * a `::after` pseudo-element, the LAST flex child, so no link before it
 * moves. `position:sticky; right:-1px` pins it to the visible right edge of
 * the scrollport as the strip scrolls (`position:absolute` would scroll away
 * WITH the content instead — sticky is the one positioning mode that stays
 * put against a scrolling ancestor). `nav-stability-test.mjs` measures only
 * the real `.nav-links a` elements' own x/width, none of which this touches.
 * Coloured to match `.site-nav`'s own background (`rgba(26,19,16,.94)`, same
 * fallback split for browsers without backdrop-filter) so it reads as the
 * bar itself fading out, not a foreign box. */
.nav-links{ position:relative; }
.nav-links::after{
  content:''; flex:0 0 22px; align-self:stretch;
  position:sticky; right:-1px; pointer-events:none;
  background:linear-gradient(to right, transparent, rgba(26,19,16,.94) 60%);
}
@supports not (backdrop-filter: blur(1px)){
  .nav-links::after{ background:linear-gradient(to right, transparent, var(--wood) 60%); }
}
.nav-links a{
  flex:0 0 auto; color:var(--bone-3); text-decoration:none; white-space:nowrap;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  font-size:.82rem; font-weight:400; letter-spacing:.04em; padding:12px 0; min-height:44px;
  display:inline-flex; align-items:center; border-bottom:0; position:relative;
  transition:color .14s;
}
/* BUG-48 (owner, 16.09: "the tabs' marks must be the same length, and only
   under the text"): the current-page mark is a short bar of one fixed width,
   centred under the word — not a border as wide as the word, which made
   "Тренажёр" carry a mark three times longer than "Уроки". Transparent until
   current, so the box never changes. */
.nav-links a::after{
  content:''; position:absolute; left:50%; bottom:7px; width:22px; height:2px;
  transform:translateX(-50%); background:transparent; transition:background-color .14s;
}
.nav-links a:hover{ color:var(--bone) }
.nav-links a:focus-visible{ outline:2px solid var(--brass); outline-offset:3px }
/* ⚠️ THE PAGE YOU ARE ON IS MARKED — BY COLOUR AND THE UNDERLINE ONLY.
   `font-weight` stays the same 400 as every sibling (set explicitly above, not
   left to inherit) and the border-bottom is 2px solid on every link, transparent
   until active — so marking a page never changes the box a link occupies, only
   what colour paints inside it. Without the mark at all a navigation is a row of
   exits with no answer to "where am I" — the single thing a person checks first. */
.nav-links a[aria-current="page"]{ color:var(--brass) }
.nav-links a[aria-current="page"]::after{ background:var(--brass-2) }
/* ⛔ NO PER-POSITION WIDTH RESERVATIONS (owner, 16.09: "the links sit at
   different distances from each other — as if some were just made wider").
   The old `nth-child(n){min-width}` rules were sized for a six-link bar
   (Lessons · Guide · Pricing · Clubs · Progress · Settings); after Gestures
   and Trainer joined, every reservation landed on the wrong link and English
   labels floated in slots cut for other words. The bar now spaces every link
   by its own `gap` alone; the language-swap shift the reservations guarded
   (docs/UX, 14.09) is measured by door-cls-test.mjs on every run, and the
   door is revealed only once it is correct (locked decision 10), so a swap
   that lands before the reveal moves nothing a reader can see. */
@media (max-width:520px){
  .site-nav-in{ gap:12px; padding:0 14px }
  .nav-links{ gap:14px }
  /* letter-spacing stays .2em from the base rule above — an em value already
     scales with font-size, so a second number here would only drift from the
     door's ratio again the next time either changed. */
  a.nav-mark{ font-size:.86rem }
  /* ⚠️ w16fixh (round 8) — game-board-contract-test.mjs, section 2: at a
     360px phone the two reservations just above (67px/100px, the widest of
     en/uk/ru for Progress/Settings) plus the GATE16-FIX fade's own 22px +
     its 14px gap add up to 217px of scrollable content for the strip's
     LAST TWO real links — wider than the 193px the strip actually has to
     show them in, so scrolling to the true end could no longer bring BOTH
     Progress and Settings inside the visible strip at once (it used to,
     before the reservation existed, when Progress/Settings still had their
     narrower unreserved English widths).
     ⚠️ THE RESERVATIONS THEMSELVES ARE NOT THE BUG AND STAY UNCHANGED —
     67px/100px are the measured widest real word in each position
     (Прогресс 66.1px, Налаштування 99.3px; measured live, круг 8), not an
     overestimate to trim. Shrinking either brings back the exact
     door-cls-test.mjs shift (§ I18N-LAZY-CLS) this reservation exists to
     remove.
     ⚠️ THE FADE IS THE ONE PIECE OF THIS ROW THAT IS DECORATIVE, NOT A
     DESTINATION — a hint that more content sits past the fold, not a link
     anyone taps. Once the strip is genuinely at its last pixel there is no
     "more" left to hint at, so letting it sit OVER the tail of Settings (a
     transparent-to-wood gradient — it tints the text, it does not hide it)
     costs nothing a reader would notice, and frees exactly the width
     Progress needs to stop being clipped. `flex-basis` shrinks from 22px
     to 8px and `margin-left:-14px` cancels the gap `.nav-links{gap}` would
     otherwise still add in front of it, netting 22+14−8=28px back — enough
     to clear the 24px shortfall with room to spare. Untouched above 520px,
     where the strip already has room for the fade at its full size
     (nav-phone-click-test.mjs's own 390px measurement: 223px of strip
     against 217px needed — no shortfall there to begin with). */
  .nav-links::after{ flex-basis:8px; margin-left:-14px }
}
/* ── Переключатель языка ─────────────────────────────────────────────────────
   ⚠️ ОТДЕЛЁН ЧЕРТОЙ ОТ ССЫЛОК. Слева — места, куда идут; справа — настройка того,
   на каком языке их читать. Одинаковые по виду, они читались бы как ещё один раздел.
   ⚠️ 44×44 — минимальная цель нажатия по WCAG 2.2, как у соседних ссылок. */
.nav-lang{ position:relative; flex:0 0 auto; padding-left:12px; margin-left:2px;
  border-left:1px solid var(--edge) }
/* ⚠️ font-family REPEATED AFTER `font:inherit` ON PURPOSE. That shorthand resets
   every font sub-property to "inherit", including family — which would otherwise
   pull the same page-dependent `Barlow,Inter,…` stack `.nav-mark`/`.nav-links a`
   were just pinned away from (see the note above them), and the two-letter code
   (EN/UA/RU) would measure a few tenths of a pixel differently between a static
   page and "/". Small on this one word, but the button sits hard against the
   right edge and any width change there moves the whole row's stopping point. */
.nav-lang-btn{ display:inline-flex; align-items:center; justify-content:flex-end; gap:6px; min-height:44px; min-width:44px;
  /* BUG-30 (owner, 16.09): no padding on the right — the chevron's ink ends
     at the same distance from the edge as the logo's ink starts on the left;
     the 44px tap target comes from min-width, not from this padding. */
  padding:0 0 0 6px; background:none; border:0; border-radius:4px; cursor:pointer;
  color:var(--bone-3); font:inherit;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  font-size:.78rem; letter-spacing:.1em; font-weight:600;
  transition:color .14s }
.nav-lang-btn:hover, .nav-lang-btn[aria-expanded="true"]{ color:var(--bone) }
.nav-lang-btn:focus-visible{ outline:2px solid var(--brass); outline-offset:2px }
.nav-lang-chev{ transition:transform .16s; opacity:.8 }
.nav-lang-btn[aria-expanded="true"] .nav-lang-chev{ transform:rotate(180deg) }
@media (prefers-reduced-motion:reduce){ .nav-lang-chev{ transition:none } }
/* ⚠️ ВЫРАВНИВАНИЕ ПО ПРАВОМУ КРАЮ. Кнопка стоит у самого края экрана; меню,
   выровненное влево, на телефоне ушло бы за экран на половину своей ширины. */
.nav-lang-list{ position:absolute; right:0; top:calc(100% - 2px); z-index:70;
  min-width:176px; margin:0; padding:6px; list-style:none;
  background:var(--wood); border:1px solid var(--edge); border-radius:8px;
  box-shadow:0 14px 34px rgba(0,0,0,.5) }
/* ⛔ ЭТА СТРОКА ОБЯЗАТЕЛЬНА. Авторский display перебивает [hidden]{display:none},
   и этот файл попадал в эту ловушку уже трижды (.nav-mine, .learn-chip, form).
   Без неё меню было бы открыто всегда. */
.nav-lang-list[hidden]{ display:none }
/* ⚠️ border:0 ОБЯЗАТЕЛЕН. На страницах-документах у ссылок есть латунная черта снизу
   (подчёркивание ссылок в тексте), и она протекала сюда — под каждым языком. */
.nav-lang-list a{ display:flex; align-items:center; justify-content:space-between; gap:14px;
  min-height:44px; padding:0 12px; border:0; border-radius:5px; text-decoration:none;
  color:var(--bone-2); font-size:.92rem; letter-spacing:.01em }
.nav-lang-list a:hover, .nav-lang-list a:focus-visible{ background:rgba(244,236,220,.07); color:var(--bone); outline:none }
.nav-lang-list a:focus-visible{ box-shadow:inset 0 0 0 2px var(--brass) }
/* ⚠️ ТЕКУЩИЙ ЯЗЫК ВИДЕН И ЦВЕТОМ, И ЗНАКОМ — цвет один не годится для тех, кто
   цвета не различает (WCAG 1.4.1). */
.nav-lang-list a[aria-current="true"]{ color:var(--brass) }
.nav-lang-list a[aria-current="true"]::after{ content:"✓"; font-size:.9rem }
@media (max-width:520px){ .nav-lang{ padding-left:8px } .nav-lang-btn{ padding:0 0 0 4px } }

/* ⚠️ AN ANCHOR MUST NOT LAND UNDER THE BAR. `#terms` scrolled the heading to
   the top of the window, which is where the navigation is. */
:target{ scroll-margin-top:64px }

/* ⛔ NO BAR AT THE TABLE, for the same reason there is no footer there: a live
   game is a full-height instrument, and a row of exits over it is an invitation
   to leave mid-hand. */
body.playing .site-nav{ display:none }
/* In the app the body is a non-scrolling flex column, so the bar is simply its
   first row — the screens take what is left. */
body.app > .site-nav{ flex:0 0 auto; position:static }
.nav-mine[hidden]{ display:none }
/* The bar has two groups — the site (Lessons … Trainer) and yours (Progress ·
   Settings). A hairline before the first "yours" link says so; the gap on
   both sides of it stays the bar's own, so nothing reads as a wider button. */
/* BUG-32 (owner, 16.09: "Progress sticks to the divider"): the divider is a
   drawn line in the extra margin, NOT a border on the link with padding —
   the active underline is the link's own border-bottom, so padding made the
   underline start at the divider and the word look glued to it. The margin
   doubles the ordinary 16px step, and the line sits in its middle: 16px from
   "Trainer", 16px from "Progress", and the underline covers the word alone. */
.nav-links a:not(.nav-mine) + a.nav-mine{ position:relative; margin-left:16px }
.nav-links a:not(.nav-mine) + a.nav-mine::before{
  content:''; position:absolute; left:-16px; top:50%; height:1.25em; width:1px;
  transform:translateY(-50%); background:var(--edge); pointer-events:none;
}
@media (max-width:520px){
  .nav-links a:not(.nav-mine) + a.nav-mine{ margin-left:14px }
  .nav-links a:not(.nav-mine) + a.nav-mine::before{ left:-14px }
}

/* ── the old "Your progress" guest-empty-state panel (.anon-first /
   #anonProgress, "Nothing is saved yet") is retired, not merely hidden —
   BUG-98 (owner, 17.09) redirects a guest to #account before this screen
   ever mounts, on both the web app and the click that would have opened it.
   See web/src/screens/Progress.jsx's own note on the same spot. */
/* ⚠️ AN AUTHOR `display` BEATS `[hidden]{display:none}`, and `.nav-links a` sets
   one. Without the line above, Progress and Settings would be painted for a
   stranger who has no account to look at. This stylesheet has now fallen into
   that trap three times; the other two are `.learn-chip` and `form` on the
   clubs page. */

/* ═══ the front door: two doors, one of them red ═══════════════════════════
   ⚠️ THE LESSONS WERE A CARD BELOW THE FOLD and were reported as not selling
   and getting lost. They are a button beside Play now. Red is the act this site
   is named after; brass is the thing it sells. One red control per screen. */
.door-row{ display:flex; gap:12px; align-items:stretch; justify-content:center;
           width:100%; max-width:460px; margin:22px auto 0 }
/* ⚠️ "Sign in / Sign up" IS FOUR WORDS AND "Learn" IS ONE. Equal halves made the
   longer one wrap to two lines, so the pair stood 88px tall for a label that
   fits on one. The signed-out door gets the room it needs; for a signed-in
   player the row is Play and Learn, both short, and the ratio costs nothing. */
.door-row #goSignup{ flex:1.7 1 0 }
/* ⚠️ A SENTENCE, NOT A BADGE — the owner's own instruction, 2026-09-11. A little
   lock icon would have been cheaper to build and would have said nothing a
   stranger could check; the words say exactly what legal.html promises. */
.no-ads-line{ margin:12px 0 0; text-align:center }
/* ⚠️ `.big-play` IS `display:block; max-width:320px; margin:22px auto` — written
   for one button owning the width. Inside the row each door takes half, so the
   old measure has to be undone here or the two buttons sit 320px wide apiece in
   a 420px box and overlap. */
.door-row > *{ flex:1 1 0; min-width:0; width:auto; max-width:none; margin:0 }
/* ⛔ THE SECOND BUTTON IS OUTLINED, NOT FILLED, and the filled version was
   reported as looking bad — it did. A solid brass slab beside a solid red one
   is two primaries, which is none: the eye has nowhere to go first. Every
   design system that has thought about this pairs a FILLED primary with an
   OUTLINED secondary, because an outline says "also a button" without
   competing.
   ⚠️ The text is brass on the page's own dark ground, which measures well past
   AA — the filled version needed a solid background-color declared under its
   gradient before any contrast tool could see it at all, and read as 1.14:1
   until it had one. An outline has no such trap. */
.outline.big-play{
  display:inline-flex; align-items:center; justify-content:center; text-decoration:none;
  border-radius:3px; color:var(--brass); background:transparent;
  border:1px solid var(--brass-2); font-family:Lato,-apple-system,BlinkMacSystemFont,sans-serif; font-weight:600;
  letter-spacing:.12em; text-transform:uppercase;
  transition:border-color .14s, color .14s, background .14s;
}
.outline.big-play:hover{ border-color:var(--brass); color:#E4C766; background:rgba(201,162,39,.08) }
.outline.big-play:focus-visible{ outline:2px solid var(--brass); outline-offset:3px }

/* ⚠️ THE LESSONS GET THEIR OWN BLOCK AT THE FOOT OF THE DOOR, with the reason
   above the button. "The first three lessons are free" says what it costs and
   never said why anybody would study a party game — so the button was asking
   for money to fix a problem the reader had not been told they had. */
/* ⛔ THE BUTTON TOUCHED THE DIFFICULTY ROW THE MOMENT SOMEBODY SIGNED OUT.
   Measured: zero pixels between them. The gap was never a gap — it was the
   "signed in as …" line, which is `display:none` for a signed-out visitor, so
   the spacing under the door existed only for people who were already in. A
   layout that depends on an optional sibling being present is a layout that
   breaks in the state it was never looked at in. */
/* 16.09 (owner): "the difficulty buttons sit too close, and under their note
   there is too big a gap". 18px above the control read as glued to the puzzle
   link; 28px gives it the same air the Learn button has above it. */
#diff{ margin-top:28px }
/* …and the gap below came from #accountMsg — the account status line that
   sits after the note and, while EMPTY, still took a line box plus its 10px
   margin (27px of nothing). An empty status line takes no room. */
#accountMsg:empty{ display:none }

.learn-block{ max-width:400px; margin:30px auto 0; padding-top:22px;
              border-top:1px solid var(--edge);
              /* 16.09: the level line and the puzzle link sat flush LEFT under a
                 centred button — every other line in this block already centres
                 itself; the block does it once for all of them now. */
              text-align:center }
/* 16.09 (owner): "Beginner · 0 of 36 — 36 to the crossing" sat 7px under the
   Learn button — `.sub-hint`'s token, meant for a note under a line of text,
   not for the first line after a 60px button. Same air as above the button. */
.learn-block .level-line{ margin:22px 0 0 }   /* BUG-62 (owner, 16.09): air under the button */
.learn-why{ margin:0 0 8px; text-align:center }
/* ⛔ TWO BUTTONS ON ONE SCREEN, IN TWO DIFFERENT SHAPES. Measured live: the
   sign-in button 384×61 and full width, this one 320×89, left-aligned, with its
   label wrapped onto a second line. A secondary action should be QUIETER than
   the primary — not a different size, a different alignment and a different
   number of lines. Side by side that reads as a layout fault rather than a
   hierarchy, and it is the second complaint this button has drawn.
   It keeps the outline, which is what makes it secondary; its box now matches
   the button above it exactly. */
.learn-block .big-play{
  /* `max-width:320px` comes from `.big-play` itself, written for a button that
     owned the width on its own. The block is already the same 384px as the door
     row above it, so releasing the cap makes the two buttons the same box —
     which is the whole point. Measured before: 384×61 against 320×89. */
  margin:14px auto 0; display:flex; width:100%; max-width:none; box-sizing:border-box;
  align-items:center; justify-content:center; text-align:center;
}
.learn-block .free-line{ margin:10px 0 22px }   /* BUG-62: air above the button */
.free-line{ margin:9px 0 0; text-align:center }
/* ⛔ THE DRILL LINK SAT AGAINST THE BUTTON ABOVE IT. Measured: ~4px, the line
   height's own rounding, not a gap. `.sub-hint`'s `margin:7px 0 0` was
   already declared on it — it did nothing, because `#goDrill` is a bare
   `<a>` (`display:inline` by default) and a vertical margin on an inline,
   non-replaced box never moves anything in the flow. `#dueNow`, the sibling
   right above it in this same block, hit the same trap and was already
   given `display:inline-block` for exactly this reason (`.due-now`, below).
   Same fix here — no new number, the 7px `.sub-hint` token just starts
   working. */
#goDrill{ display:inline-block }
/* ⚠️ TWO DOORS SIDE BY SIDE NEED ABOUT 430px. Below that "Sign in / Sign up"
   wraps and the pair stands 88px tall for a label that fits on one line —
   measured at 390px. They stack instead, full width each, which is what a phone
   wants anyway: one thumb, one target, no aiming. */
@media (max-width:430px){
  .door-row{ flex-direction:column; gap:10px; max-width:320px }
  .door-row > *{ flex:none; width:100% }
}

/* ⚠️ I18N-LAZY-CLS (r8w/clsdict, круг 8) — THREE MORE LINES THAT WRAP
 * DIFFERENTLY BY LANGUAGE, ONLY ON A NARROW SCREEN, MEASURED ONE AT A TIME:
 *
 *   #goSignup   "Sign in / Sign up" 61px (1 line) → uk/ru up to 89px (2
 *               lines) — the comment on `.door-row #goSignup` above already
 *               names the English version of this same wrap; a translated
 *               label is longer still and wraps at a WIDER column than
 *               English does (measured: still wrapping at 460px, gone by
 *               470px, where English never wraps at all).
 *   .no-ads-line "No ads, no trackers, no cookies." 20px → uk/ru 40px,
 *               wrapping up to 480px.
 *   .learn-why  the 60,000-games line, 40px (English's own 2 lines already)
 *               → 60px in Russian's 3, but ONLY under ~410px — measured
 *               unchanged from 420px up.
 *
 * Same root cause as `.promise` above, same fix, scoped this time to the one
 * width band (390–480px, a phone) where any of the three actually happens:
 * a Slow-3G uk/ru reader sees the English prerender first, then the door
 * grows under them the moment the dictionary chunk lands. `min-height`
 * pinned to the tallest measured variant removes the move for every reader,
 * language and width in between (a phone between 431–480px reserves a few
 * spare pixels it never uses — the cost of one number covering three
 * separate, close-but-not-identical thresholds instead of three).
 *
 * ⚠️ 🆕 A FOURTH LINE, FOUND BY THE FLAKY BATTERY STEP ITSELF, NOT BY EYE
 * (r8w/w16fixd, круг 8) — `.whatsnew-text` (WhatsNewBanner.jsx, CR
 * "что нового" 2026-09-14/15) sits BETWEEN `.promise` and `.door-row` and is
 * ALSO run through `t()`, so it carries the exact same lazy-dictionary wrap
 * this whole block already fixes for the three lines below it — missed the
 * first time only because the banner did not exist yet when this block was
 * written. `door-cls-test.mjs`'s uk/ru-under-Slow-3G check kept reporting a
 * real, DETERMINISTIC ~0.018–0.020 CLS — reproduced bit-for-bit across a
 * dozen runs at load averages from 3.5 to 7.3, i.e. not machine noise — that
 * a `layout-shift` source dump traced to `#whatsNew` growing ~20px (uk) the
 * instant the dictionary lands, pushing `.door-row`/`.no-ads-line`/
 * `.learn-block` down by that same ~20px (real `previousRect`→`currentRect`
 * deltas, not the harmless zero-geometry instrumentation artifact this
 * file's own header names for the OTHER source of CLS on this scenario).
 * Measured at every width from 390 to 480px, `.whatsnew-text`'s own height
 * (English/Ukrainian/Russian): 390px 79.81/99.77/119.72px, 414px 59.86/
 * 99.77/99.77px, 431–480px 59.86/79.81/79.81px — worst case in this band is
 * Russian at 390px, 119.72px, so the reservation below is pinned there, same
 * convention as the three lines above it (round up, accept spare pixels at
 * the wider end of the band). */
@media (max-width:480px){
  .door-row #goSignup{ min-height:90px }
  .no-ads-line{ min-height:40px }
  .learn-why{ min-height:60px }
  /* ⚠️ 148px, NOT 120 (battery 53, 15.09): the 15.09 banner grew by one clause
     ("a free table-memory drill") and its uk/ru wording runs to seven lines at
     390px — 120px no longer covered the lazy uk/ru swap and the door's CLS on
     Slow 3G rose from 0.011/0.007 to 0.018/0.016 (limit 0.015). */
  .whatsnew-text{ min-height:148px }
}

/* ⚠️ THE PAGE'S OWN INDEX, not the site's bar. Eight long cards need a way to
   reach the eighth without scrolling past seven — that used to live in this
   page's private sticky navigation, and it went when the site grew one shared
   bar. Two rows of sticky chrome is one too many on a phone, so this sits in
   the page where a reader meets it once and moves on.
   ⚠️ 2026-09-13 — AND STAYS THAT WAY ON A PHONE. Above 720px the trade
   reverses: characters.html and gestures.html run 10,000–13,000px, the
   viewport is tall enough that a second, thinner bar under the site's own
   still leaves most of the screen for the cards, and a page that long with no
   persistent way to jump around (the UI/UX pass, finding 4) is the worse
   trade. `.ch-index-bar` is a plain, unstyled wrapper below 720px — the
   phone case is untouched — and becomes the sticky bar only above it. */
/* BUG-28 (owner, 16.09): the jump list is a plain block in the flow at EVERY
   width — no sticking, no hide-on-scroll, no dark box. It stuck above 720px
   and slid away on scroll, so a wide window and a narrow one behaved
   differently and the bar "twitched"; the tinted box drew the eye to a
   list that should read as chips on the page. The chips keep their own
   shells (rule 3 below). */
.ch-index-bar{ position:static; padding:4px 0; background:transparent; border:0; }
.ch-index{ display:flex; align-items:baseline; gap:12px; flex-wrap:wrap;
           max-width:var(--w-page); margin:0 auto 26px; padding:0 22px }
.ch-index-lead{ flex:0 0 auto; font-size:.78rem; letter-spacing:.05em; color:var(--bone-3) }
.ch-index-links{ display:flex; gap:14px; flex-wrap:wrap }
/* ⚠️ 44px, NOT 24. This is a tap target like any other on the site — the top
   bar's own links (.nav-links a) already hold 44px — and this one had never
   been measured against that bar, found in the same UI/UX pass. */
.ch-index-links a{ color:var(--brass); text-decoration:none; font-size:.86rem;
                   padding:6px 0; min-height:44px; display:inline-flex; align-items:center;
                   border-bottom:1px solid transparent }
/* (hover: the text turns brass, the shell does not move — rule 3 below) */

/* The way to the next chapter, at the foot of each one. It used to live in each
   page's own sticky bar and went when the site grew a single navigation — a
   reader who finishes a chapter wants the next, not a trip back to the
   contents. */
.ch-next{ display:flex; gap:18px; flex-wrap:wrap; max-width:var(--w-page);
          margin:34px auto 0; padding:18px 22px 0; border-top:1px solid var(--edge) }
.ch-next a{ color:var(--brass); text-decoration:none; font-size:.92rem;
            padding:6px 0; min-height:24px; display:inline-flex; align-items:center;
            border-bottom:1px solid transparent }
.ch-next a:hover{ border-bottom-color:var(--brass-2) }
/* ⚠️ gestures.html PUTS TWO .ghost BUTTONS IN HERE — second UI/UX pass,
   2026-09-13. `.ghost` (defined near the top of this file) gives itself
   padding:12px 20px and a border on all four sides, but `.ch-next a` above is
   more specific (class+tag beats one class) and was winning on two
   properties: the narrow `padding:6px 0` sat the label flush against the
   button's own edge, and the explicit `border-bottom` stayed transparent
   (var(--brass-2) on hover) while `.ghost`'s shorthand coloured the other
   three sides var(--edge) (var(--brass) on hover) — a box with one mismatched
   edge. characters.html's `.ch-next a` links carry no `.ghost` class, so
   this rule never reaches that page. */
.ch-next a.ghost{ padding:12px 20px; border-bottom-color:var(--edge) }
.ch-next a.ghost:hover{ border-bottom-color:var(--brass) }

/* ── CN characters — round 8 UI/UX pass (2026-09-13) ─────────────────────
   Four fixes for the Archetypes page, reported against a screenshot:
   1. .ch-key ran padding:0 at the top and 26px at the bottom, so the row of
      tags read as falling away from its own caption. One token, both sides.
   2. .ch-face (the sticky portrait) does nothing for navigation and, on a
      1440×900 window, ran to ~29% of the viewport height while pinned beside
      scrolling text. A portrait is decoration, not a tool a reader reaches
      for mid-scroll, so it returns to the normal flow. .ch-index-bar stays
      sticky — THAT is the thing a reader nine cards deep actually needs —
      capped to one row (never wraps, never grows) and it hides on the way
      down / returns on the way up so it can never sit over the paragraph a
      reader just scrolled to. Transform only, so nothing reflows; motion
      switched off under prefers-reduced-motion.
   3. Three treatments for the same idea — a short label about one archetype:
      a filled caps pill (.ch-verdict), a bare link (.ch-index-links a). One
      chip face for both: same background, border, padding, size, case: the
      only thing left to vary is text colour. The jump list also gets a real
      aria-current, kept in step with scroll position by characters.js.
   4. "WATCH FOR" was .64rem (~10px) — unreadable at either width this page
      ships. 14px clears the 13/14px floor at both 390 and 1440 without a
      media query, and the sign list itself now matches the paragraph above.
   6. The last box in every card (.stat.terse) was a third bordered/filled
      callout after two real ones, with nothing saying it was different. Its
      text is a narrower, more specific restatement of the stat above it (a
      detail, not a citation) — so it is a plain trailing sentence now. */

/* 1 */
.ch-key{ padding:16px 0; }

/* 2 */
@media (min-width:720px){
  .ch-face{ position:static; top:auto; }
}
/* BUG-37 (owner, 16.09): the lead ("Jump to one:") is a line of its own with
   air under it, and the list keeps a clear gap before the next heading —
   both used to sit on top of each other. */
.ch-index-lead{ flex:0 0 100%; margin-bottom:4px; }
.ch-index-bar{ margin-bottom:14px; }
@media (min-width:720px){
  /* however many rows 9 names wrap into at this width, it is a small
     fraction of even a short laptop window — measured up to 3 rows, ~120px,
     against a 25vh budget that starts at 175px. A hard cap here would only
     turn "wraps to two rows" into "half the list is unreachable". */
}

/* 3 — one shell for every chip: the verdict pills and the jump-list links */
.ch-verdict,
.ch-index-links a{
  background:rgba(179,166,146,.07); border:1px solid var(--edge-2);
  border-radius:20px; padding:5px 12px; font-size:.72rem;
  letter-spacing:.09em; text-transform:uppercase; font-family:"JetBrains Mono",monospace;
  white-space:nowrap;
}
.ch-verdict.read{ background:rgba(179,166,146,.07); border-color:var(--edge-2); color:var(--brass); }
.ch-verdict.loud{ background:rgba(179,166,146,.07); border-color:var(--edge-2); color:var(--bone); }
.ch-verdict.soft{ background:rgba(179,166,146,.07); border-color:var(--edge-2); color:var(--bone-2); }
.ch-verdict.flat{ background:rgba(179,166,146,.07); border-color:var(--edge-2); color:var(--blood-lit); }
/* ⚠️ FIX34-2 #2 — `.flat`'s OWN LABEL BROKE THE "short chip" ASSUMPTION.
   "Silence screams" / "is loud" / "is a murmur" are 15-20 characters;
   "Silence means nothing — it is their normal" (the flat verdict, both as
   the legend chip in `.ch-key` and as the per-card `<p class="ch-verdict
   flat">` tag) is 44 — and `white-space:nowrap` above, with no `max-width`,
   means an inline-block/inline-flex box renders at its full, unwrapped
   intrinsic width no matter how narrow the viewport is. Measured at 360px:
   the chip alone was 360px wide, 22px of it past the edge — the entire
   sideways scroll responsive-test.mjs catches on characters.html. The other
   three verdicts never hit this because they are short enough to fit
   nowrap even on a phone; `.flat` is the one word count nobody re-checked
   after BP's/BN's merge pinned every chip to one shared, nowrap shell.
   `.ch-index-links a` keeps nowrap — its labels are single words/short
   names and are meant to stay one line — only the verdict pill gets room to
   wrap, and a max-width tied to its own container keeps it from ever again
   claiming more than the column it sits in. */
.ch-verdict{ white-space:normal; max-width:100%; }
.ch-index-links a{
  color:var(--bone-2); text-decoration:none;
  display:inline-flex; align-items:center; min-height:44px;
}
/* BUG-36 (owner, 16.09: "a yellow line blinks under the chip for half a
   second when the mouse leaves"): the sitewide `a` rule animates border-color
   over .14s and paints the hover border brass; the old hover here set
   `border-bottom:none`, so on leave the bottom edge came back in the
   sitewide brass and faded to the chip's own colour — a blink. The chip now
   keeps its one shell on every side in every state and animates only the
   text colour. */
.ch-index-links a{ transition:color .14s; }
.ch-index-links a:hover,
.ch-index-links a:focus-visible,
.ch-index-links a[aria-current="true"]{ color:var(--brass); border-color:var(--edge-2); }

/* 4 */
.ch .watch{ font-size:1rem; }
.ch .watch b{ font-size:14px; }

/* 6 — a narrower, more specific restatement of the stat above it, not a
   citation, so it reads as the rest of the card's prose: same colour as
   .tell, one notch smaller than the two real callouts above it. */
.ch .stat.terse{
  margin:10px 0 0; padding:0; background:none; border:none; border-radius:0;
  color:var(--bone-2); font-size:.94rem; line-height:1.55;
}

/* ── Ответ на вопрос урока, на экране конца партии ───────────────────────────
   ⚠️ ЗНАК И ЦВЕТ ВМЕСТЕ. ✓ ✗ – читаются без цвета (WCAG 1.4.1); цвет лишь ускоряет.
   ⚠️ «НЕ БЫЛО» ПРИГЛУШЕНО, А НЕ КРАСНОЕ. Ситуация, которая не представилась, — не
   ошибка ученика, и красный здесь научил бы бояться партий без нужной ситуации. */
.lesson-check{ display:flex; gap:12px; align-items:flex-start; margin:0 0 14px;
  padding:12px 14px; border-radius:8px; border:1px solid var(--edge); text-align:left }
.lesson-check-mark{ flex:0 0 auto; font-size:1.25rem; line-height:1.3; font-weight:700; min-width:1.1em; text-align:center }
.lesson-check-title{ margin:0 0 3px; font-weight:600; color:var(--bone) }
.lesson-check-said{ margin:0; font-size:.9rem; line-height:1.55; color:var(--bone-2) }
.lesson-check.is-yes{ border-color:var(--brass-2); background:rgba(201,162,39,.07) }
.lesson-check.is-yes .lesson-check-mark{ color:var(--brass) }
.lesson-check.is-no{ border-color:rgba(180,38,44,.55); background:rgba(180,38,44,.07) }
.lesson-check.is-no .lesson-check-mark{ color:#E0666C }
.lesson-check.is-none .lesson-check-mark{ color:var(--bone-3) }

/* Тихая строка «пора повторить» под кнопкой школы: она появляется только когда
   что-то действительно просрочено, поэтому не должна выглядеть как ещё одна кнопка. */
.due-now{ display:inline-block; margin-top:6px; text-decoration:none; opacity:.85 }
.due-now:hover{ opacity:1; text-decoration:underline }

/* CR "что нового" (2026-09-14) — the door's own "what's new" line, between
   the promise and the door row (Start.jsx). A quiet card, same restraint as
   `.onb-checklist` just below (--wood-3, a brass-tinted edge, no fill of its
   own) — it is information, not a call to action, and must never compete
   with the one red Play button under it (RULE "one red button per screen"). */
.whatsnew{ display:flex; align-items:flex-start; gap:10px; max-width:400px;
  margin:16px auto 0; padding:10px 12px; border:1px solid var(--edge);
  border-radius:8px; background:var(--wood-3); text-align:left }
.whatsnew-text{ margin:0; flex:1 1 auto; font-size:.86rem; line-height:1.45; color:var(--bone-2) }
/* ⚠️ NOT RED — the ✕ closes a piece of information, it is not a second
   primary action. Same `.icon` grey as the in-game "leave" ✕ (Game.jsx's
   `#quit`), reused rather than inventing a second neutral colour. */
.whatsnew-close{ flex:0 0 auto; line-height:1; margin:-4px -4px 0 0 }

/* The door's rotating invitation (owner, 15 Sep 2026) — one line pointing at
   the free memory trainer or at the school, alternating between visits. See
   PromoBanner.jsx / promo.js. Same restraint as `.whatsnew` above: a quiet
   card, never a second red control beside the one Play button (RULE "one red
   button per screen"), and the call to action is a brass LINK, not a slab.

   ⚠️ THE SLOT RESERVES ITS OWN HEIGHT, AND THAT IS THE WHOLE POINT. The banner
   is dismissed for the rest of the day one commit AFTER hydration (promo.js's
   header records why it cannot be read during render), so without a reserved
   slot everything below it would jump on a device that closed it earlier —
   the CLS bug door-cls-test.mjs exists to catch. The slot keeps its box
   whether the card is inside it or not. */
.promo-slot{ min-height:64px; margin:16px auto 0; max-width:400px }
.promo{ display:flex; align-items:center; gap:10px;
  padding:10px 12px; border:1px solid var(--edge); border-radius:8px;
  background:var(--wood-3); text-align:left }
/* ⚠️ THE UNDERLINE LIVES ON AN INNER <span>, AND THE SITE'S LINK BORDER IS OFF.
   Two separate causes of the same ugly line, both found by looking at a 3x
   crop rather than at the CSS. The link is a flex item, which blockifies it,
   so a decoration on the link itself is painted across the whole box; and
   every link on this site carries a `border-bottom` as its underline, which on
   a blockified link is drawn once, full width, under the LAST line — the text
   ended mid-way and the line kept going. An inline span underlines the words
   themselves, on both lines, and the border is turned off here only. */
.promo-cta{ flex:1 1 auto; font-size:.86rem; line-height:1.45; color:var(--brass);
  text-decoration:none; border-bottom:0; min-height:24px }
.promo-cta span{ text-decoration:underline; text-underline-offset:3px }
/* ⚠️ NOT RED, same reasoning as `.whatsnew-close` above it. */
.promo-close{ flex:0 0 auto; line-height:1; margin:-4px -4px 0 auto }
@media (max-width:420px){ .promo-slot{ min-height:88px } }

/* w10-onboard (AF-3b, round 8) — the first-session checklist widget. A card,
   not a banner: it sits under the door's own secondary links and must never
   read as loud as the one red Play button above it (RULE "one red button").
   No fill of its own beyond the room colour one level up (--wood-3) and a
   quiet brass-tinted border, same restraint `.lesson-check` uses for the
   same reason. */
.onb-checklist{ max-width:400px; margin:22px auto 0; padding:14px 16px;
  border:1px solid var(--edge); border-radius:8px; background:var(--wood-3); text-align:left }
.onb-title{ margin:0 0 8px; display:flex; align-items:baseline; justify-content:space-between;
  gap:10px; font-size:.92rem; font-weight:600; color:var(--bone) }
.onb-count{ font-size:.78rem; font-weight:400; color:var(--bone-3); white-space:nowrap }
.onb-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:8px }
.onb-item a, .onb-item .onb-btn{ display:flex; align-items:center; gap:9px; text-decoration:none;
  color:var(--bone-2); font-size:.86rem; min-height:24px; width:100%; text-align:left;
  background:none; border:0; padding:0; font:inherit; cursor:pointer }
.onb-item a:hover, .onb-item .onb-btn:hover{ color:var(--bone) }
.onb-mark{ flex:0 0 auto; width:1.1em; text-align:center; color:var(--bone-4) }
.onb-item.is-done .onb-mark{ color:var(--brass) }
.onb-item.is-done a, .onb-item.is-done .onb-btn{ color:var(--bone-3) }
.onb-hide{ color:var(--bone); /* touch-test: the inherited --bone-3 measured 3.17:1 on this card */ display:inline-block; margin-top:12px; padding:0; background:none; border:0;
  font:inherit; text-decoration:underline; cursor:pointer; font-size:.78rem }
.onb-hide:hover{ color:var(--bone-2) }

/* ── the checklist's own loading placeholder (BUG-140, owner, 18.09) ────────
   Same idiom as `.skel-bar`/`.skel-block`'s own comment a few screens up
   (BUG-129, Progress.jsx): the SAME classes the real card uses
   (`.onb-checklist`/`.onb-title`/`.onb-list`/`.onb-item`/`.onb-mark`/
   `.onb-hide`), carrying no real text, so `.onb-item a, .onb-item .onb-btn`'s
   own `min-height:24px` a few lines up gives the placeholder row and the real
   row the identical box — swapping the HTML in Start.jsx once boot() answers
   changes nothing about the height. */
.onb-skel-count{ display:inline-block; width:5.5em; height:.86em }
.onb-item .onb-skel-label{ display:block; width:70%; height:.86em }
.onb-skel-hide{ display:inline-block; width:3.4em; height:.78em; margin-top:12px }

/* ── gesture pictograms — gestures.html (gesture-art.js) ──────────────────
   Schematic line-art sitting NEXT TO the photographic art rather than
   replacing it: the photo is what a table looks like, the pictogram is what
   the rulebook — or a confirmed convention — actually specifies. Kept in one
   colour, no fill, so it never competes with the sepia portraits above it. */
.g-svg{ color:var(--bone-3); flex:0 0 auto; }
.g-picto{ margin-top:10px; }
.g-wrap{ display:flex; flex-wrap:wrap; align-items:center; gap:16px; margin:18px 0; max-width:100%; }
.g-wrap > *{ min-width:0; }
.g-wrap p{ margin:0; font-size:.98rem; line-height:1.6; color:var(--bone-2); }
.g-wrap p b{ color:var(--bone); }
/* ⚠️ NEVER nowrap here. The don card's caption is a full sentence; nowrap made it
   580px wide inside a 220px column and the whole gestures page scrolled sideways
   by 300px on every phone (responsive-test, battery 16). Short cites wrap to
   nothing; long ones wrap to two lines. */
.g-cite{ font-size:.78rem; color:var(--bone-3); }
.g-law{ margin:8px 0 40px; padding:22px 24px; border:1px solid var(--brass-2); border-radius:12px; background:rgba(201,162,39,.05); }
.g-law > p{ margin:0 0 6px; font-size:.98rem; line-height:1.6; color:var(--bone-2); }
.g-pair{ display:flex; gap:22px; flex-wrap:wrap; margin:18px 0 10px; }
.g-pair figure{ margin:0; display:flex; flex-direction:column; align-items:center; gap:8px; width:150px; }
.g-pair figcaption{ font-size:.84rem; color:var(--bone-2); text-align:center; line-height:1.45; }
.g-unconfirmed{ opacity:.68; }
.g-unconfirmed .g-svg{ color:var(--bone-3); }
.g-fork{ display:flex; gap:20px; flex-wrap:wrap; align-items:flex-start; margin:10px 0; }
.g-fork figure{ margin:0; display:flex; flex-direction:column; align-items:center; gap:6px; width:130px; }
.g-fork figcaption{ font-size:.82rem; color:var(--bone-3); text-align:center; line-height:1.4; }

/* ═══ BUG-49 — Женя's review, relayed by the owner 16.09 ══════════════════════
   "Make all body text the secondary colour. Do not single anything out."
   Her example: the gestures lede, where one sentence sat in bright bold
   inside a paragraph of muted prose. Every paragraph and list item of the
   document pages reads in --bone-2; a <b>/<strong> inside them keeps its
   place in the sentence but no longer changes colour or weight. Headings,
   labels, links, buttons and the game screens are untouched. */
body.doc:not(.poster) main p, body.doc:not(.poster) main li, body.doc:not(.poster) main dd,
body.doc:not(.poster) .ch-dek, body.doc:not(.poster) .ch-lede, body.doc:not(.poster) .ch-body p, body.doc:not(.poster) .g-law p{ color:var(--bone-2); }
body.doc:not(.poster) main p b, body.doc:not(.poster) main p strong, body.doc:not(.poster) main li b, body.doc:not(.poster) main li strong,
body.doc:not(.poster) main dd b, body.doc:not(.poster) main dd strong{ color:inherit; font-weight:inherit; }
/* (the two print pages — body.poster — are ink on white and keep their own colours) */
/* …and a chapter title in the guide's list is a LINK LABEL, not emphasis in
   prose: --bone at rest, --brass on hover/focus (guide-links-test), exactly
   as school.css's own rule says — restated here at the specificity the
   blanket rule above would otherwise beat. */
body.doc:not(.poster) main .guide-list a b{ color:var(--bone); font-weight:600; }
body.doc:not(.poster) main .guide-list a:hover b,
body.doc:not(.poster) main .guide-list a:focus-visible b{ color:var(--brass); }

/* BUG-50 (owner, 16.09): the legal pages exist in five languages — two of them
   (Français, Deutsch) for the law, not for the product: Quebec's Charter of
   the French language and Consumer Protection Act want the contract in
   French, a German buyer expects it in German. The site's own switcher
   knows only the three languages the game speaks, so the other versions are
   offered right under the heading, on every one of the five, not buried
   after the section index (or missing, as on uk/ru). */
.legal-langs{ display:flex; flex-wrap:wrap; gap:4px 12px; align-items:baseline; margin:-8px 0 20px; font-size:.9rem; color:var(--bone-3); }
.legal-langs span{ margin-right:2px; }

/* BUG-97 (owner, 17.09: "Learn to read a table should have a gap above and
   below, and the same for the difficulty buttons"). The Learn button sat 22px
   under the free line and the puzzle link hugged it; the difficulty control
   had its note 8px under it. Both controls now carry air on both sides. */
#start #goLearn.big-play{ margin:30px auto 26px }
#start #diff{ margin:32px auto 18px }
#start #diff + p, #start #diff + .sub-hint{ margin-top:0 }

/* BUG-98 (owner, 17.09): a visitor without an account sees Progress and
   Settings dimmed — a reason on hover, a click goes to sign-in — not hidden, so the bar keeps its width and nothing jumps when they sign in. The
   class comes from guest.js in <head>, before the first paint; api.js keeps it
   in step on sign-in/out. */
html.guest .nav-mine{ opacity:.45; cursor:default }
html.guest .nav-mine:hover{ opacity:.8 }

/* ── the "Report a problem" tab (r8w/w16, owner 17.09.2026) ───────────────
   "the product is a startup with bugs — a small persistent tab on the side of
   every screen". src/client/report-tab.js mounts it; this is everything about
   how it looks.

   ⛔ NOT RED (RULE 16 — one red control per screen, and red is the colour of
   the thing that ends something). Brass on wood, the site's quiet secondary.

   ⚠️ position:fixed, SO IT CONTRIBUTES NOTHING TO LAYOUT. It is appended to
   <body> after the first paint and must move nothing when it arrives —
   report-tab-browser-test.mjs measures total layout shift after first paint
   against the same 0.01 bar w19-1-look-test.mjs §6 holds every page to.

   ⚠️ IT SITS UNDER THE SHEET IT OPENS. `.sheet` is z-index 80; this is 40, so
   the dialog and its backdrop cover the tab rather than leaving a brass sliver
   floating over a modal.

   ⚠️ AND IT IS CENTRED VERTICALLY, NOT PINNED TO A CORNER. A corner is where a
   phone's own gestures live (the iOS home indicator, Android's back swipe) and
   where floating chat widgets have trained everybody to expect an advert. */
.report-tab{
  position:fixed; right:0; top:50%; transform:translateY(-50%); z-index:40;
  writing-mode:vertical-rl; text-orientation:mixed;
  width:32px; padding:16px 0; border:1px solid var(--edge); border-right:0;
  border-radius:5px 0 0 5px; background:var(--wood-2); color:var(--bone-2);
  font-family:inherit; font-size:.72rem; letter-spacing:.12em; text-transform:uppercase;
  cursor:pointer; box-shadow:-6px 0 18px -10px rgba(0,0,0,.9);
  /* Never taller than the screen it is pinned to, on any phone or in any
     language — vertical text in Ukrainian is the longest of the three. */
  max-height:78vh; overflow:hidden;
}
.report-tab:hover{ color:var(--brass); border-color:var(--brass-dim); }
/* The site's own focus ring, not the browser's — and never `outline:none`. */
.report-tab:focus-visible{ outline:2px solid var(--brass); outline-offset:-2px; color:var(--brass); }
/* ⛔ BUG-118 (owner, 17.09.2026) — REVERSES BUG-111's ONE EXCEPTION. "the
   same tab as every other page" — the board was the last screen with no way
   to report a problem, for the same reason the lesson/result/Settings
   screens no longer have their own button: where you can report a bug must
   not depend on where you are standing. The tab keeps its usual position
   (right edge, vertically centred, z-index 40 — under `.sheet`'s 80) and
   gets no exception here any more.
   ⚠️ THE BOARD MAKES ROOM FOR IT INSTEAD OF HIDING IT. `.board` is the grid
   that holds both the seat list/transcript AND `.act` (`.act{grid-column:
   1/-1}` spans the same content box) — padding the RIGHT edge of that one
   container shifts all three (the seat strip, the transcript and its own
   scrollbar, and the action bar's buttons) left by the tab's width, so
   nothing the board draws ever shares a pixel with the tab's box. The
   header's ✕ needs no such lane: `.bar` is a fixed 56px/50px strip pinned to
   the TOP (see `.bar` above), and the tab is centred at mid-height, so the
   two are never close at any of the sizes this is measured at
   (report-tab-browser-test.mjs). */
body.playing .board{ padding-right:40px }
/* Paper has no buttons. */
@media print{ .report-tab{ display:none !important; } }
@media (max-width:420px){
  .report-tab{ width:28px; font-size:.66rem; padding:12px 0; }
  body.playing .board{ padding-right:36px }
}

/* The honeypot, off-screen rather than display:none — some autofillers already know
   to skip a hidden field, none of them skip one that is merely far away. The
   same rule contact.html carries inline; report-problem.js's own copy of the
   field needs it on every page the tab can open, which is all of them. */
.hp-field{ position:absolute; left:-9999px; top:auto; width:1px; height:1px; overflow:hidden; }
