/*
Theme Name: Elsteiner Summer
Theme URI: https://elsteinergames.com
Author: Elsteiner Games
Description: Successor to elsteiner-2026. v1 block theme; design system authored Tier-1 onward per docs/areas/website/ia/.
Version: 0.7.41
Requires at least: 6.4
Requires PHP: 8.0
Text Domain: elsteiner-summer
*/

/* The Version above is the cache-busting `?ver=` on EVERY asset functions.php
   enqueues — tokens.css, web-blocks.css, web-blocks-local.css and eg-web.js all
   read `wp_get_theme()->get( 'Version' )`. Bump it with any change to those
   files or returning visitors keep the cached copy and the deploy is invisible.
   0.6.5 — horizontal-scroll edge cue: web-blocks-local.css gains the mask and
   eg-web.js the state that drives it, so a stale pair would leave the CSS
   waiting on a data attribute nothing sets.
   0.6.7 — el/score-calculator at 360px: web-blocks-local.css stacks the seat
   row and lifts the radio and caption sizes. CSS-only, but a returning phone
   is exactly the visitor who would otherwise keep the broken cached copy.
   0.6.10 — F12 table bottom-border override (below) + F24 heading scale
   (theme.json): raised h1/h2/h3 to fluid clamp() sizes, and fixed the real
   reason they read too small in the first place — theme.json's
   styles.elements.{h1,h2,h3} hand-typed var(--wp--preset--font-size--h1)
   etc, but WP kebab-cases numeric-suffixed slugs when generating the
   preset variable, so the actual custom property is
   --wp--preset--font-size--h-1 (confirmed on staging's
   global-styles-inline-css). The old references pointed at a variable that
   never existed, so h1/h2/h3 never picked up a theme.json font-size at
   all — same bug for the paired line-height custom properties. This file
   also carries its own ?ver=, so it needs the same bump discipline as the
   files above.
   0.6.11 — Printables unit (feedback 2026-08-19, F8): :has() opt-out so the
   score-sheets .eg-dlgrid escapes the shared game_extra template's reading
   measure, plus the intro/card-row gap. CSS-only, same cache-busting
   reasoning as every entry above.
   0.6.12 — batch fix after first staging deploy (feedback 2026-08-19):
   rules TOC on the live game_extra rules route (reading-measure reach-
   through; the F12 table-border rule already covered it via the
   pre-existing inc/eg-prose-wrap.php wrap, verified live, not changed),
   archhero-to-grid/facets gap on /games/, /puzzles/, /decks/, dlgrid/
   actions-secondary margin-block + 3-up columns on score-sheets, footer
   newsletter heading scoped back down off the theme.json h2 default,
   home "All seven games" heading padding-breakpoint parity with the
   grid/hero below it. CSS + templates only, same cache-busting
   reasoning as every entry above.
   0.7.0 — MINOR, not a patch: the 2026-08-19 coder handoff (LOCKED web
   library) lands as a design-system step, not a fix. tokens.css and
   web-blocks.css are replaced wholesale from the drop; the web type scale
   becomes FLUID (T2 ruling, scale C: h1 40px at 360 to 54px at 1280,
   display 40-68, h2 24-34, h3 20-25, lead 19-24) and theme.json carries the
   same clamps with the designer's cqw written as vw, replacing the interim
   4vw/3vw/2vw clamps 0.6.10 shipped. N6-page-head arrives as the ONE page
   top (.eg-pagehead, supersedes the .eg-archhero band); F2's game strip
   gains a linked red value and a two-up grid below 640; the mark law's
   struck name is retired (.eg-facet--none .eg-facet-name is now
   `color: inherit`, so a no-results option is quiet ink and a dead pointer
   and nothing else). Every one of those is a returning-visitor cache risk
   on its own, and the header logo silently grows from 20px to 25px on
   desktop because .eg-logo-text rides --eg-w-fs-h3 (deliberately NOT
   pinned, per the T2 logo-rider-B ruling).
   0.7.4 — Slovenian presentation merge (Greg's ruling 2026-08-20, ticket S):
   the three-player game stops being a page and becomes a ?players=3 mode of
   /games/slovenian-tarok/, so the published count goes 7 to 6 and the
   templates here lose a card each. NO CSS in this file changed — the switch
   reuses the A2 facet chip wholesale and its placement rules live in
   el-blocks/blocks/single-game/style.css, cache-busted by that block's own
   block.json version (0.1.0 to 0.2.0). The bump is here because
   front-page.html and games-archive.html changed their copy ("Seven games,
   one deck" to "Six", "All seven games" to "All six", the "7 games" eyebrow
   to "6 games") and a returning visitor holding a cached page should not be
   the last one still counting seven.
   0.7.5 — the C family lands (drop R4b, 2026-08-22, ticket CT): C1 contract
   table, C2 scope filter, C3 contract disclosures. assets/design/web-blocks.css
   was re-cut from the drop's blocks.css with scripts/split_web_bundle.py and
   grew by exactly 50 rules, all of them .eg-ctw* / .eg-scopes* / .eg-cdetail*
   (0 removed, 0 changed — the rest of the layer is byte-for-byte what 0.7.0
   installed, and tokens.css did not move at all). NO CSS in this file changed:
   the block ships its own two-rule style.css for the one thing the designer
   layer cannot carry (a filtered <tbody> hidden with [hidden] needs to beat
   the below-640 `display: block` stacking rule). The bump is here because
   web-blocks.css itself is enqueued from this file's ?ver= and a returning
   reader holding the cached copy would get the new markup with none of its
   styling.
   0.7.7 — the Z family lands as A8/A9 (drop R4d, 2026-08-22, ticket PB):
   A8 category-band, A9 category-intro. The /puzzles/ archive and every
   per-game puzzles page now build their five type bands from these units
   instead of the interim F22/F23/F15 markup: an A9 head (live COUNT, the
   ratified one-sentence LINE from eg_puzzle_type_meta(), an ALL link) atop
   an A8 row that SAMPLES the type at most 8 cards deep with a tail plate,
   never the whole type. assets/design/web-blocks.css was re-cut from the
   drop's blocks.css with scripts/split_web_bundle.py: 26 selectors added
   (.eg-qband*, .eg-qintro*, .eg-qmore*), 0 removed, 0 changed — nothing
   outside the Z family moved. tokens.css gains exactly one property inside
   its existing .eg-web block, --eg-w-qcard (21rem, measured not rounded);
   the drop's own tokens.css carries a much wider unrelated diff (print-
   layer type scale / heading-spacing tokens already ratified 2026-08-19
   but never installed here) that is NOT taken by this bump — flagged in
   DROP-INTEGRATION-STATUS.md instead, since it rides on a different
   ratification than this ticket's write set covers. The bump is here
   because web-blocks.css and tokens.css are both enqueued from this
   file's ?ver=. Interim .puzzle-grid__band* and .game-puzzle-list__type-group*
   block CSS retired (blocks/puzzle-grid/style.css,
   blocks/game-puzzle-list/style.css) along with the markup it styled; the
   interim .eg-cmp-scroll trailing-edge fade is gone from these rows with
   it (MANIFEST.md law: a faded live link reads as a disabled one).
   0.7.10 — PM coder update of 2026-08-30 applied (ticket CU). Two halves.
   (a) The book masters are re-run through scripts/build_book_content.py, so
   derived/book-content/ and el-blocks/assets/book-content/ pick up the three
   Calling the King contract renames (Farbendreier "No-Trump", Farbensolo
   "No-Trump Solo", Sechserdreier "Blind Solo" — "Blind Six" and "Grand
   No-Trump Solo" are gone), King Ultimo announced by the called King's
   holder, the Forehand's opening pass, Strawman losing its 35:35 draw and
   Calling the Twenty losing "forced Solo" from Expansions.
   EG_BOOK_IMPORT_VERSION goes 5 -> 6 in the same commit because that content
   is baked into post_content at import and cannot self-heal.
   (b) Difficulty leaves every reader-facing puzzle surface (ruling §2: the
   reader sees Example or Puzzle, never a pip or the words easy/tricky).
   blocks/game-puzzle-list/render.php drops the eg-badge--diff dot badge from
   the A7 card foot; blocks/puzzle-board/render.php drops the eg-wpuzzle-diff
   dots from the read-reveal bar and the five-star el-stars mark from the
   legacy header, and stops reading _puzzle_difficulty entirely. The meta and
   its admin column/metabox in includes/cpt-puzzle.php are untouched — it is
   internal editorial data. The GAME facts strip keeps its own difficulty
   dots (§7 leaves that ruling alone); this is a different mark on a
   different surface. No CSS changed: .eg-badge--diff / .eg-wpuzzle-diff /
   .el-stars rules stay in place, unreferenced by these two blocks, because
   .el-stars is still used by the _partials preview. The bump is here because
   this is a reader-facing markup change on the puzzle pages and a cached
   copy would keep showing the retired pips.
   0.7.11 — Example/Puzzle tier lands (ticket TR, PM ruling 2026-08-30 §2,
   item 2 unblocked). scripts/build_book_content.py derives a fixed
   'example'|'puzzle' tier per puzzle straight from its source difficulty
   string (beginner-friendly/easy -> example, real-play/tricky -> puzzle;
   numeric fallback 1 -> example, 2/3 -> puzzle) into puzzles.json;
   inc/book-content-import.php carries it to _puzzle_tier meta
   (EG_BOOK_IMPORT_VERSION 6 -> 7, since existing puzzle rows otherwise pass
   the unchanged-content check and never pick up the new field).
   includes/cpt-puzzle.php gets the one shared reader, eg_puzzle_tier() (falls
   back to _puzzle_difficulty when _puzzle_tier isn't written yet), and
   eg_puzzle_tier_label(). blocks/puzzle-board/render.php's read-reveal shell
   splits on it: Example renders the answer open beside the question with no
   <details>/reveal control at all; Puzzle keeps exactly today's closed
   <details> reveal (so the analytics reveal event, untouched, only ever
   fires on Puzzles now — correct per ruling, Examples have nothing to
   reveal). Both blocks/puzzle-board/render.php's eg-wpuzzle-bar and
   blocks/game-puzzle-list/render.php's eg-qcard-foot gain a second eg-badge
   showing the tier word beside the existing id/type badge. Existing
   .eg-badge / .eg-web styling only, no new CSS. Regenerated bundle:
   120 example / 216 puzzle across 336 puzzles.
   0.7.12 — DROP-full-library-2026-08-30 taken (ticket FL). Standing
   web/design-delivery/web-library/ refreshed from the drop (fixes the
   stale, pre-R5b tokens.css MANIFEST flags as stale-list item 1; the
   standing blocks.css was also a leftover combined book+web copy, replaced
   by the drop's clean pre-cut web bundle). assets/design/web-blocks.css
   re-cut via scripts/split_web_bundle.py: 681 rules, 1 selector added
   (.eg-web table td strong + em, the native-names-on-tables rule), 0
   removed, K1 hand centring (.eg-web .eg-hand justify-content: center)
   confirmed present, matching MANIFEST exactly. The drop's tokens.css
   carries a much wider diff (R5b book-scale restore + four sticky-colour
   tokens + C10 vign-below), all confirmed .eg-book-scoped and unreferenced
   by web-blocks.css (MANIFEST says as much); not taken into this theme's
   tokens.css, print-layer work out of this ticket's scope. Eleven
   corrected card-preview faces re-pulled and normalised (card-back,
   clubs-cavalier, diamonds-1, hearts-1/2, hearts-king, spades-10/queen,
   tarok-9/13/18); same paths, no template change. Known discrepancy
   (not fixed, per MANIFEST + ticket): the drop's own web/puzzle-card.html
   specimen still shows a DIFFICULTY dots slot; the site stays ahead of it
   (0.7.11's tier badges, zero pips), confirmed unchanged by this drop via
   PHP harness on blocks/game-puzzle-list/render.php.
   0.7.35 — the F1 secondary tiles burst out of their own grid tracks at
   tablet width. The library fixes the column count at four past its 640
   container query, but the primary tile takes about half that row, so the
   four tracks compute to 94px while a label needs about 110px; the tiles then
   overflowed their borders and the container. 0.7.34's fifth tile made it
   plainly visible. The column count now follows the width, and because
   auto-fit collapses unused tracks, 1280 still shows all five in one row.
   Scoped to the same 640 container tier the library's own four-column rule
   uses: unscoped it also reached the narrow tier and collapsed the phone
   layout from two tiles per row to one, which measurement caught.
   0.7.34 — the ruled name lands (coder addendum 2026-09-17, section D6,
   standing since 2026-08-30). Reader copy says scorekeeper, and the route is
   /games/{id}/scorekeeper/ with a permanent redirect from the old
   /calculator/ address. The printed book was the risk: six pointer targets in
   assets/book-pointers.json resolved to /calculator, and a printed QR goes
   through that map, so the map moved in the same commit and nothing printed
   breaks. Existing game_extra posts migrate their slug on scaffold sync
   (version 23). Also section D4: every game hub now links to its variants
   page, labelled "More rules and expansions", which is the label the designer
   is moving to in round R10, and house law C5 applied to two of our own
   strings that claimed the site holds "the full" catalogue. Section D2: a
   review notice baked into a page is now hidden from readers and still shown
   to editors, so /traditional-scoring/ stops opening with a DRAFT box.
   0.7.33 — page furniture was trapped in the reading measure (ticket W-E).
   One root cause, five symptoms: the extras hub's four action tiles rendered
   87px wide with their labels painting outside the tile borders; the score
   sheets' download grid dropped to two tracks and wrapped the third card;
   the About pillars sat in 186px columns; and page titles hung 206px left of
   the body under them. .eg-actions and .eg-pillars now join .eg-dlgrid's
   existing :has() escape from the 44rem prose cap, and the two units that
   declare their own --eg-w-page-max are bled past main's padding so that cap
   can actually bind. Running prose keeps its measure — only furniture moved.
   The N6 band also reaches the four pages that were each inventing their own
   title treatment (extras, about, dictionary, glossary), which closes the
   title/body edge break as a side effect. Last arrow dingbat gone from 404.
   0.7.32 — W-F. The glossary shipped literal markdown asterisks in six terms
   (the R3 parser ran definitions through the inline-markdown converter but
   not the terms, so a native word italicised in the master arrived as
   "(*Koenigrufen*)"), and all 64 terms were red underlined links pointing at
   their own row — clicking one did nothing. Terms now render their emphasis
   and are plain ink per the library; the row keeps its id so existing deep
   links still resolve. Score sheets: the "Also for this game" row was a bare
   secondary strip borrowing the E3 chooser's label class, with two tiles both
   typed reference; it is a complete F1 unit now (primary "Read full rules",
   then calculator and puzzles) and the pending-cheat-sheet note no longer
   touches the row below it. EG_BOOK_SCAFFOLD_VERSION 22.
   0.7.31 — the rules pages were unreadable and their navigation unreachable
   (ticket W-C). Paragraph rhythm: the design layer's rule is a CHILD
   combinator (.eg-prose > * + *) but WordPress inserts an .entry-content
   wrapper, so every paragraph on an 11,661px page had 0px of separation and
   headings butted their first paragraph; the rhythm now reaches through the
   wrapper using the library's own --eg-w-sp-5. Sticky TOC: body carried
   overflow-x:hidden, which per spec forces overflow-y:auto and made BODY the
   nearest sticky ancestor instead of the viewport, so the TOC scrolled away
   and left a 288px column empty for 11,000px; html already guards the
   horizontal axis, so the body rule is gone and the TOC clamps at 32px.
   Mobile gutter: the page-head's 360 tier used a different spacing token
   from every body unit (36px vs 52px); one token now. Tap targets brought to
   the library's 44px minimum: TOC summary and links, scope chips, a band's
   "all" link, deck game names — padding only, type sizes untouched.
   0.7.30 — second Teams harvest pass: each chapter's Cheat Sheet ladder is
   read as well as its core ladder, which gives zwanzigerrufen a Teams column
   it did not have (3 of 8 contracts). Contracts with no structured data
   anywhere in the masters keep an honestly empty cell — koenigrufen's eight
   advanced contracts have no Teams field in any chapter section, so no value
   was invented for them. EG_BOOK_IMPORT_VERSION 13.
   0.7.29 — regression sweep of 23 pages x 2 widths after the fix wave.
   Two residues closed: the /puzzles/ landing still emitted its page-head
   band as <header> (the block half of the 0.7.26/0.7.27 landmark fix — the
   single-game and single-deck bands stay <header> because theirs sit inside
   an <article>, where that is correct); and the About page's one em dash
   turned out to live in the SSOT at ssot/company/brand.json (a positioning
   pillar's summary), not in the page, so it is fixed at source and the
   bundle re-synced. Book-side, reported not patched: prose arrows survive in
   generated content ("Normal Game > Farberl > Solo" on /start20/, "(void >
   play a Tarok)" on rules pages) — the masters own that wording.
   0.7.28 — the contract table gains a TEAMS column (2 vs 2 / 1 vs 3 / all
   vs all). W-A's duplicate-table fix had removed the chapter ladder that
   carried it, and no surface reproduced it — while the PM ruling of
   2026-08-30 makes it load-bearing ("the teams column on every contract
   table carries the difference" between a partner No-Trump and a solo one).
   The data is now harvested from the chapter's own ladder into a new
   contracts-meta.json bundle asset and joined to rows by SSOT contract id,
   never by row order; a contract with no harvested value renders an empty
   cell rather than a guess, and a game whose ladder carries no team data
   (zwanzigerrufen) renders no column at all. EG_BOOK_IMPORT_VERSION 12.
   0.7.27 — the el/page-head BLOCK also emitted its band as <header>, giving
   every game_extra page a second banner landmark (the archive templates were
   fixed in 0.7.26; this is the dynamic-block half of the same defect). A
   <header> whose nearest sectioning root is <body> is a banner, and this band
   sits directly in <main>. It is a page title, so it is a <div> now.
   0.7.26 — two residues from 0.7.25's frame work, caught on live re-measure.
   (a) The /games/ and /decks/ archive templates rendered the page-head band
   with tagName "header", so every archive carried a SECOND banner landmark
   beside the site header; the band is a page title, not a site banner, so it
   is a plain group now. (b) The band still floated 24px below the header:
   0.7.25 cancelled main's 48px padding but not the 24px margin-block-start
   WordPress's constrained layout puts on main itself.
   0.7.25 — the design-review fix wave (tickets H2, W-A, W-B). H2, theme
   frame: header, main and footer now share ONE frame (main was on 72rem
   while every other .eg-web frame used --eg-w-page-max 1180px, which is why
   the logo hung left of the H1); .eg-feature bleeds edge to edge like the
   page head, so its red rule reads as a section edge instead of a floating
   stripe; game cards are white again (a stale AA hotfix used a substring
   selector, [class*="game-card"], that clobbered .eg-gcard's own
   --eg-card-white); duplicate banner/contentinfo landmarks removed (every
   template-part carried tagName, so WP wrapped the part's own <header> in a
   second one) and the duplicate id="newsletter"; the newsletter privacy
   link now passes AA at 4.97:1 (it lost to .eg-footer a on source order and
   rendered paper-on-paper at 1.11:1); the page-head band meets the header
   instead of floating under 48px of white. W-A, content: the rules pages
   printed the contract table TWICE (the chapter's own ladder table beside
   the C1 block) — suppressed at build time; native parentheticals can no
   longer break across three lines; puzzle titles drop the em dash and stop
   calling an Example a Puzzle ("Calling the King: Example C1"). W-B, game
   page: contract group headings carry the house band rule (2px ink, upright
   caps) instead of an invented italic label, and their names now match the
   rules page (English-leading, book spelling).
   0.7.24 — arrow dingbats removed from reader-facing copy. The design
   library uses NO arrows in any web template (its pager unit is explicit:
   "Plain links; 44px minimum target"), yet the puzzle prev/next nav shipped
   left/right arrows, the game_extra breadcrumb shipped a left arrow, and
   the community hub shipped "Browse " + arrow. Words carry it: the
   breadcrumb now reads "Back to {Game}". Also gives that breadcrumb link
   the library's 44px minimum target (it measured 17px tall live).
   0.7.23 — the footer newsletter form is now the library's G8 email-CTA
   unit (ticket H1) instead of a hand-rolled lookalike: bordered paper-2
   panel, 48px controls (was 21px browser default, below the 44px touch
   minimum), Inter uppercase label on --eg-track-caps, and the house privacy
   line, which the lookalike omitted entirely. view.js follows G8's own
   structure (STATUS is a sibling of the form, not a descendant).
   0.7.22 — page-reference stripping left visible wreckage in reader prose.
   A page ref never stands alone in the masters: it is parenthesised, fenced
   by commas, or introduced by a preposition. The build stripped only the
   "p. XX" token, so every rules page shipped "are at the back,, for tables"
   and koenigrufen shipped "the contract table on tells you what each
   contract takes"; the glossary shipped orphaned trailing commas after
   "See also:". scripts/build_book_content.py now removes each wrapper WITH
   its reference (longest form first) and tidies doubled/orphaned commas;
   unit-checked against all three shapes plus two no-op sentences. Content
   regenerated, EG_BOOK_IMPORT_VERSION 10 forces the re-import.
   0.7.21 — the newsletter block's stylesheet was never enqueued: its
   block.json declared render + viewScript but no `style` key, so
   blocks/newsletter-signup/style.css (which exists and is correct) never
   loaded. Two live consequences on every page's footer form, both found in
   the 2026-08-30 critical homepage review: the form rendered as raw browser
   chrome (21px inputs against the 48px --eg-control-h token), and the
   ANTI-BOT HONEYPOT — hidden by that stylesheet with position/left, never
   display:none — rendered as a visible 153px text field directly above the
   real email input, so a human filling it had their signup silently
   rejected as bot traffic. Both block.json copies now declare the style.
   0.7.20 — HOTFIX for 0.7.18: the SEOPress social filters
   (og/twitter title + description) echo their value VERBATIM, so the bare
   strings 0.7.18 returned printed as visible text above the site header on
   every route seo-meta.php owns (home, games, decks, glossary, puzzles) —
   caught by Greg on the home page, "lines without context in the top".
   Social filters now return complete <meta> tags; the titles_* filters keep
   taking bare strings, which is their own separate contract.
   0.7.19 — crawl rev 3 wave 2 (tickets R4/R5). R4: slovenian facts strip
   stamps the inactive mode hidden server-side (the "Players 4 3" no-JS
   double-print); the engine-dump Summary paragraph (game.json description)
   is off every game page — the Greg-supplied standfirst carries that job;
   contract-band derivation unified into inc/contract-bands.php (the 3p/4p
   bucket split is genuine SSOT variant data and stays). R5: contract
   reader-prose map extended to all six games (26 new entries, 3 documented
   book-side gaps); slovenian native-vs-Simplified VP split captioned at
   page and per-contract level per the book's own ruling; difficulty glyphs
   become eg-dot markup at build time (meta-description leak closed);
   EG_BOOK_IMPORT_VERSION 9 forces the re-import.
   0.7.18 — external crawl rev 2/3 wave 1 (tickets R1/R2/R3 + orchestrator
   fix-ups). R1: SEO title/canonical/description fixed on virtual routes
   (games, decks, glossary, puzzles archive) via SEOPress filters — root
   cause was template_include routing that never set query flags, so
   SEOPress saw every virtual route as the blog index; em dashes retired
   from reader-facing site copy (10 strings). R2: contract-table reader
   prose replaces SSOT engine notes (koenigrufen map, hard leak filter for
   all games), C2 scope filter now removes bands server-side (works with
   JS off, chips are plain links), English-leading contract names. R3:
   /glossary/ now syncs from the book's 10-glossary.md (59 entries) instead
   of the engine vocabulary; /dictionary/ drops VERIFY/placeholder/dash
   rows; per-game puzzle pages drop the vestigial same-page ALL links.
   Orchestrator: advanced contract badges on game pages now link with
   ?scope=advanced (the default rules view no longer shows that band), and
   the merged "Advanced / expansion contracts" heading reads "Advanced
   contracts" (no expansion-scope contract exists).
   0.7.17 — CP: Greg's 14 coder pointers (2026-08-30) applied to
   el/score-keeper-koenigrufen, CALC-LAB-C2 ported (drop
   drop-calculator-lab-c2-2026-08-30). Sheet now prints ONE VP number per
   player per hand (CON/BON are entry-only fields on the Record-a-hand
   form) with a bold red-rule TOTAL foot row; no pre-printed empty rows;
   empty-state copy lifted from the lab; exactly two actions (Record a
   hand / New game), entry form closed by default; "First to 50 victory
   points" fact removed from the block; live per-row Hand VP while
   typing; a − sign-toggle button beside every numeric field plus typed
   minus, always rendering the true minus sign (U+2212); cells sized for
   a signed three-digit number, right-aligned, tabular figures; buttons
   are 4px radius (never pill), primary red-deep / secondary ink outline,
   Cancel never primary; sheet cells are DISPLAY only (paper table,
   hairline rules, paper-2 head/foot bands, 1.6px red head rule); below
   640px the sheet restructures into per-player strips, two up (lab's
   .c2-strips copied wholesale), zero horizontal scroll at 360px with
   four players and −215. Values now reference library tokens
   (var(--eg-…)) instead of the lab's standalone copies. The block lives
   in el-blocks (own block.json version 0.1.0 → 0.2.0, its own cache-bust
   path); this theme-changelog entry is the record of intent per ticket
   CP, not a claim that this file's ?ver= gates the block's CSS.
   0.7.16 — the theme stylesheet was DEAD since 0.7.7: the 0.7.7 changelog
   prose contained selector globs with the character pair star-slash
   (".eg-qband" + star + slash + ...) which terminates a CSS comment early; the exposed
   prose mis-parsed and the browser dropped EVERY rule in this file
   (styleSheets entry: 0 cssRules, verified via CDP on staging). Fixed by
   rewriting the two prose lines without the pair. Never write bare glob
   chains like x-star-slash-y-star inside CSS comments. Also: the revived 0.7.7-era
   .eg-pagehead bleed needed !important physical margins to beat WP
   constrained layout's margin auto !important on game_extra tops; the
   redundant 0.7.15 duplicate rule is removed in favour of the original.
   0.7.15 — N6 page-head full bleed (ruled 2026-08-19, T1a: "the paper-2
   band paints full bleed"). WP constrained layout was boxing .eg-pagehead
   to the content column (900px on game_extra, 1200px on singles/archives)
   so the band read as a rectangle around the headline (Greg, 2026-08-30).
   Theme reach-through only — the design layer is untouched: container-unit
   bleed (body.eg-web is the inline-size container; cqw excludes the
   scrollbar where vw would not), max-width freed from WP's clamp. The
   formula self-cancels when the parent is already full width.
   0.7.14 — score-keeper polish after Greg's live review: legend copy
   simplified (CON "Contract points", BON "Bonus points" — the made/failed
   and trull/ultimo/valat wording belonged to the deleted rules engine);
   the "＋ Record a hand" button now toggles to "Cancel" while the entry
   panel is open, so only one live action path shows at a time.
   0.7.13 — score keeper rebuilt to a dumb sheet (Greg's ruling 2026-08-30,
   ticket SK). el/score-keeper-koenigrufen: setup now asks only the four
   player names (no contract-table question, no book/house mode split);
   recording a hand asks CON and BON per player and the sheet sums them to
   VP, replacing the deleted declarer picker, contract dropdown, card-point
   readout and bonus checkboxes. render.php's config shrinks to
   game/name/seats/sessionEnd — no more embedded SSOT contract table.
   view.js's .eg-kc-table drops rowspan (one row per hand, three cells per
   player) and gains data-label attributes so style.css can reflow it into
   stacked hand cards below 640px with zero horizontal scroll, instead of
   forcing the old fixed 34rem min-width. NO CSS in THIS file changed — the
   block ships its own style.css, cache-busted independently, and its
   view.js is cache-busted via its own index.asset.php version
   (egsk010 -> egsk011). The bump is here per ticket SK's instruction to
   keep this file's version counter moving with the feedback round (0.7.12
   already taken by ticket FL).
   0.7.8 — /puzzles/ becomes a landing (Greg's ruling, 2026-08-22, ticket
   PZ, supersedes 0.7.7's category-destination IA on the archive): the
   0.7.7 A8/A9 wiring stays live on the per-game pages, uncapped (no
   sample, no tail plate, tail hrefs repointed from a /games/{id}/puzzles/
   {type}/ route that never existed to a same-page #type-{slug} anchor);
   el/puzzle-grid (blocks/puzzle-grid/render.php) drops its A2 facet rail,
   A3 chips, A4 result bar, and all A8 card rows and instead lists one A9
   intro entry per game with a published puzzle, in the book's order,
   linking to that game's own puzzles page — plus a new type legend
   (eg-deflist, the same markup el/glossary-list already emits) so
   Decision/Exchange/Play/Counting/Ruling are explained before a reader
   hits them. No CSS in web-blocks.css or tokens.css changed: every class
   the new markup uses (eg-qband, eg-qintro*, eg-deflist) already existed
   for this exact purpose, and blocks/puzzle-grid/style.css and
   blocks/game-puzzle-list/style.css only lose dead rules for markup
   render.php no longer emits. The bump is here anyway because this is a
   reader-facing markup change on the site's most-linked page (a cached
   /puzzles/ copy would keep showing the retired facet rail) — same
   cache-busting reasoning as every prior entry.
   0.7.6 — the E family lands (drop R4c, 2026-08-22, ticket PR): E1
   download-card, E2 downloads-grid, as amended by Greg's generic-icon
   ruling the same day. assets/design/web-blocks.css was re-cut from the
   drop's blocks.css with scripts/split_web_bundle.py: 4 selectors added
   (the eg-dl--form/eg-dl--ref thumb variants), 1 removed
   (.eg-dl-thumbpending, no longer needed once every card always has a
   glyph), 4 changed (.eg-dl base padding, .eg-dl-thumb/-thumbimg
   simplified to full-width object-fit:contain, .eg-dl-body gains its own
   horizontal padding, .eg-dlgrid's track sizing). The old 640-and-up
   side-by-side @container block for .eg-dl/.eg-dl-body is gone outright
   (the card stacks at every width now). Nothing outside the .eg-dl and
   .eg-dlgrid selectors moved; tokens.css did not change. Also added assets/design/miniatures/
   sheet-generic.svg (new asset directory, SVG only, no normalize_web_assets
   budget needed) and retired two interim theme overrides this drop
   obsoletes (see "Printables unit" below for the line-by-line justification
   of what stayed and what left). The bump is here because web-blocks.css
   itself is enqueued from this file's ?ver=.
   0.6.13 — verifier findings: archhero gap rule needed body.eg-web prefix
   (web-blocks.css's later `.eg-web .eg-grid { margin: 0 auto }` tied and
   won on order — measured margin-top 0 on /decks/ and /games/); rules TOC
   desktop open state moved to view.js (closed <details> content cannot be
   shown by CSS; Chromium uses content-visibility). */


/* ------------------------------------------------------------------------
   Base — mobile-first defaults.
   ------------------------------------------------------------------------ */

/* Border-box everywhere — without this, blocks that combine width:100% with
   their own padding (e.g. WP inner alignfull groups) overflow their parent
   at narrow viewports. Critical for 360px mobile. */
*, *::before, *::after {
  box-sizing: border-box;
}

html {
  font-size: 16px;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  /* Belt-and-braces: prevent any rogue child from forcing a horizontal
     scrollbar on the document. Deliberately the ONLY overflow-x guard in
     this pair (see the removed body rule below, W-C finding 2) — the
     root element's own overflow, when not visible, is what the UA
     propagates to the real viewport scrollport per spec; that is enough
     on its own for the belt-and-braces goal. */
  overflow-x: hidden;
}
body {
  margin: 0;
  font-family: var(--wp--preset--font-family--serif, Cormorant Garamond, Georgia, serif);
  font-size: 17px;
  line-height: 1.6;
  color: var(--wp--preset--color--ink, #1a1a1a);
  background: var(--wp--preset--color--paper, #faf8f3);
  /* W-C finding 2 (blind review 2026-08-30): this body-level overflow-x:
     hidden was redundant with html's own (above) for the horizontal-
     scrollbar guard, but NOT harmless: per the CSS overflow spec, when
     one axis is non-visible the other computes to auto, so body's
     overflow-y silently became `auto` too — making BODY itself (whose
     box height auto-fits its content, so it has zero internal scroll
     range) the nearest "scrolling ancestor" for every position:sticky
     descendant on the page, ahead of the real viewport. That's why
     .eg-rules-toc (blocks/rules-toc/style.css) never stuck: verified live
     on koenigrufen/rules/ — getBoundingClientRect().top tracked
     window.scrollY 1:1 (391.9 / -2608.1 / -5608.1 at scrollY 0/3000/6000,
     an exact -3000/-3000 delta = static, not sticky) before this rule was
     removed; after removal (isolated test via an injected stylesheet,
     confirmed only then written here), top clamped to the intended 32px
     at every scrollY. html's own overflow-x:hidden above still guards
     against a horizontal scrollbar (confirmed no regression: document
     scrollWidth stayed <= clientWidth after this change), since the
     UA propagates the root element's own non-visible overflow to the
     viewport regardless of what body declares. max-width: 100vw (kept)
     is a plain box constraint, not an overflow declaration, so it does
     not re-create the scroll-container bug. */
  max-width: 100vw;
}

/* Site title (header logo) — display serif. */
.wp-block-site-title {
  font-family: var(--wp--preset--font-family--display, Playfair Display, Georgia, serif);
  font-weight: 700;
  letter-spacing: -0.01em;
}
.wp-block-site-title a {
  color: var(--wp--preset--color--ink, #1a1a1a);
  text-decoration: none;
}

/* Navigation — sans-serif. */
.wp-block-navigation {
  font-family: var(--wp--preset--font-family--sans, Inter, system-ui, sans-serif);
  font-weight: 500;
}
.wp-block-navigation a {
  color: var(--wp--preset--color--ink, #1a1a1a);
  text-decoration: none;
}
.wp-block-navigation a:hover {
  text-decoration: underline;
}

/* Headings — display serif. */
h1, h2, h3, .wp-block-heading {
  font-family: var(--wp--preset--font-family--display, Playfair Display, Georgia, serif);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
  margin: 1.5em 0 0.5em;
  text-wrap: balance;
}
h1, .wp-block-heading[class*="has-text-align-"] { margin-top: 0; }

/* Body links. */
a {
  color: var(--wp--preset--color--red, #b22222);
  text-underline-offset: 2px;
}

/* ------------------------------------------------------------------------
   Layout container — keeps content readable at all viewports.
   ------------------------------------------------------------------------ */

main.wp-block-group.alignfull > .wp-block-group:not(.alignfull),
main.wp-block-group.alignfull > * {
  /* Constrain top-level main content to 1200px and add side padding */
  max-width: 1200px;
  margin-left: auto;
  margin-right: auto;
}

/* Wide-screen: more breathing room. */
@media (min-width: 1500px) {
  main.wp-block-group.alignfull > * {
    max-width: 1200px;
  }
}

/* Mobile padding for any direct main child that doesn't have explicit padding. */
@media (max-width: 768px) {
  main.wp-block-group.alignfull > *,
  main.wp-block-group.alignfull {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }
}

/* ------------------------------------------------------------------------
   Header / footer (parts/header.html, parts/footer.html) — already use
   alignfull + their own padding; don't double up.
   ------------------------------------------------------------------------ */

header.wp-block-group.alignfull,
footer.wp-block-group.alignfull {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding-left: 1.25rem;
  padding-right: 1.25rem;
}
@media (min-width: 768px) {
  header.wp-block-group.alignfull,
  footer.wp-block-group.alignfull {
    padding-left: 2rem;
    padding-right: 2rem;
  }
}
header.wp-block-group > .wp-block-group {
  max-width: 1200px;
  margin-left: auto;
  margin-right: auto;
}

/* ------------------------------------------------------------------------
   Mobile nav — hamburger toggle.
   The Navigation block's mobile overlay needs visible toggle styling.
   ------------------------------------------------------------------------ */

@media (max-width: 768px) {
  .wp-block-navigation__responsive-container-open,
  .wp-block-navigation__responsive-container-close {
    color: var(--wp--preset--color--ink, #1a1a1a);
  }
  .wp-block-navigation__responsive-container.is-menu-open {
    background: var(--wp--preset--color--paper, #faf8f3);
  }
}

/* ------------------------------------------------------------------------
   Pillars block — 3-column cards on desktop, stacked on mobile.
   Markup: section.el-pillars > ul.el-pillars__list > li.el-pillars__card
   ------------------------------------------------------------------------ */

section.el-pillars {
  width: 100%;
  margin: 0 auto;
}
ul.el-pillars__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1.5rem;
  grid-template-columns: 1fr;
}
@media (min-width: 720px) {
  ul.el-pillars__list {
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem;
  }
}
li.el-pillars__card {
  padding: 1.75rem 1.5rem;
  background: var(--wp--preset--color--paper, #faf8f3);
  border: 1px solid var(--wp--preset--color--ink-3, #e0d8c8);
  border-radius: 8px;
  list-style: none;
}
.el-pillars__card-title {
  font-family: var(--wp--preset--font-family--display, Playfair Display, Georgia, serif);
  font-size: 1.25rem;
  font-weight: 700;
  margin: 0 0 0.5rem;
  color: var(--wp--preset--color--ink, #1a1a1a);
}
.el-pillars__card-summary {
  font-family: var(--wp--preset--font-family--serif, Cormorant Garamond, Georgia, serif);
  font-style: italic;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--wp--preset--color--ink-2, #444);
  margin: 0 0 1rem;
}
ul.el-pillars__card-points {
  list-style: disc;
  margin: 0;
  padding-left: 1.25rem;
  font-family: var(--wp--preset--font-family--sans, Inter, system-ui, sans-serif);
  font-size: 14px;
  line-height: 1.6;
  color: var(--wp--preset--color--ink-2, #444);
}
ul.el-pillars__card-points li {
  margin-bottom: 0.25rem;
}

/* ------------------------------------------------------------------------
   Brand hero — big tagline + centered CTAs.
   ------------------------------------------------------------------------ */

.el-brand-hero {
  padding: 3rem 1.25rem 2.5rem;
  background: var(--wp--preset--color--paper, #faf8f3);
  text-align: center;
  box-sizing: border-box;
  width: 100%;
  overflow-wrap: break-word;
}
@media (min-width: 768px) {
  .el-brand-hero {
    padding: 6rem 2rem 4rem;
  }
}
.el-brand-hero__inner {
  max-width: 800px;
  margin: 0 auto;
  padding: 0 0.5rem;
  box-sizing: border-box;
}
.el-brand-hero__tagline {
  font-family: var(--wp--preset--font-family--display, Playfair Display, Georgia, serif);
  font-size: clamp(1.6rem, 6vw, 4rem);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin: 0 0 1rem;
  color: var(--wp--preset--color--ink, #1a1a1a);
  text-wrap: balance;
  overflow-wrap: break-word;
  hyphens: auto;
}

/* Mobile: ensure long tagline never overflows the viewport. */
@media (max-width: 480px) {
  .el-brand-hero__tagline {
    font-size: 1.5rem;
    line-height: 1.2;
    letter-spacing: -0.01em;
  }
  .el-brand-hero {
    padding: 2rem 1rem 1.5rem;
  }
  .el-brand-hero__inner {
    padding: 0;
  }
}
@media (max-width: 380px) {
  .el-brand-hero__tagline {
    font-size: 1.35rem;
  }
}

/* Force every direct child of <main alignfull> to respect viewport width. */
main.wp-block-group.alignfull,
main.wp-block-group.alignfull > * {
  box-sizing: border-box;
  max-width: 100%;
}
.el-brand-hero__mission {
  font-family: var(--wp--preset--font-family--serif, Cormorant Garamond, Georgia, serif);
  font-size: clamp(1.05rem, 2vw, 1.25rem);
  font-style: italic;
  color: var(--wp--preset--color--ink-2, #444);
  margin: 0 auto 2rem;
  max-width: 600px;
  line-height: 1.5;
}
.el-brand-hero__ctas {
  display: inline-flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  justify-content: center;
}
.el-brand-hero__cta {
  display: inline-block;
  padding: 0.75rem 1.5rem;
  font-family: var(--wp--preset--font-family--sans, Inter, system-ui, sans-serif);
  font-size: 15px;
  font-weight: 500;
  text-decoration: none;
  border-radius: 6px;
  transition: transform 0.1s ease;
}
.el-brand-hero__cta:hover { transform: translateY(-1px); }
.el-brand-hero__cta--primary {
  background: var(--wp--preset--color--ink, #1a1a1a);
  color: var(--wp--preset--color--paper, #faf8f3);
}
.el-brand-hero__cta--secondary {
  background: transparent;
  color: var(--wp--preset--color--ink, #1a1a1a);
  border: 1px solid var(--wp--preset--color--ink-3, #ccc);
}

/* ------------------------------------------------------------------------
   Featured game — card with name + meta + flavor + CTA.
   ------------------------------------------------------------------------ */

.el-featured-game {
  text-align: center;
  padding: 2rem 1.25rem;
}
.el-featured-game__inner {
  max-width: 720px;
  margin: 0 auto;
}
.el-featured-game__label {
  font-family: var(--wp--preset--font-family--sans, Inter, system-ui, sans-serif);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--wp--preset--color--ink-3, #888);
  margin: 0 0 0.5rem;
}
.el-featured-game__name {
  font-family: var(--wp--preset--font-family--display, Playfair Display, Georgia, serif);
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  font-weight: 700;
  margin: 0 0 0.5rem;
}
.el-featured-game__players {
  font-family: var(--wp--preset--font-family--sans, Inter, system-ui, sans-serif);
  font-size: 14px;
  color: var(--wp--preset--color--ink-2, #444);
  margin: 0 0 1rem;
}
.el-featured-game__flavour {
  font-family: var(--wp--preset--font-family--serif, Cormorant Garamond, Georgia, serif);
  font-style: italic;
  font-size: 1.1rem;
  color: var(--wp--preset--color--ink-2, #444);
  margin: 0 0 1.5rem;
  line-height: 1.5;
}
.el-featured-game__cta {
  display: inline-block;
  padding: 0.6rem 1.25rem;
  font-family: var(--wp--preset--font-family--sans, Inter, system-ui, sans-serif);
  font-size: 14px;
  font-weight: 500;
  background: var(--wp--preset--color--ink, #1a1a1a);
  color: var(--wp--preset--color--paper, #faf8f3);
  border-radius: 6px;
  text-decoration: none;
}

/* ------------------------------------------------------------------------
   Game-card grid — games archive shows multiple cards.

   F4/F25 root cause: .eg-grid is a `wp:group` block rendered by WP core as
   `is-layout-flow` (its className points at a hand-written CSS-grid rule in
   web-blocks.css rather than a block-level "layout":{"type":"grid"}
   attribute), so WP's global styles still inject
   `:root :where(.is-layout-flow) > * { margin-block: 24px 0 }` on every
   child except the first (verified live: matched rule on the 2nd/3rd
   .eg-gcard, absent on the 1st). Under `display:grid` that pushes row 1's
   2nd/3rd column siblings down 24px while the true first child (Strawman
   on /games/) keeps margin-top:0 and sits flush against the archive header
   band above it — off the baseline the rest of its row sits on. The
   grid's own `gap` (web-blocks.css) already provides consistent spacing,
   so drop WP's injected flow margin for grid children entirely, on both
   /games/ (.eg-gcard) and /decks/ (.eg-dcard) grids.

   The old `.el-game-card` / `.wp-block-el-game-card` rules that used to
   live in this section are removed, not kept alongside the fix: el/game-
   card's own co-located style.css already documents them as dead ("the
   old .el-game-card* rules matched no markup this block emits any more")
   — except `.wp-block-el-game-card` itself, which get_block_wrapper_
   attributes() still emits on the real <a class="eg-gcard …">, so its
   `display:block` rule was silently overriding web-blocks.css's
   `.eg-gcard { display:flex }` the whole time (higher specificity: two
   classes under `main.wp-block-group >` beats `.eg-web .eg-gcard`).
   ------------------------------------------------------------------------ */

.eg-grid > .eg-gcard,
.eg-grid > .eg-dcard {
  margin-block: 0;
}

/* Batch fix item 2 (feedback 2026-08-19): the SAME margin-block:0 fix above
   removed the only vertical gap that used to sit between .eg-archhero (the
   header band, .eg-web .eg-archhero in web-blocks.css — no bottom margin/
   padding of its own) and the card grid or facet rail directly below it —
   /games/ and /decks/ use .eg-grid, /puzzles/ (el/puzzle-grid) uses
   .eg-facets next. Both are now flush against the band. Adjacent-sibling
   selector, no template change needed. */
/* Specificity note (verified live 2026-08-19): web-blocks.css loads AFTER
   this file and sets `.eg-web .eg-grid { margin: 0 auto; ... }` (0,2,0),
   which tied and beat the original `.eg-archhero + .eg-grid` (0,2,0) on
   order — computed margin-top was 0 on /decks/ and /games/. Prefix with
   body.eg-web (0,2,1) so the gap actually applies; horizontal auto
   centering from web-blocks.css is preserved because only margin-top is
   set here. */
body.eg-web .eg-archhero + .eg-grid,
body.eg-web .eg-archhero + .eg-facets {
  margin-top: var(--eg-w-sp-5, 2rem);
}

/* Batch fix item 2: .eg-archhero (the band's own box, background-color)
   has no width rule of its own — it's simply 100% of <main>'s available
   width — while its child .eg-archhero-inner centers ITS content at
   max-width:var(--eg-w-page-max) with its own horizontal padding. Because
   --eg-w-page-max (1180px, tokens.css) never actually binds inside this
   theme's ~1088px main content column, .eg-archhero-inner's real limiting
   factor is that same padding, which .eg-grid's cards share too — so the
   coloured band background reads as wider than the text/cards it frames by
   exactly that padding, on both sides (most visible on the right, against
   the grid's last card). Background-color paints the full border-box
   regardless of padding, so padding alone can't shrink what's visible —
   shrink the box itself (width + auto margins) by the same inset instead,
   then pull .eg-archhero-inner back out by that same amount so its own
   padding lands the TEXT exactly where it already sat: only the visible
   background box moves, not the copy. Same tokens/breakpoint as
   .eg-archhero-inner / .eg-grid (web-blocks.css) so the two stay in
   lock-step at every width. */
/* body.eg-web prefix on both: web-blocks.css's own `.eg-web .eg-archhero-
   inner` rule (2 classes) already sets `margin: 0 auto`, so a same-
   specificity override here would lose on source order alone — the extra
   type selector matches this file's existing override convention (see the
   game-card / puzzle-teaser sections above) rather than reaching for
   !important. */
body.eg-web .eg-archhero {
  width: calc(100% - 2 * var(--eg-w-sp-4, 1rem));
  margin-inline: auto;
}
body.eg-web .eg-archhero-inner {
  margin-inline: calc(var(--eg-w-sp-4, 1rem) * -1);
}
@container eg-web (min-width: 640px) {
  body.eg-web .eg-archhero { width: calc(100% - 2 * var(--eg-w-sp-5, 1.5rem)); }
  body.eg-web .eg-archhero-inner { margin-inline: calc(var(--eg-w-sp-5, 1.5rem) * -1); }
}
/* The two .eg-archhero blocks above now serve ONE page: the (currently
   drafted) blog index, el/blog-archive-hero. /games/, /decks/, /puzzles/ and
   the game and deck singles moved to N6-page-head below on 2026-08-19. They
   stay because A1 is still a live block in the library, not because anything
   in this theme's shipped nav reaches it today. */

/* ------------------------------------------------------------------------
   N6 page head — the ONE page top (drop 2026-08-19, T1a, LOCKED tier).
   ------------------------------------------------------------------------ */

/* The ruling is "paper-2 band paints FULL BLEED across eg-web; text keeps the
   grid left edge" — the exact opposite of what the A1 band above does, where
   we SHRANK the band to stop it out-reaching the cards. So the band escapes
   the content column instead, and the inner puts the text back on the grid.

   Both halves are box arithmetic against this theme's own content column
   (`.eg-web .wp-site-blocks > main.wp-block-group`, further up this file:
   max-width 72rem, margin-inline auto, padding-inline clamp(1rem,4vw,2.5rem)),
   not a new layout:

   BAND. main is centred with symmetric padding, so the horizontal centre of
   any block inside it is the viewport centre; `margin-inline: calc(50% - 50vw)`
   therefore lands the band's left edge at 0 and `width: 100vw` carries it to
   the far edge, at every viewport, whether the page top is a direct child of
   main (the archive templates) or sits inside the block's own <article>
   (el/single-game, el/single-deck, el/puzzle-grid). max-width:none is needed
   because WP's constrained layout caps every child at contentSize.

   TEXT. The inner then re-creates the column the cards sit in: the same 72rem
   cap, and padding = main's own inline padding PLUS the sp-4/sp-5 the
   design system already gives .eg-grid / .eg-pagehead-inner. Because 72rem
   (1152px) is below both --eg-w-page-max (1180) and WP's contentSize, the
   grid's own caps never bind and the two left edges agree at every width:
   below 1152 both are padding + sp-N from the viewport edge, above it both
   are (100vw - 1152)/2 + padding + sp-N.

   Only padding-inline is set, so the drop keeps ownership of the band's
   vertical rhythm (sp-6/sp-5, and sp-7/sp-6 in its own >=640 container rule).
   body.eg-web prefix for the usual source-order reason: web-blocks.css loads
   after this file and its own `.eg-web .eg-pagehead-inner` is the same
   two-class specificity. */
/* When the page head IS the first thing in main, the band must meet the
   header. 0.7.25 cancelled main's 48px padding-top, but WordPress's own
   constrained-layout rule (:root :where(.is-layout-constrained) > * {
   margin-block-start: 24px }) still pushed main itself down, leaving a white
   strip above a full-bleed band. Measured live 2026-08-30: header bottom 65,
   band top 89. Dynamic blocks (single-game, single-deck) wrap the band one
   level inside their own wrapper, so both shapes are matched. */
body.eg-web .wp-site-blocks > main.wp-block-group:has(> .eg-pagehead:first-child),
body.eg-web .wp-site-blocks > main.wp-block-group:has(> :first-child > .eg-pagehead:first-child) {
  margin-top: 0;
}

/* F1 secondary tiles: column COUNT must follow the available width, not be
   fixed. The library pins four columns once the container passes 640
   (web-blocks.css, the eg-web 640 container query). At 768 the primary tile
   already takes about half the row, leaving the secondary grid roughly 410px,
   so four tracks compute to 94px each while a label like "Scorekeeper" has a
   min-content width near 110px: the tiles then burst out of their own tracks
   and past the container, which is visible on every game hub. Adding the fifth
   tile (the variants link, addendum section D4) made it worse, so the floor is
   set here rather than left to the library. auto-fit collapses the tracks it
   does not need, so 1280 still renders all five tiles in one row. Scoped to
   the same 640 container tier the library's four-column rule uses, so the
   narrow tier keeps its two-up layout. */
@container eg-web (min-width: 640px) {
  body.eg-web .eg-actions-secondary {
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  }
}

body.eg-web .eg-pagehead {
  max-width: none;
  /* cqw, not vw: the eg-web container's width is the real layout width,
     so the bleed lands exactly on the viewport edges instead of
     overhanging by half the scrollbar (measured -7.5px with 50vw). */
  width: 100cqw;
  /* physical longhands + !important: WP constrained layout puts
     margin-left/right: auto !important on direct children of a layout
     container (game_extra tops), which beats any non-important margin. */
  margin-left: calc(50% - 50cqw) !important;
  margin-right: calc(50% - 50cqw) !important;
  /* The mobile media rule below (main > * padding at <=768px) would pad
     the band's own box on archive templates, shifting the inner text 20px
     right of the grid. The band's text inset belongs to -inner alone. */
  padding-inline: 0;
}

/* H2 finding B (2026-08-30, coordinator addendum): "the paper-2 band paints
   full bleed" (N6's own RULED note above) reads as a band that starts the
   page, not one floating 72px below it. Header bottom to .eg-pagehead top
   measured 65/137 live; a local tokens.css+web-blocks.css+style.css harness
   reproducing header + main + pagehead (post finding-1/-4 fixes, so a
   single, unwrapped <header>) isolates the remaining piece to exactly
   main's own inline `padding-top: var(--wp--preset--spacing--7)` (48px,
   theme.json) — .eg-pagehead sat at main's own top padding edge with
   `margin-top: 0`, confirmed gap 48px before this rule, 0 after. Only
   templates whose FIRST rendered thing inside <main> is the page-head band
   lose their top inset — decks-archive.html / games-archive.html /
   single-game_extra.html render .eg-pagehead as main's direct child;
   el/single-deck and el/single-game (render.php) wrap it one level deeper
   in the block's own get_block_wrapper_attributes() element first (verified
   by reading render.php, not assumed), so the selector is a DESCENDANT
   combinator, not `>` — still `:first-child` so it only fires when
   .eg-pagehead is the first thing in ITS OWN parent (the true page top),
   not some unrelated nested occurrence. All five share the same 48px main
   padding-top, so one token-driven rule covers every one of them without a
   template-by-template edit. Templates with no page-head band (front page,
   plain pages, prose singles) never match `:first-child` here and keep the
   top spacing they already have. */
body.eg-web main.wp-block-group.alignfull .eg-pagehead:first-child {
  margin-top: calc(-1 * var(--wp--preset--spacing--7, 48px));
}

/* Measured fix 2026-08-20 (verifier round 2, second pass): the band text
   column mirrors the CONTENT GRID's left edge, which is main's column
   (1200px cap + main padding) PLUS .eg-grid's own inline padding. Both
   components are viewport-dependent: main padding is spacing-6 above
   768px and 1.25rem below (this file's own media query); the grid padding
   is eg-w-sp-4 below the 640px container width and sp-5 above. Rule order
   matters: the >=769px media rule is written LAST so it wins the 640-768
   overlap against the container rule.

   W-C finding 3 (blind review 2026-08-30): the base tier's target no
   longer holds live — re-measured on koenigrufen/rules/, koenigrufen/,
   /games/ and /decks/ at 360px, every one of them the same way: the page
   title sat at x=36 while every body-content unit beneath it (.eg-gcard,
   .eg-dcard, .wp-block-paragraph in .eg-prose, .eg-actions,
   .el-contract-index — a different block on every page) sat at x=52, a
   consistent 16px step, mobile-only (1280px already matches at x=89 on
   both, untouched). This is universal across templates, not one page's
   local wrapper, so the general rule below is the right scope. 52px =
   main's own real padding-left (32px — WP's per-block "constrained
   layout" style, not this file's 1.25rem child rule, wins the cascade on
   <main> itself) + the theme's standing 1.25rem local inset every
   body-content wrapper still carries on top of it. Swapping this tier's
   second addend from eg-w-sp-4 (16px) to the same
   --wp--preset--spacing--6 (32px) token the >=769px tier below already
   uses reproduces that 52px exactly (measured after: title and body both
   x=52) without inventing a new constant. The >=640px container-query
   tier is untouched — out of this finding's measured scope. */
body.eg-web .eg-pagehead-inner {
  max-width: 1200px;
  padding-inline: calc(1.25rem + var(--wp--preset--spacing--6, 2rem));
}
@container eg-web (min-width: 640px) {
  body.eg-web .eg-pagehead-inner {
    padding-inline: calc(1.25rem + var(--eg-w-sp-5, 1.5rem));
  }
}
@media (min-width: 769px) {
  body.eg-web .eg-pagehead-inner {
    padding-inline: calc(var(--wp--preset--spacing--6, 2rem) + var(--eg-w-sp-5, 1.5rem));
  }
}

/* Same gap the A1 band needed, for the same reason (.eg-pagehead declares no
   bottom margin of its own, and .eg-grid's `margin: 0 auto` in web-blocks.css
   ties on specificity and wins on order without the body prefix). */
body.eg-web .eg-pagehead + .eg-grid,
body.eg-web .eg-pagehead + .eg-facets {
  margin-top: var(--eg-w-sp-5, 2rem);
}

/* Carried forward by hand from the designer's own web bundle: the 2026-08-19
   drop used to ship one combined blocks.css, and scripts/split_web_bundle.py
   kept only selectors mentioning .eg-web, so the designer's two `html` rules
   (`scrollbar-gutter: stable` and the P1-P4 prompt scroll lock) were dropped
   on the way in. Since 2026-10-18 web-blocks.css is a verbatim copy of the
   canon web bundle and carries both rules itself; this declaration stays as
   the theme's own statement of the invariant (same value, so it is
   idempotent). scrollbar-gutter is load-bearing for the full-bleed band above:
   without it a page that gains a scrollbar reserves no gutter and 100vw
   overshoots the viewport by the scrollbar's width (body's overflow-x:hidden
   clips it, but the band still reads off-centre). The prompt lock
   (`html.eg-prompt-open { overflow: hidden }`) is inert until something sets
   that class; it belongs with P1-P4 when they are wired. */
html { scrollbar-gutter: stable; }

/* ------------------------------------------------------------------------
   H2 finding 2 (2026-08-30, blind design review) — .eg-feature full bleed.
   ------------------------------------------------------------------------ */

/* web-blocks.css gives `.eg-feature` no width rule of its own (only
   `.eg-feature-inner` gets `max-width: var(--eg-w-page-max); margin: 0
   auto`), so as a plain child of <main> it inherited main's own box —
   1180px + main's own clamp() inset (see the "Global content column" rule
   above) — and its 3px top rule read as a short stripe with paper visible
   past both ends instead of running the full width of the viewport, same as
   N6 .eg-pagehead did before that band got its own bleed treatment. SAME
   technique, reused rather than reinvented: escape main's column with
   `width: 100cqw` + physical `margin-left/right: calc(50% - 50cqw)
   !important` (cqw not vw for the same scrollbar-gutter reason the pagehead
   comment above documents; !important to beat WP constrained layout's
   `margin-left/right: auto !important` on direct main children). No
   padding-inline reset is needed here (unlike .eg-pagehead): .eg-feature
   itself declares no padding of its own — only .eg-feature-inner does, and
   that inner column already runs on the same `var(--eg-w-page-max)` frame
   finding 1 put main back onto, so it keeps the text where it already sat. */
body.eg-web .eg-feature {
  max-width: none;
  width: 100cqw;
  margin-left: calc(50% - 50cqw) !important;
  margin-right: calc(50% - 50cqw) !important;
}

/* ------------------------------------------------------------------------
   Rule-list (rules page).
   ------------------------------------------------------------------------ */

.el-rule-entry {
  padding: 1rem 0;
  border-bottom: 1px solid var(--wp--preset--color--ink-3, #ddd);
}
.el-rule-entry:last-child { border-bottom: none; }
.el-rule-entry__title {
  font-family: var(--wp--preset--font-family--display, Playfair Display, Georgia, serif);
  font-size: 1.25rem;
  margin: 0 0 0.5rem;
}
.el-rule-entry__title a {
  color: var(--wp--preset--color--ink, #1a1a1a);
  text-decoration: none;
}
.el-rule-entry__desc {
  color: var(--wp--preset--color--ink-2, #444);
  font-size: 0.95rem;
}
.el-rule-entry__scopes {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0 0;
  display: inline-flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.el-rule-entry__scope {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  font-family: var(--wp--preset--font-family--sans, Inter, system-ui, sans-serif);
  font-size: 11px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-radius: 3px;
  background: var(--wp--preset--color--paper-2, #f0ebde);
  color: var(--wp--preset--color--ink-2, #444);
}

/* ------------------------------------------------------------------------
   Footer columns — stack on mobile, 3-up on desktop (already controlled
   by wp:columns block markup; here we just refine).
   ------------------------------------------------------------------------ */

footer .wp-block-columns {
  margin-top: 0;
  margin-bottom: 0;
}
footer ul.is-style-no-bullets {
  list-style: none;
  padding-left: 0;
  margin: 0;
}
footer ul.is-style-no-bullets li {
  margin-bottom: 0.25rem;
}
footer a {
  color: var(--wp--preset--color--ink-2, #444);
}

/* Batch fix item 4 (feedback 2026-08-19): .el-newsletter-signup__heading
   (blocks/newsletter-signup/render.php) is a bare <h2> with no CSS of its
   own anywhere (not in that block's style.css, not in web-blocks.css), so
   it's one of the few headings on the site that actually falls through to
   theme.json's global h2 element style — 0.6.10's h-N var fix (see the
   changelog note at the top of this file) made that style resolve
   correctly for the first time, at var(--wp--preset--font-size--h2)'s
   clamp max (2rem/32px). That reads oversized next to the rest of the
   footer (12px column labels, 14-17px body copy) — restore a footer-
   appropriate size without reverting the theme.json fix or touching
   newsletter-signup's own files (outside this ticket's write set). Reuses
   the h3 preset — WP kebab-cases the numeric-suffixed slug the same way
   (see the changelog note), so --wp--preset--font-size--h3 does not exist
   either; go straight to --h-3. */
.el-newsletter-signup__heading {
  font-size: var(--wp--preset--font-size--h-3, 1.25rem);
}

/* Batch fix item 5 (feedback 2026-08-19): .eg-sectionhead (web-blocks.css)
   never gains the >=640px sp-5 horizontal-padding bump that .eg-grid,
   .eg-pillars and .eg-hero-inner all get at the same container breakpoint
   — it stays at sp-4 (16px) forever, so on the home page "All seven games"
   sits 8px left of the hero copy and the card grid beneath it at desktop
   widths (measured on staging: hero/pillars/grid content all start at the
   same x, .eg-sectionhead-title starts 8px earlier). Match the same
   breakpoint the rest of the page-max sections use so the heading lines up
   with what it introduces. body.eg-web prefix for the same source-order
   reason as .eg-dlgrid above — web-blocks.css's own `.eg-web .eg-sectionhead`
   rule is the same 2-class specificity and loads after this file. */
@container eg-web (min-width: 640px) {
  body.eg-web .eg-sectionhead { padding-inline: var(--eg-w-sp-5); }
}

/* ------------------------------------------------------------------------
   Page templates — apply container constraint to <main>.
   ------------------------------------------------------------------------ */

/* Global content column — every page's <main> sits in a centered, padded
   column instead of running edge-to-edge (templates render alignfull, which
   ignored theme.json contentSize). Header/footer are separate template parts
   with their own inner width, so they stay full-bleed. */
/* Specificity must beat WP's .wp-block-group.alignfull (0,2,0) — main itself
   carries alignfull, so a plain `.wp-site-blocks > main` (0,1,1) loses. */
/* H2 finding 1 (2026-08-30, blind design review): this box used its own
   ad hoc 72rem (1152px) cap, a leftover from before the .eg-web design
   system dropped in. Every OTHER .eg-web frame — .eg-header-inner,
   .eg-footer-inner, .eg-hero-inner, .eg-feature-inner, .eg-grid,
   .eg-pillars, .eg-listhdr, .eg-sectionhead, .eg-archhero-inner — uses the
   ONE page-max token instead: `max-width: var(--eg-w-page-max)` (1180px,
   tokens.css). 1152 vs 1180 (a 28px gap, doubled to ~56px of total drift
   once each side re-centers) is exactly the "header/footer sit on a
   different frame than main" gap measured live (box edges 57/1209 for main
   vs 43/1223 for header/footer @1280 — verified again with a local
   tokens.css+web-blocks.css+style.css harness at viewport 1280:
   .eg-web .wp-site-blocks > main computed left:56.5 / right:1208.5 /
   width:1152 before this fix). Swapping in the token makes main's own box
   land on the SAME 1180px frame header and footer already use — one
   constant, one source of truth, instead of two numbers that were meant to
   agree and drifted. This was already known to bite: the .eg-archhero
   bleed/pull-out hack further up this file exists ONLY because
   "--eg-w-page-max never actually binds inside this theme's ~1088px main
   content column" (its own comment) — with main now on the same token,
   that inner mismatch closes at the source instead of needing a per-band
   workaround. Padding-inline (the clamp below) is left as designed — it is
   a separate, already-tuned inset budget shared by .eg-pagehead-inner's
   own measured-fix formula (LOCKED tier), not respecified here. */
.eg-web .wp-site-blocks > main.wp-block-group {
  max-width: var(--eg-w-page-max, 1180px);
  margin-inline: auto;
  padding-inline: clamp(1rem, 4vw, 2.5rem);
  padding-bottom: 4rem;
  box-sizing: border-box;
}

/* Tighter reading measure for long-form single content (blog posts, pages).

   This wins over the block layout, which is usually what we want and once cost
   an afternoon: /family-tree/ declared contentSize 800 / wideSize 1100 on both
   the main group and post-content, the section correctly carried .alignwide,
   and it still rendered at 704px, because 44rem caps the post-content WRAPPER
   before any child alignment is considered. A page whose content is a table
   rather than prose opts out with .is-wide-page on its main (see
   templates/page-family-tree.html). */
.single .wp-site-blocks > main > :where(.wp-block-post-content, .wp-block-post-title),
.page .wp-site-blocks > main > :where(.wp-block-post-content, .wp-block-post-title) {
  max-width: 44rem;
  margin-inline: auto;
}
/* The opt-out. The title keeps the reading measure; only the content escapes,
   so a wide table does not drag its heading out to 1100px with it. */
.wp-site-blocks > main.is-wide-page > .wp-block-post-content {
  max-width: none;
}

/* ------------------------------------------------------------------------
   Rules TOC on game_extra (F11 batch fix, feedback 2026-08-19).
   ------------------------------------------------------------------------ */

/* Reach-through for templates/single-game_extra.html's .eg-rules-layout
   (F11 batch fix, feedback 2026-08-19): el/rules-toc placed the group
   between <main> and wp:post-content, so the direct-child selector above no
   longer matches post-content on ANY game_extra sub-page — restore the same
   narrow measure through the extra level of nesting for every page that
   isn't the rules chapter. Descendant (not direct-child) on the last hop:
   inc/eg-prose-wrap.php inserts its own `.eg-prose` div between
   .eg-rules-layout and wp:post-content's own output for every "prose"
   extra slug (rules, variants, kontra, holding, ...), so the real depth is
   1 level for score-sheets/puzzles/calculator/extras but 2 for prose pages
   — .wp-block-post-content only ever appears once inside .eg-rules-layout
   either way, so the descendant combinator is exact, not a broad match. */
.single-game_extra .wp-site-blocks > main > .eg-rules-layout .wp-block-post-content {
  max-width: 44rem;
  margin-inline: auto;
}
/* The rules chapter itself opts back out: el/rules-toc only ever renders on
   the "rules" extra (blocks/rules-toc/render.php returns '' everywhere
   else), so :has(.eg-rules-toc) is true precisely when this IS the rules
   page — no second class or template needed. The chapter needs the wider
   column (~1090px) so the >=1024px sticky sidebar (blocks/rules-toc/
   style.css) has room beside it instead of crushing it into 44rem. */
.single-game_extra .wp-site-blocks > main > .eg-rules-layout:has(.eg-rules-toc) .wp-block-post-content {
  max-width: none;
  margin-inline: 0;
}

/* W-C finding 1 (feedback 2026-08-19, blind review 2026-08-30): every rules
   page reads as one undifferentiated wall of text. Root cause: web-blocks.css
   (design layer, out of this ticket's write set) sets `.eg-web .eg-prose > *
   + * { margin-top: var(--eg-w-sp-5) }` — a CHILD combinator — but on the
   live game_extra "rules" route WordPress inserts a `.entry-content` wrapper
   between `.eg-prose` and the actual paragraphs/headings
   (inc/eg-prose-wrap.php wraps wp:post-content's own output), so the real
   DOM is `.eg-prose > .entry-content > p`, never `.eg-prose > p` directly —
   the rule's selector never matches and 10 consecutive paragraphs measured
   0px apart. Measured live (koenigrufen/rules/, 1280px): before 0px gap /
   0px margin-top; after 24px / 24px, matching --eg-w-sp-5's own value (not
   hardcoded — the var is used directly, no fallback needed since
   tokens.css defines it unconditionally ahead of this file). Added
   alongside the original selector (kept, for any .eg-prose usage without
   the wrapper) rather than replacing it — purely additive, no specificity
   fight, so no !important needed. This also closes the second half of the
   finding (headings butting against their first paragraph): h2/h3 are
   direct .entry-content children too, so a paragraph immediately after a
   heading is a `* + *` match and now gets the same 24px. */
.eg-web .eg-prose > .entry-content > * + * {
  margin-top: var(--eg-w-sp-5);
}

/* Re-skin our existing el-* CTA buttons to the designer's button treatment
   so they inherit the accessible palette (white on deep red passes AA; the
   old hero CTAs failed color-contrast under the new tokens). */
body.eg-web a[class*="__cta"],
body.eg-web button[class*="__cta"] {
  display: inline-block;
  text-decoration: none !important;
  font-weight: 600;
  border-radius: var(--eg-btn-radius, 6px);
  padding: 0.7em 1.4em;
  line-height: 1.2;
  /* default (unmodified cta) = accessible primary; --secondary overrides */
  background: var(--eg-btn-bg, #8a0f22);
  color: var(--eg-btn-fg, #fff) !important;
  border: 1.5px solid transparent;
}
body.eg-web a[class*="__cta--primary"],
body.eg-web button[class*="__cta--primary"] {
  background: var(--eg-btn-bg, #8a0f22);
  color: var(--eg-btn-fg, #fff) !important;
  border: 1.5px solid transparent;
}
body.eg-web a[class*="__cta--primary"]:hover,
body.eg-web button[class*="__cta--primary"]:hover {
  background: var(--eg-btn-bg-hover, #7A0619);
}
body.eg-web a[class*="__cta--secondary"],
body.eg-web a[class*="__cta--ghost"],
body.eg-web button[class*="__cta--secondary"] {
  background: transparent !important;
  color: var(--eg-btn2-fg, #2a2320) !important;
  border: 1.5px solid var(--eg-btn2-border, #2a2320);
}

/* Every OTHER action button (form submits, WP element buttons, block
   submit/reveal buttons) gets the accessible primary treatment — deep red +
   white — so no button anywhere renders the buggy red-text-on-dark. */
body.eg-web .wp-element-button,
body.eg-web button[type="submit"],
body.eg-web input[type="submit"],
body.eg-web [class*="__submit"],
body.eg-web [class*="__reveal-btn"],
body.eg-web [class*="__answer-submit"] {
  background: var(--eg-btn-bg, #8a0f22) !important;
  color: var(--eg-btn-fg, #fff) !important;
  border: 1.5px solid transparent !important;
  text-decoration: none !important;
  border-radius: var(--eg-btn-radius, 6px);
  font-weight: 600;
}
body.eg-web .wp-element-button:hover,
body.eg-web button[type="submit"]:hover,
body.eg-web [class*="__submit"]:hover {
  background: var(--eg-btn-bg-hover, #7A0619) !important;
}
/* Never let the site link colour bleed into any button. */
body.eg-web button,
body.eg-web .wp-element-button {
  text-decoration: none;
}

/* H2 finding 3 (2026-08-30, blind design review): this AA-contrast hotfix
   pre-dates el/game-card's H4-game-card rewrite (see the block's own
   style.css: "now emits H4-game-card markup ... eg-gcard, eg-gcard-media,
   eg-gcard-body, eg-gcard-title, eg-gcard-desc, eg-gcard-meta"). Its intent
   was the meta row's own fill ("4 players" text); its selector
   `[class*="game-card"]` is a substring match that ALSO catches the block's
   own core-generated wrapper class `wp-block-el-game-card` — the same class
   that carries `eg-gcard` — so it force-painted the WHOLE card paper
   (#FBF7F1) over web-blocks.css's `.eg-web .eg-gcard { background:
   var(--eg-card-white) }` (measured live: .eg-gcard computed
   rgb(251,247,241), the page ground, not rgb(255,255,255), the specimen's
   card white). The companion `__players`/`__meta` BEM selectors below never
   matched the rewritten markup either (dead code, same class the block's
   own style.css already flags) — dropped along with the background line
   rather than left as inert legacy selectors pointing at markup that no
   longer exists. If a meta-row AA fix is still needed for the current
   .eg-meta-item / .eg-meta-label markup, that is a fresh, separately-scoped
   fix, not a revival of this one. */

/* Dark cards (puzzle teaser has a near-black tabletop bg) rendered red text +
   red links = the red-on-black bug. Force light text + a light CTA inside any
   dark teaser/card so contrast passes. */
body.eg-web [class*="puzzle-teaser"] :where(h1, h2, h3, h4, p, span, time, a) {
  color: #F4EFE6 !important;
}
body.eg-web [class*="puzzle-teaser"] a[class*="cta"],
body.eg-web [class*="puzzle-teaser"] [class*="__cta"] {
  background: #F4EFE6 !important;
  color: #241f28 !important;
  border-color: #F4EFE6 !important;
}
body.eg-web [class*="puzzle-teaser"] a:not([class*="cta"]) {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* H2 finding A (2026-08-30, coordinator addendum, CDP matched-rules dump on
   .eg-ecta-privacy a): the newsletter's G8 eg-ecta unit is a light
   --eg-paper-2 panel that H1 ported into the dark --eg-ink footer. Both
   colliding rules live in the generated design layer (assets/design/
   web-blocks.css, never hand-edited): `.eg-web .eg-ecta-privacy a { color:
   var(--eg-ink-3) }` (the panel's own correct intent) loses to `.eg-web
   .eg-footer a { color: var(--eg-paper) }` (paper-on-paper-2, 1.11:1)
   purely because the footer rule sits later in that generated file at the
   SAME specificity (0,2,1). Same fix pattern as every other .eg-web-vs-
   .eg-footer collision in this file (see the body.eg-web prefix notes
   above, e.g. .eg-archhero + .eg-grid): add the type selector to win on
   specificity instead of source order, scoped to the panel's own privacy
   line only so ordinary footer links keep their paper colour. --eg-ink-3 on
   --eg-paper-2 is the SAME pairing the coordinator already measured for the
   privacy line's surrounding sentence — 4.97:1, clears the 4.5:1 AA
   minimum. text-decoration-color set to match (the footer's own translucent
   paper underline would otherwise stay all but invisible on the light
   panel too). */
body.eg-web .eg-ecta-privacy a {
  color: var(--eg-ink-3);
  text-decoration-color: var(--eg-ink-4);
}

/* ------------------------------------------------------------------------
   Tables — rules-chapter tables (F12).
   ------------------------------------------------------------------------ */

/* assets/design/web-blocks-local.css styles every core/table inside
   .eg-prose (the rules chapter + its scoring tables — .eg-prose is used
   ONLY by el/rules-content, see blocks/rules-content/render.php) with a
   border-bottom on every row's cells, but then explicitly zeroes it again
   on the LAST row (`tbody tr:last-child td { border-bottom: none; }`), so
   every one of those tables is missing its closing bottom edge — reads as
   "top borders only". That rule lives outside this ticket's write set
   (assets/design/), so the fix is an override here instead of an edit
   there. This stylesheet is enqueued BEFORE web-blocks-local.css
   (functions.php: 'elsteiner-summer' has no deps, 'eg-web-local' depends on
   'eg-web-blocks' which depends on 'eg-web-tokens'), so a same-specificity
   rule here would still lose on source order — !important forces the win
   regardless of load order, matching this file's own existing convention
   for the same kind of cross-file override (see the button and game-card
   sections above). */
body.eg-web .eg-prose .wp-block-table tbody tr:last-child td {
  border-bottom: 1px solid var(--wp--preset--color--ink-4, #ddd) !important;
}

/* ------------------------------------------------------------------------
   Printables unit (feedback 2026-08-19; re-audited for the R4c drop,
   2026-08-22, ticket PR — see the retirement note at the end of this
   section for what left and why)
   ------------------------------------------------------------------------ */

/* F8: the score-sheets page's cards (.eg-dlgrid, assets/design/
   web-blocks.css) are capped and centred (R4c: `repeat(auto-fit,
   minmax(19rem,22rem))`, was `repeat(auto-fit, minmax(21rem,1fr))`), but
   templates/single-game_extra.html sets contentSize:900px on <main>, and
   that page's .wp-block-post-content is ALSO capped to 44rem by the
   "Tighter reading measure" rule above ("The opt-out" section) — either
   width is well under what three 22rem tracks plus two 20px gaps need
   (1096px, the R4c drop's own MANIFEST.md measurement against the site's
   1180px page-max). single-game_extra.html is the SHARED template for
   every game_extra sub-page (rules, variants, puzzles, calculator,
   score-sheets, ...), so widening it (or bumping contentSize) would widen
   every one of those, not just the printables page. Rather than add a
   per-page template just for this, :has() scopes the escape to exactly the
   post-content that carries .eg-dlgrid — the same "title keeps the
   measure, content escapes" opt-out already established above, applied
   without an .is-wide-page class on <main>. Specificity (0,4,1) beats the
   existing (0,2,1) reading-measure rule outright, so !important is not
   needed here. STILL NEEDED after R4c: the drop changed the grid's track
   sizing, not the page template or this ancestor chain, so the escape this
   rule buys is exactly as necessary as it was for the 21rem floor.

   F11 batch fix (feedback 2026-08-19) added el/rules-toc's .eg-rules-layout
   group between <main> and wp:post-content on every game_extra sub-page
   (see "Rules TOC on game_extra" above), so score-sheets' post-content is
   no longer a direct child of <main> either — both the plain selector and
   this :has() escape need the same extra level added.

   W-E (feedback round 2026-08-19, blind review 2026-08-30): the SAME
   squeeze hits two more furniture units for the same reason — F1's
   .eg-actions (koenigrufen/extras/, 87px tiles with labels painting
   outside the tile border) and H2's .eg-pillars (/about/, three columns
   forced to 186px). Not a new root cause, so not a new rule: added to this
   :has() list rather than duplicated. This escape alone is enough for
   .eg-actions (it declares no max-width of its own — same as the working
   single-game/render.php instance of it, which sits directly in <main>
   with no post-content ancestor to escape from at all, landing at
   ~1116px there); .eg-dlgrid and .eg-pillars need the further bleed rule
   below as well, since their own `max-width: var(--eg-w-page-max)`
   (web-blocks.css) still can't outgrow this now-uncapped but still
   main-padding-bound post-content box on its own. */
.single .wp-site-blocks > main > .wp-block-post-content:has(.eg-dlgrid, .eg-actions, .eg-pillars, .eg-sk, .game-puzzle-list),
.page .wp-site-blocks > main > .wp-block-post-content:has(.eg-dlgrid, .eg-actions, .eg-pillars, .eg-sk, .game-puzzle-list),
.single-game_extra .wp-site-blocks > main > .eg-rules-layout .wp-block-post-content:has(.eg-dlgrid, .eg-actions, .eg-pillars, .eg-sk, .game-puzzle-list) {
  max-width: none;
}

/* W-E: .eg-dlgrid and .eg-pillars both declare their OWN
   `max-width: var(--eg-w-page-max); margin: 0 auto` (web-blocks.css) —
   authored to bind directly against the true page frame, same token
   main's own box (further up this file) uses. The :has() escape above
   only removes the 44rem reading-measure cap; it leaves these two still
   bound by main's own padding-inline (32px, always, from
   templates/*.html's own inline style — the LOCKED clamp() a few rules up
   in this file does not actually win against that inline longhand, so the
   real, measured inset is the flat 32px/48px below, not the clamp
   formula), plus another 1.25rem this file's own "mobile padding for any
   direct main child" rule (above) adds at <=768px on whichever ancestor
   sits directly under <main> (.eg-rules-layout for game_extra,
   .wp-block-post-content itself for page.html — same net inset either
   way). Undo exactly that, symmetrically (both sides get the identical
   value, so width + margin-inline sums back to 100% of the *un-widened*
   box — never over-constrained, no !important needed, and no 100cqw
   viewport trick either: unlike .eg-pagehead's edge-to-edge band, these
   need to land CENTERED on main's own frame, which a cqw-relative margin
   does not do once max-width caps the result below 100cqw — verified
   against a local tokens.css+web-blocks.css+style.css harness before
   landing this rule, cqw gave x:0 instead of main's own x:42.5 at 1280).
   Measured after (1280): .eg-dlgrid and .eg-pillars both x:42.5 w:1180,
   exactly main's own box — three 352px .eg-dlgrid tracks fit (content
   1132px, R4c's own "3×352+40=1096" floor), .eg-pillars reaches 1180/345px
   matching the specimen. .eg-actions is deliberately NOT in this rule —
   see the comment above. */
/* SK1 (2026-10-30): the scorekeeper section is drawn to sit straight in the
   page frame with its own 16px gutter (.eg-sk padding) and its own
   page-max cap; inside the extra template's two paddings it got 256px at
   360 and truncated eight names. Same bleed as the grids. The puzzle
   bands (eg/game-puzzle-list) had the same squeeze: 302px cards in a
   256px column, cut at the right edge (2026-10-30). The bands carry their
   own page-max and gutter, like the scorekeeper. */
body.eg-web .wp-block-post-content .eg-dlgrid,
body.eg-web .wp-block-post-content .eg-pillars,
body.eg-web .wp-block-post-content .eg-sk,
body.eg-web .wp-block-post-content .game-puzzle-list {
  margin-inline: calc(-1.25rem - var(--wp--preset--spacing--6, 2rem));
  width: calc(100% + 2.5rem + 2 * var(--wp--preset--spacing--6, 2rem));
}
@media (min-width: 769px) {
  body.eg-web .wp-block-post-content .eg-dlgrid,
  body.eg-web .wp-block-post-content .eg-pillars,
  body.eg-web .wp-block-post-content .eg-sk,
  body.eg-web .wp-block-post-content .game-puzzle-list {
    margin-inline: calc(-1 * var(--wp--preset--spacing--6, 2rem));
    width: calc(100% + 2 * var(--wp--preset--spacing--6, 2rem));
  }
}

/* F8: intro paragraph and the card row read as touching with no rule
   between them. web-blocks.css (designer-owned) sets no margin-top on
   .eg-dlgrid, so this doesn't fight anything — added here rather than
   there per the write-set split for this ticket. STILL NEEDED after R4c:
   the re-cut layer still emits no margin-top for .eg-dlgrid (confirmed by
   diffing the re-cut file against the installed one), so nothing upstream
   closed this gap. */
body.eg-web .eg-dlgrid {
  margin-top: var(--eg-w-sp-5, 2rem);
}

/* Batch fix item 3 (feedback 2026-08-19): .eg-dlgrid and .eg-actions-
   secondary (web-blocks.css) are both `display:grid`/`display:flex`, but
   WP still classes their wp:group wrapper `is-layout-flow` (the className
   attribute points at hand-written CSS rather than a block "layout":
   {"type":"grid"|"flex"} attribute — same F4/F25 root cause already fixed
   for .eg-grid above), so WP's global styles inject
   `:root :where(.is-layout-flow) > * { margin-block: 24px 0 }` on every
   child but the first. Under grid/flex that margin lands INSIDE each
   card's own cell/row instead of between rows, so cards start at different
   vertical offsets within an otherwise aligned row ("staggered tops",
   .eg-dlgrid; "Also for this game" row, .eg-actions-secondary). Drop it,
   same as .eg-grid > .eg-gcard above. STILL NEEDED after R4c: the bug is in
   WP's own block-attribute vs. hand-written-CSS mismatch, not in the
   design layer, and R4c changed neither .eg-dlgrid's display:grid nor its
   className. */
.eg-dlgrid > *,
.eg-actions-secondary > * {
  margin-block: 0;
}

/* RETIRED 2026-08-22 (ticket PR, R4c drop). Two overrides removed here,
   both obsoleted by the same drop:

   1. The 3-up grid-forcing override (`@container eg-web (min-width:1024px)
      { body.eg-web .eg-dlgrid { grid-template-columns: repeat(auto-fit,
      minmax(16rem,1fr)); ... } }`). It existed because the OLD
      `minmax(21rem,1fr)` track let 1fr stretch two cards to half the row
      at large widths and still miscounted at mid widths, so this file
      hand-lowered the floor to 16rem as a fallback. R4c's own
      `minmax(19rem,22rem)` plus `justify-content:center` is a MEASURED
      cap-and-floor pair (MANIFEST.md: 22rem is the largest track that lets
      three fit in the page-max content box; 23rem missed by 12px and
      silently dropped to two columns) — it is the fix this file's own
      comment was reaching for by hand, now done properly upstream and
      without a fixed-1fr-stretch failure mode. Re-adding a floor override
      on top of an already-capped track would only refight a problem the
      drop already solved, and risks under/overshooting the drop's own
      measured number.
   2. The stale note that eg_download_card() emits no thumb and needs no
      override to close the gap. v21 of book-scaffold.php (ticket PR)
      brings the thumb slot BACK (see EG_BOOK_SCAFFOLD_VERSION's changelog)
      as one shared generic glyph. No override is needed for it EITHER: the
      re-cut web-blocks.css carries full `.eg-dl-thumb` / `.eg-dl-thumbimg`
      styling, including the eg-dl--form / eg-dl--ref variant treatments
      (verified by diffing the re-cut file against the previously installed
      one — only .eg-dl* / .eg-dlgrid selectors moved, nothing else in the
      layer did), so the old comment is simply superseded rather than
      replaced with a new rule.

   Not verified live (no local WordPress in this worktree — see
   00-shared.md "Verification you CAN do"): the exact pixel width
   single-game_extra.html's post-content column reaches after the :has()
   escape, which the retired override's own comment estimated at
   "~1024-1056px" against a floor math that no longer exists. Flagged in
   the PR report as a staging-verify item rather than asserted here. */

/* ------------------------------------------------------------------------
   Tap targets (W-C finding 4, blind review 2026-08-30) — 44px minimum,
   per the library's own pager law ("Plain links; 44px minimum target").
   .eg-rules-toc__summary / .eg-rules-toc__item a (blocks/rules-toc/) and
   .eg-qintro-all (blocks/puzzle-grid/) and .eg-dgame-name
   (blocks/single-deck/) are fixed in each block's own style.css — this
   ticket owns all three. .eg-facet is NOT: it's emitted by both
   blocks/contract-table/render.php (a concurrent sibling ticket's write
   set) and blocks/single-game/render.php (out of this ticket's write
   set), and its sizing rule lives in the generated
   assets/design/web-blocks.css, which this ticket must never hand-edit —
   so this is the one tap-target fix that lands here instead, as a
   cascade override, the same pattern this file already uses for the
   rules-chapter table border fix above.

   !important note: verified via the actual live <head> order
   (koenigrufen/rules/) that this stylesheet (a <link>) prints AFTER every
   block's own inline style tag (register_block_type_from_metadata prints
   block CSS inline, earlier in <head>) but BEFORE web-blocks.css (also a
   <link>, later). So a block's own style.css (e.g. puzzle-grid's) beats
   web-blocks.css on a same-specificity tie by source order alone — no
   !important needed there — but THIS file, sitting between the two,
   needs !important to beat web-blocks.css's later .eg-facet rule; matches
   this file's own documented convention (see "Tables" section above). */
body.eg-web .eg-facet {
  min-height: 44px !important;
}

/* Coordinator addendum (ticket W-E, 2026-08-30): the 404 page's single link
   (templates/404.html) is plain paragraph markup with no block of its own,
   so it never got the pager law's "44px minimum target" the breadcrumb link
   already carries (blocks/extra-breadcrumb/style.css — same
   inline-flex/align-items/min-height shape, reused here rather than
   reinvented). No !important: this selector (0,3,1) is not competing with
   anything from web-blocks.css for this class. */
body.eg-web .eg-404-back a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
