/* ============================================================================
   FERNDOWN JUNIOR CHESS — component styles
   FJC-30. Externalised from the two approved mockups' inline <style> blocks.
   Load AFTER tokens.css: every value here resolves through a custom property.

   ── WHY THIS IS A FILE ─────────────────────────────────────────────────────
   The target CSP carries no `unsafe-inline` (FJC-124). An inline <style> block
   is blocked outright under that policy, and so is a `style=` attribute — the
   ticket's wording says "blocks", but `style-src` does not distinguish. The six
   inline attributes the mockups carried are therefore gone too, replaced by the
   real classes marked · INLINED · below. Leaving them would have meant a page
   that renders wrong under the very policy this ticket exists to enable.

   ── NO HARD-CODED COLOURS ──────────────────────────────────────────────────
   FJC-28's second acceptance criterion. Every colour resolves through a token;
   there is no bare hex in this file. That is mechanically checkable:

       grep -nE '#[0-9a-fA-F]{3,8}|rgba?\(' site/assets/css/site.css

   should return nothing. Keep it that way — the palette only stays measurable
   while tokens.css remains the only place a colour is written down.

   ── WHAT IS DELIBERATELY NOT HERE ──────────────────────────────────────────
   The game viewer: .game, .gb-*, .moves, .mv, .annot, .controls, .pgn-* and
   .sq.hl. Per Peter's decision on 27/07/2026 those belong to epic FJC-4, which
   builds the viewer from vendored chess.js against a bespoke CSS-grid board
   (ADR-005). The article mockup's version is a prototype ADR-005 has already
   decided to replace, and porting it here would land an implementation that
   works well enough to become permanent by accident.

   The board primitives below (.board, .sq, .pc, .coord) ARE here, because the
   home page's hero board uses them and is not part of the viewer.

   Chess pieces render as inline Cburnett SVG per ADR-006, not as the Unicode
   glyphs the mockups used — FJC-159 vendors them. .pc styling is kept because
   the sizing and placement survive the change of what sits inside it.

   Also not ported: `.ph`, the mockups' photo PLACEHOLDER — a patterned box
   standing in for an upload that had not happened yet. Real photographs come
   from Azure Blob per ADR-004 and use `.photo`. If a template turns out to need
   an authoring placeholder, --on-pine-fig and --fs-glyph-xl are already in
   tokens.css for it; they are unreferenced here for exactly that reason and
   are not strays.
   ============================================================================ */

/* ── Reset and base ─────────────────────────────────────────────────────── */
* { box-sizing: border-box; margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--f-body);
  color: var(--ink);
  background: var(--chalk);
  line-height: var(--lh-body);
  font-size: var(--fs-body-fluid);
  -webkit-font-smoothing: antialiased;
}

/* ⚠️⚠️ `height: auto` IS THE OTHER HALF OF `max-width: 100%` AND LEAVING IT OUT
   SILENTLY DISTORTS IMAGES. width/height attributes are presentational hints, so
   a 440x546 image constrained to 263px wide keeps its 546px HEIGHT and stretches
   — it looks wrong and no gate says a word: it is valid, accessible, same-origin
   and inside the 320px viewport.
   ⚠️ This bit TWICE. .poster-figure img already carries height:auto with a
   comment explaining exactly this, and FJC-59's three illustrations hit it again
   anyway, because the fix lived on one component instead of the base rule.
   Caught by comparing rendered height against natural height in the browser —
   pass/fail could not see it. Fixed at the root 11 August 2026.
   ⛔ Do not remove it to "let the attributes control size". The attributes still
   supply the aspect ratio, which is what reserves space and prevents layout
   shift; height:auto only stops them being taken literally. */
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

h1, h2, h3 {
  font-family: var(--f-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  font-weight: 700;
}

/* ── Layout ─────────────────────────────────────────────────────────────── */
.wrap { width: min(var(--wrap), var(--gutter)); margin-inline: auto; }
.measure { width: min(var(--measure), var(--gutter)); margin-inline: auto; }
.section { padding: var(--sp-section) 0; }
.section--pine { background: var(--pine); color: var(--on-pine); }
.section--tint { background: var(--pine-tint); }
.section--buff { background: var(--buff); }

/* ── Skip link and focus ────────────────────────────────────────────────────
   The ring is scoped: --focus-light on pale surfaces, --brass-lite on the dark
   ones. A single ring colour cannot clear 3:1 against both --card and
   --pine-900, and FJC-26 measured brass at 2.17–2.80:1 on light backdrops.
   WCAG 2.2 makes focus appearance AAA rather than AA (2.4.13), so this is
   above the required bar — but an invisible focus ring on a children's club
   site is not a thing to leave measured-and-unfixed. */
:focus-visible {
  outline: 3px solid var(--focus-light);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* ⚠️ .find-visual WAS MISSING FROM THIS LIST UNTIL FJC-40, AND IT IS A DARK
   SURFACE. It is the only dark panel that is not a .section--pine, a .hero or a
   .masthead — it gets its darkness from a gradient in its own rule — so reading
   the list above and asking "is this a dark section?" finds it, and reading the
   selectors alone does not.

   The button inside it therefore drew --focus-light on near-black green.
   Measured in the browser against the composited backdrop (--scrim-pine at 0.88
   over the board pattern, so two surfaces, not one):

     --focus-light   1.79:1 over a light square, 2.18:1 over a dark one  ✗
     --focus-dark    3.78:1                      4.60:1                  ✓

   NOT introduced by FJC-40 — index.html's "Get directions" button has sat in
   this panel since FJC-32 and had the same ring. FJC-40 added a second instance,
   which is how it was noticed. Focus appearance is WCAG 2.4.13 and therefore
   AAA, so this was never an AA failure; it is fixed for the reason the comment
   below already gives — an invisible focus ring on a children's club site is
   not a thing to leave measured-and-unfixed. */
/* ⚠️⚠️ AND .board-frame IS THE SAME TRAP AGAIN, CAUGHT BEFORE IT SHIPPED THIS
   TIME — FJC-79. It is dark for exactly the reason .find-visual is: a gradient
   in its own rule, no --pine ancestor, and a class name that says nothing about
   it. It became focusable when the board became a keyboard widget, and it is
   the FRAME rather than the grid that takes focus, so the ring lands on the
   dark green surround.

   Measured from the tokens rather than eyeballed:

     --focus-light  2.71:1 over --sq-frame,  1.89:1 over --sq-frame-lift  ✗
     --focus-dark   5.71:1                   3.99:1                        ✓

   ⛔⛔ AND THOSE FIGURES ARE ABOUT THE WRONG SURFACE. FJC-201, 18/08/2026.
   `.board-frame` WAS IN THE LIST BELOW UNTIL THEN, AND THAT WAS THE DEFECT.

   AN OUTLINE IS PAINTED OUTSIDE THE BORDER BOX. With `outline-offset: 2px` the
   ring never touches the frame at all — it lands 2px clear, on `.game`'s
   near-white --foot-tint. Read from a screenshot of a focused frame's left edge,
   offsets from the border box, negative being outside it:

     -14..-6   #f7f7f5 -> #f0f0ee     the .game panel
      -5..-3   #dcb259 x3             the ring
      -2..-1   #ececea #ebebe9        the 2px offset gap
       0..     #27523b -> #204432     the frame's own gradient

   So --focus-dark sat at ~1.9:1 against what it was actually drawn on, across
   48 frames on 9 pages. ⭐ THE REASONING ABOVE WAS SOUND AND THE GEOMETRY WAS
   OVERLOOKED, which is why it is kept rather than replaced.

   ⛔ THE FIX IS THE TOKEN, NOT THE OFFSET. The frame is a DARK component sitting
   on a LIGHT panel, and the ring belongs to the panel. --focus-light measures
   4.09:1 there. A negative outline-offset would have put the ring inside on the
   gradient instead — better contrast, and it cuts across a --r-3xl corner and
   makes this the only ring on the site that behaves differently.

   ⚠️⚠️ AND THE "AAA" LINE THAT USED TO BE HERE WAS WRONG. It read "focus
   appearance is WCAG 2.4.13 and therefore AAA — so this was not an AA failure
   either." Checked against the W3C text at FJC-201 rather than recalled:
   SC 1.4.11 Non-text Contrast is **Level AA**, it **does** cover focus
   indicators, and for an indicator drawn OUTSIDE a component it requires 3:1
   against "the background the component sits on". 2.4.13 is AAA and asks for
   more on top. **This was an AA failure for as long as it stood.**

   ⚠️ NOTE THE SELECTOR SHAPE. Every entry below is `<ancestor> :focus-visible`,
   matching things INSIDE a dark panel. `.board-frame` needed `.board-frame:focus-
   visible` because it is itself the focusable thing — and being in this list at
   all was the mistake, so do not add it back. */
.site-header :focus-visible,
.site-footer :focus-visible,
.section--pine :focus-visible,
.hero :focus-visible,
.masthead :focus-visible,
.find-visual :focus-visible {
  outline-color: var(--focus-dark);
}

/* The ring sits outside the frame's rounded corner rather than cutting across
   it. `--r-xs` from the base rule would square off a --r-3xl surface. */
.board-frame:focus-visible { border-radius: var(--r-3xl); }

.skip {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--brass);
  color: var(--ink);
  padding: 0.7rem 1.1rem;
  font-weight: 700;
  z-index: 200;
  border-radius: 0 0 var(--r-lg) 0;
}
.skip:focus { left: 0; }

/* ── Visually hidden, but PRESENT — FJC-80 ────────────────────────────────────
   The site's first such utility, and it exists for one thing: the chess viewer's
   live region, which has to be in the accessibility tree and out of the layout.

   ⚠️⚠️ THE ONE WAY TO GET THIS WRONG IS `display: none`, AND IT LOOKS RIGHT.
   A display:none live region is removed from the accessibility tree, so updating
   its text announces NOTHING — the feature is silently absent while the markup,
   the ARIA attributes and every reasonable code review all look correct.
   `visibility: hidden` fails the same way. `tools/viewer-check.js` asserts the
   computed style is neither.

   ⚠️ `white-space: nowrap` is not decoration either. Without it the 1px box wraps
   the text one character per line, and several screen readers then read it a
   character at a time — "M, o, v, e, space, one".

   ⚠️ NOT `left: -999px` like .skip above. That works, and it makes the document
   scrollable to the left in some engines when the text is long; the clip-path
   form takes the element out of the flow entirely. .skip needs to come BACK on
   focus, which is why it uses the older shape. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Eyebrow ────────────────────────────────────────────────────────────────
   FJC-26 [A]: --brass-ink, not --brass. Brass is 2.17:1 on buff and fails even
   the 3:1 large-text threshold, so no size setting rescues it. The inline
   glyph tracks the text rather than staying gold, or the label reads two-tone. */
.eyebrow {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono-wide);
  text-transform: uppercase;
  color: var(--brass-ink);
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}
.eyebrow .glyph { font-size: 1.1em; line-height: 1; }
.section--pine .eyebrow,
.hero .eyebrow,
.masthead .eyebrow { color: var(--brass-lite); }

/* ── Header ─────────────────────────────────────────────────────────────── */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--pine);
  border-bottom: 1px solid var(--brass-border);   /* FJC-26 [N]: was .18 */
  transition: box-shadow 0.25s var(--ease);
}
.site-header.is-scrolled { box-shadow: var(--sh-header); }

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 0;
  width: min(var(--wrap), var(--gutter));
  margin-inline: auto;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  text-decoration: none;
  color: var(--on-pine);
}

.emblem {
  width: 2.55rem;
  height: 2.55rem;
  flex: none;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 32% 28%, var(--buff-lite), var(--emblem-lo));
  color: var(--pine-700);
  border-radius: var(--r-circle);
  font-size: var(--fs-glyph);
  line-height: 1;
  box-shadow: var(--sh-emblem);
}

.brand-name {
  font-family: var(--f-display);
  font-weight: 800;
  font-size: var(--fs-md);
  letter-spacing: var(--ls-tight);
  line-height: 1;
}

.brand-sub {
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--brass-lite);
  margin-top: 2px;
}

/* ── Navigation ─────────────────────────────────────────────────────────────
   ⚠️ aria-current="page" is NOT styled here as a per-page attribute in the
   markup. Consistency-gate checks 1 and 2 compare the <header> block
   byte-for-byte across every page, so an attribute that differs per page
   breaks them the moment a second page exists. FJC-31 owns that decision;
   until it is made, the rule below is written to work whether the attribute
   is authored or applied at runtime. */
.nav { display: flex; align-items: center; gap: 0.35rem; }

/* ⚠️ THE opacity BELOW IS A CONTRAST HAZARD FOR ANY RULE THAT GIVES A NAV LINK A
   BACKGROUND. Kept here after FJC-163 removed the rule that first proved it, so
   the lesson outlives the code that taught it.

   FJC-32 found `.nav a.is-cta` failing AA: --brass behind --ink measures 5.60:1
   on its own and passes comfortably, but opacity:.9 was not reset, the element
   composited over the pine header, and axe measured the result at 4.41:1 against
   the 4.5:1 threshold — a 0.09 miss, live on eight of nine pages.

   ⚠️ NEITHER OF THIS PROJECT'S COLOUR CONTROLS COULD SEE IT. FJC-26 measured 137
   colour PAIRINGS from the tokens and this pairing passes: the defect is
   composition, not palette. Consistency check 11 asserts only that colours live
   in tokens.css, and this one did. The tell was that contact.html alone passed —
   nav.js marked the CTA aria-current there, and that rule DOES reset opacity, so
   the single page where the bug was masked was the single page that measured
   clean.

   So: reset opacity to 1 on any nav rule that adds a background, and measure the
   composited result rather than the token pair. */
.nav a {
  text-decoration: none;
  color: var(--on-pine);
  font-weight: 600;
  font-size: var(--fs-sm);
  padding: 0.5rem 0.7rem;
  border-radius: var(--r-lg);
  position: relative;
  opacity: 0.9;
  transition: opacity var(--t-base), background var(--t-base), color var(--t-base);
}
.nav a:hover { opacity: 1; background: var(--white-wash-hover); }

.nav a[aria-current="page"] { opacity: 1; color: var(--card); }
.nav a[aria-current="page"]::after {
  content: "";
  display: block;
  height: 2px;
  background: var(--brass-lite);
  border-radius: 2px;
  margin-top: 3px;
}

/* FJC-163 removed `.nav a.is-cta` and its three companions — two here and one in
   the mobile block below, which was nearly missed by counting the rules next to
   each other rather than grepping the class. The CTA left the primary
   nav when the nav adopted functional spec §5's six items, and `is-cta` now has
   zero instances anywhere under site/ — a styled component with no users reads to
   the next person as a component that exists. The contrast lesson it carried is
   preserved above `.nav a`, where the opacity that caused it still lives. */

.nav-toggle {
  display: none;
  background: transparent;
  border: 1px solid var(--brass-border);          /* FJC-26 [H]: was .40 */
  color: var(--on-pine);
  border-radius: var(--r-lg);
  padding: 0.55rem 0.7rem;
  cursor: pointer;
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  letter-spacing: 0.1em;
  align-items: center;
  gap: 0.5rem;
}

/* ── Buttons ────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  text-decoration: none;
  font-family: var(--f-body);
  font-weight: 700;
  font-size: var(--fs-base);
  padding: 0.9rem 1.4rem;
  border-radius: var(--r-xl);
  cursor: pointer;
  border: 2px solid transparent;
  transition: transform var(--t-fast) var(--ease), background var(--t-base),
              border-color var(--t-base), color var(--t-base);
}
.btn:hover { transform: translateY(-2px); }
.btn--primary { background: var(--brass); color: var(--ink); }
.btn--primary:hover { background: var(--brass-lite); }
.btn--pine { background: var(--pine); color: var(--card); }
.btn--pine:hover { background: var(--pine-700); }
.btn--ghost {
  background: transparent;
  color: var(--on-pine);
  border-color: var(--brass-border);              /* FJC-26 [G]: was .55 */
}
.btn--ghost:hover { border-color: var(--brass-lite); background: var(--white-wash); }
.btn .arw { transition: transform var(--t-base); }
.btn:hover .arw { transform: translateX(3px); }

/* ── Hero ───────────────────────────────────────────────────────────────── */
.hero {
  background: linear-gradient(160deg, var(--pine) 0%, var(--pine-700) 78%);
  color: var(--on-pine);
  overflow: hidden;
  position: relative;
}

.hero-inner {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: var(--sp-grid-gap);
  align-items: center;
  padding: var(--sp-hero-top) 0 var(--sp-hero-bottom);
}

.hero h1 {
  font-size: var(--fs-h1-hero);
  color: var(--card);
  margin: 1.1rem 0 0;
  max-width: 15ch;
}
.hero h1 em { font-style: normal; color: var(--brass-lite); }

.hero-lede {
  margin-top: 1.35rem;
  font-size: var(--fs-lede);
  color: var(--on-pine-lede);
  max-width: 46ch;
  opacity: 0.95;
}

.hero-cta { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-top: 2rem; }

.hero-facts { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 2.2rem; }

.fact {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  padding: 0.42rem 0.72rem;
  border: 1px solid var(--brass-border);           /* FJC-26 [M]: was .42 */
  border-radius: var(--r-pill);
  color: var(--on-pine);
}
.fact b { color: var(--brass-lite); font-weight: 700; }

/* ── Board (shared: hero board and, later, the viewer) ──────────────────── */
.hero-board { justify-self: center; width: min(440px, 84vw); }

.board-frame {
  padding: 5%;
  border-radius: var(--r-3xl);
  background: linear-gradient(150deg, var(--sq-frame-lift), var(--sq-frame));
  background-color: var(--sq-frame);
  box-shadow: var(--sh-board-frame);
}

.board {
  container-type: inline-size;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  aspect-ratio: 1;
  border-radius: var(--r-sm);
  overflow: hidden;
  box-shadow: var(--sh-board);
}

/* ── Hero artwork ─────────────────────────────────────────────────────────────
   The club's illustration, mounted in the same .board-frame the hero board used
   to sit in. Director's instruction, 17 August 2026.

   ⚰️ `.hero .board` AND `.hero .board.is-in` LIVED HERE AND WERE REMOVED WITH THE
   BOARD, because they now match NOTHING — measured across all 29 pages, not
   assumed: `.hero .board` returns 0 elements while `.board` still returns 35 on
   the Learn diagrams and the viewer, so the general component rule stays and
   only the hero-scoped pair went. Nothing in CI lints CSS, so a rule matching
   nothing survives every gate silently; that is the `.is-cta` lesson.

   ⭐ The reasoning they carried is worth keeping, because it is why this pair is
   scoped to .hero rather than sitting on the component: the reveal was once on
   `.board` itself, so EVERY board on the site began at opacity 0 and waited for
   nav.js. That is fine for one decorative hero and fatal for a teaching diagram
   — FJC-36's Learn positions ARE the content, and they would have rendered
   invisible with JavaScript off, the exact failure FJC-78 exists to prevent.
   A flourish belongs to the thing it flourishes, not to the component. If the
   board ever returns to the hero, restore the pair scoped the same way.

   ⚠️⚠️ AN INVISIBLE HERO PASSES EVERY GATE THIS PROJECT OWNS. This starts at
   opacity 0 and only nav.js's `is-in` reveals it, so the selector here, the
   id in index.html and the getElementById in nav.js are ONE MECHANISM IN THREE
   FILES. Break any one and the hero renders blank while html-validate, axe,
   check:csp and Reflow all stay green — a fully transparent element is valid,
   named, same-origin and narrow enough for 320px. The instrument that sees it is
   a rendered screenshot, which is how the 4x4 avatar was caught.

   The radius matches .board's --r-sm rather than the frame's --r-3xl: the frame
   keeps its own large corner and the artwork sits inside it, exactly as the
   board's grid did. */
/* ⚠️⚠️ THE HIDDEN STATE IS INSIDE `@media (scripting: enabled)` AND THAT IS THE
   WHOLE POINT. The board's version was unconditional, so with JavaScript off the
   hero was blank — acceptable while it held a decorative board whose caption
   described it in words, and NOT acceptable now. This slot holds the club's
   artwork, it carries real alt text, and it is the page's LCP element: gating it
   behind script is FJC-78's rule inverted, on the site's front page, and the
   comment above quotes that rule about the very component it replaced.
   ⭐ Measured, not assumed: with scripting disabled the hero previously computed
   opacity 0 and stayed there. Inside this query it computes 1.
   ⚠️ A browser too old for the `scripting` feature ignores the block entirely and
   shows the artwork immediately — it loses the flourish and keeps the content,
   which is the right way round to degrade.
   ⛔ Do not "simplify" this back out of the media query. */
@media (scripting: enabled) {
  .hero .hero-art {
    opacity: 0;
    transform: translateY(14px) scale(0.985);
    transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
  }
  .hero .hero-art.is-in { opacity: 1; transform: none; }
}
.hero-art img { border-radius: var(--r-sm); width: 100%; }

/* ── Teaching diagram — FJC-36 ────────────────────────────────────────────────
   A static position, authored by tools/fen-to-board.js and pasted in as flat
   markup. No JavaScript, no viewer, no build step.

   ⚠️ Unlike the hero board this is NOT aria-hidden. The hero is decoration and
   its caption says in words what it shows; here the position is the content, so
   the board carries role="img" and an author-written label. ADR-006 draws that
   line: labelling is waived for a decorative board and stands wherever the
   pieces carry meaning. */
.diagram {
  width: min(420px, 92%);
  margin: 2.4rem auto;
}
.diagram .board-frame { padding: 4%; }
.diagram figcaption { margin-top: 0.9rem; }

.sq { position: relative; aspect-ratio: 1; display: grid; place-items: center; }
.sq.light { background: var(--sq-light); }
.sq.dark { background: var(--sq-dark); }

/* FJC-32: .pc IS the <svg> now, not a span holding a glyph — ADR-006 renders
   every piece as a Cburnett <use>. Two consequences worth stating, because both
   look like omissions:

   1. Sized in per-cent, not font-size. An <svg> takes no intrinsic size from
      `font-size`, so the mockup's 9.3cqw could not carry over. Cburnett's
      artwork occupies roughly two thirds of its own 45×45 viewBox, so filling
      the square gives the usual board inset without hand-tuning a margin.

   2. No .wp / .bp colour rules. The vendored symbols carry their own fills, and
      FJC-159 kept them byte-verbatim for the BSD-3 attribution — so a `color`
      here would do nothing. --piece-light and --piece-dark are consequently
      unreferenced in this file. They are NOT strays: ADR-006 records that the
      fills could be driven from tokens, which is the only route by which those
      two would come back. */
.pc { display: block; width: 100%; height: 100%; }

/* FJC-26 [D][E]: the coordinates were #5c6f52 / #e9e2cd at opacity .92, which
   measured 3.68:1 and 3.49:1. The opacity is REMOVED rather than reduced —
   it was part of the cause, not a separate concern. */
.coord {
  position: absolute;
  font-family: var(--f-mono);
  font-weight: 700;
  font-size: 2.3cqw;
  line-height: 1;
  pointer-events: none;
}
.coord.rank { top: 5%; left: 6%; }
.coord.file { bottom: 3%; right: 7%; }
.sq.light .coord { color: var(--coord-light); }
.sq.dark .coord { color: var(--coord-dark); }

/* FJC-26 [I]: opacity:.85 deleted. It alone put this at 4.08:1. */
.board-caption {
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--brass-lite);
  text-align: center;
  margin-top: 1rem;
}

/* ── Facts strip ────────────────────────────────────────────────────────── */
.strip { background: var(--pine-900); color: var(--on-pine); }

.strip-inner {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--brass-rule);
  width: min(var(--wrap), var(--gutter));
  margin-inline: auto;
}

.strip-item { background: var(--pine-900); padding: 1.5rem 1.25rem; text-align: center; }
.strip-item .glyph { font-size: var(--fs-glyph); color: var(--brass-lite); line-height: 1; }
.strip-item .big {
  font-family: var(--f-display);
  font-weight: 800;
  font-size: var(--fs-lg);
  color: var(--card);
  margin: 0.5rem 0 0.15rem;
  letter-spacing: -0.01em;
}
/* opacity:.85 is KEPT. FJC-26 measured this pairing and it passes; only three
   opacities were required to go (.board-caption and both .coord rules), and
   those failed. Dropping a passing one would be an unrequested change to an
   approved design smuggled in under a contrast fix. */
.strip-item .sm {
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--brass-lite);
  opacity: 0.85;
}

/* ── Section heads ──────────────────────────────────────────────────────── */
.head { max-width: 60ch; }
/* h1 shares the h2 step. A page whose <h1> is its section heading — the news
   index (FJC-34) — should not jump a size purely because the element changed. */
.head h1,
.head h2 { font-size: var(--fs-h2); margin: 0.85rem 0 0; color: var(--ink); }
.head p { margin-top: 1rem; color: var(--ink-soft); font-size: var(--fs-md); }
.head--center { margin-inline: auto; text-align: center; }
.head--flush { margin: 0; }                        /* · INLINED · home:639 */

/* ── The article rail — FJC-199 ───────────────────────────────────────────────
   ONE COLUMN, NOT THREE. A .wrap holding a prose column used to stack three
   different geometries on top of each other, measured at a 1280px viewport:

     .wrap                     80..1200   1120   centred
     .head       max-width:60ch  80.. 658    578   FLUSH LEFT
     bare <h2>   no width rule   80..1200   1120   FLUSH LEFT
     .measure                   300.. 980    680   CENTRED, inset 220px

   So the paragraphs were not narrower than the dek — 680 against 578 — they were
   INDENTED 220px while the headings started hard left. 96 of 98 wrap/viewport
   combinations were misaligned. Reported by the director from the deployed site.

   ⚠️⚠️ THE BARE HEADINGS WERE THE SHARPER HALF AND NOTHING SAID SO. 97 of them
   across 11 files matched no width, NO FONT-SIZE and no margin rule at all: line
   79 sets only family/line-height/letter-spacing/weight, and `* { margin: 0 }` at
   line 47 removes the UA margin too. They rendered at the browser default 1.5em
   with zero space above — smaller than .prose h2 and jammed against the text.

   ⚠️⚠️ `max-width: none` IS LOAD-BEARING AND ITS ABSENCE LOOKS FINE ON READING.
   Measured on learn/tactics.html at 1280 with the width and margin but no reset:
       .head 351..929 (578)   .measure 300..980 (680)   -> 51px INSIDE the prose.
   60ch clamps the 680px box back to 578 and `margin-inline: auto` then centres
   THAT. The fix half-works, and a screenshot of it looks correct.

   ⚠️ THE 92% IS A SECOND GUTTER, NOT THE FIRST. min(680, 92%) resolves 92%
   against the WRAP, not the viewport, so at a 320px viewport this is 271px —
   which is exactly what .measure already is, and exactly the 271px column the two
   measured 320px findings further down this file depend on. ⛔ Do not "fix" it to
   100%: that aligns the headings to the wrap instead of to the prose and undoes
   the whole change.

   ⛔⛔ SCOPED TO A MODIFIER, NEVER GLOBAL. .head also sits above full-bleed card
   grids — .sessions, .find-grid, .activities, .news-grid, .poster-layout — where
   flush-left against the grid's own left edge is correct. 21 of the 47 <main>
   wraps are grids, and two of them (.hero-inner, .involve-inner) are themselves
   display:grid and would break outright.
   ⚠️ Named --prose and not --article because article.article already exists on
   the news posts and means something else.
   ⚠️ .head--flush (line 581) is inert under this modifier: `margin: 0` at 0-1-0
   loses to `margin-inline: auto` at 0-2-0. Its only instance is index.html's news
   head, which is in a GRID wrap, so there is no live conflict — but if one ever
   arises the fix is :not(.head--flush), not deleting this.
   ⚠️ This block sits ABOVE `.related .head { max-width: none }` on purpose: that
   selector is also 0-2-0, so source order is what lets it keep winning. */
.wrap--prose > .crumb,
.wrap--prose > .head,
.wrap--prose > .hero-cta,
.wrap--prose > h2,
.wrap--prose > h3 {
  width: min(var(--measure), var(--gutter));
  margin-inline: auto;
}
.wrap--prose > .head { max-width: none; }

/* The size and lead-in are .prose h2's (line 1194) and --fs-xl, so a section
   heading reads the same whether it is inside a prose block or a sibling of one.
   ⚠️ h3 is --fs-xl (21.6px) rather than .policy h3's --fs-md: --fs-md is 18.4px
   against prose at 17.92px, a 0.48px step, which reads as bold body text rather
   than as a heading. The policy pages get away with it because theirs are run-in
   headings under a numbered §; a Learn page's h3 is a real section.
   ⚠️⚠️ scroll-margin-top is 5.5rem because the header is position:sticky and
   MEASURED 69px at 1280 / 80px at 375. Before this, 129 of 130 anchor landings on
   the eight indexed pages put the heading BEHIND the header — -45px on the policy
   pages, which do set a scroll margin, and -69 to -81px on the Learn pages, which
   set none. Nothing in CI can see this; it was found by driving the anchors. */
.wrap--prose > h2 { font-size: var(--fs-h2-prose); margin-top: 2.8rem; scroll-margin-top: 5.5rem; }
.wrap--prose > h3 { font-size: var(--fs-xl);       margin-top: 1.8rem; scroll-margin-top: 5.5rem; }

.section--pine .head h2 { color: var(--card); }
.section--pine .head p { color: var(--on-pine-soft); }

/* ── Sessions ───────────────────────────────────────────────────────────── */
.sessions { display: grid; grid-template-columns: 1fr 1fr; gap: 1.2rem; margin-top: 2.6rem; }

.session {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-4xl);
  padding: 1.6rem 1.6rem 1.75rem;
  box-shadow: var(--sh-card);
}

.session .clock {
  font-family: var(--f-mono);
  font-weight: 700;
  color: var(--pine);
  font-size: var(--fs-2xl);
  letter-spacing: 0.02em;
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
.session .clock .dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--r-circle);
  background: var(--brass);
}

.session .role {
  margin-top: 0.35rem;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* FJC-26 [L]: the two segments used to butt together, and no colour on the
   brass hue can be both dark enough against the white card and light enough
   against --pine. The 2px gap shows the track between them, so each segment is
   adjacent to the track and the card and never to the other. This is the one
   fix in FJC-26 that is markup-shaped rather than a repaint. */
.splitbar {
  display: flex;
  gap: 2px;
  height: 0.7rem;
  border-radius: var(--r-pill);
  overflow: hidden;
  margin: 1.4rem 0 0.7rem;
  background: var(--pine-tint);
}
.splitbar .learn { background: var(--pine); flex: 0 0 46%; }
.splitbar .play { background: var(--brass-ink); flex: 1; }

.splitkey {
  display: flex;
  justify-content: space-between;
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.splitkey b { color: var(--ink); }

.session-note {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem 1.6rem;
  margin-top: 0.4rem;
  padding-top: 1.5rem;
  border-top: 1px dashed var(--line);
}
.session-note p {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  color: var(--ink-soft);
  flex: 1 1 260px;
  font-size: var(--fs-sm);
}
.session-note .glyph { color: var(--brass-ink); font-size: var(--fs-lg); line-height: 1.3; flex: none; }

/* ── Reassurance cards ──────────────────────────────────────────────────── */
.reassure-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.1rem; margin-top: 2.4rem; }

.rcard {
  display: flex;
  gap: 1rem;
  background: var(--card);
  border-radius: var(--r-3xl);
  padding: 1.35rem 1.4rem;
  border: 1px solid var(--line);
}
.rcard .glyph {
  width: 2.7rem;
  height: 2.7rem;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--r-xl);
  background: var(--pine-tint);
  color: var(--pine);
  font-size: var(--fs-2xl);
  line-height: 1;
}
.rcard h3 {
  font-family: var(--f-display);
  font-size: var(--fs-prose);
  font-weight: 700;
  margin-bottom: 0.2rem;
  letter-spacing: -0.01em;
}
.rcard p { color: var(--ink-soft); font-size: var(--fs-sm); line-height: 1.55; }

/* ── Activities ─────────────────────────────────────────────────────────── */
.activities { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 2.6rem; }

.act {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-3xl);
  padding: 1.4rem;
  transition: transform var(--t-fast) var(--ease), border-color var(--t-base),
              box-shadow var(--t-base);
}
.act:hover { transform: translateY(-3px); border-color: var(--brass); box-shadow: var(--sh-lift); }
.act .glyph { font-size: var(--fs-glyph); color: var(--pine); line-height: 1; }
.act h3 {
  font-family: var(--f-display);
  font-size: var(--fs-prose);
  font-weight: 700;
  margin: 0.7rem 0 0.35rem;
  letter-spacing: -0.01em;
}
.act p { color: var(--ink-soft); font-size: var(--fs-sm); line-height: 1.5; }

/* ── News cards ─────────────────────────────────────────────────────────── */
.news-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
/* FJC-32: the teaser grid is a <ul>, which the mockup's <div> was not. That is
   forced by consistency check 5 — it locates the data-teasers container with a
   backreferenced tag name and a non-greedy body, so the match ends at the first
   closing tag of the same name. On a <div> wrapping cards that contain <div>s
   the container would close on an inner one, and the gate would find no teaser
   links at all. A <ul> has no nested <ul>, so it closes where it should. */
.news-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.2rem; margin-top: 2.4rem; list-style: none; }
.news-grid > li { display: flex; }
.news-grid > li > .post { flex: 1; }

.post {
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-4xl);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition: transform var(--t-fast) var(--ease), box-shadow var(--t-base);
}
.post:hover { transform: translateY(-3px); box-shadow: var(--sh-lift); }

.post-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1.3rem;
  background: var(--pine);
  color: var(--on-pine);
}
.post-tag {
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--brass-lite);
}
.post-date { font-family: var(--f-mono); font-size: var(--fs-micro); color: var(--on-pine-meta); }

.post-body { padding: 1.25rem 1.3rem 1.4rem; display: flex; flex-direction: column; flex: 1; }
/* FJC-173, 15 August 2026: these were h3 (h4 for .ladder). Each sat directly
   under the page h1 with no h2 between, so the outline skipped a level. The
   headings are the page's top-level content, so h2 is the CORRECT level rather
   than a workaround. ⛔ Tag and selector move together — nothing lints CSS. */
.post-body h2 {
  font-family: var(--f-display);
  font-size: var(--fs-md);
  font-weight: 700;
  line-height: var(--lh-heading);
  letter-spacing: -0.01em;
}
.post-body p { color: var(--ink-soft); font-size: var(--fs-sm); margin-top: 0.55rem; flex: 1; }

.readmore {
  margin-top: 1rem;
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--pine);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}
.readmore .arw { transition: transform var(--t-base); }
.post:hover .readmore .arw,
.readmore:hover .arw { transform: translateX(3px); }

.textlink {
  color: var(--pine);
  font-weight: 700;
  text-decoration: none;
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  white-space: nowrap;
}
.textlink .arw { transition: transform var(--t-base); }
.textlink:hover .arw { transform: translateX(3px); }

/* ── News index pagination — FJC-34 ───────────────────────────────────────────
   Real anchors, no JavaScript. The whole control is links and one current-page
   marker, so it works with scripting off by construction rather than by fallback.

   ⚠️ THE SIZING IS AN ACCEPTANCE CRITERION, NOT TASTE. WCAG 2.2 SC 2.5.8 sets a
   24x24 CSS pixel minimum for pointer targets. These are 44px, which also clears
   the AAA 2.5.5 bar, because a row of small numbered links is the classic way to
   fail 2.5.8 and the cost of clearing it properly is nil.

   ⚠️ --line-strong, not --line: on these the border IS the control boundary, so
   1.4.11 applies at 3:1 and --line manages 1.28:1. That makes three components
   using it — .tag, .postnav a and these — against the five FJC-26 listed. */
.pagin {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 2.8rem;
}

.pagin-pages {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.pagin a,
.pagin .is-current {
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: 0 0.85rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xl);
  background: var(--card);
  color: var(--pine);
  text-decoration: none;
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  font-weight: 700;
  transition: border-color var(--t-base), background var(--t-base);
}
.pagin a:hover { border-color: var(--brass); background: var(--buff-lite); }

/* The current page is a <span>, not a link to where you already are. Marked with
   aria-current so it is announced rather than merely coloured. */
.pagin .is-current {
  background: var(--pine);
  color: var(--card);
  border-color: var(--pine);
}

.pagin-step .arw { transition: transform var(--t-base); }
.pagin-step:hover .arw { transform: translateX(3px); }
.pagin-step.is-prev:hover .arw { transform: translateX(-3px); }

/* ── Get involved ───────────────────────────────────────────────────────── */
.involve-inner { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 2.5rem; align-items: center; }
.involve .cta-row { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-top: 1.8rem; }

/* · INLINED · home:691-692. The heading carried its own clamp, 0.15rem short
   of --fs-h2 at full width — see the note on --fs-h2-involve in tokens.css. */
.involve .head h2 { font-size: var(--fs-h2-involve); }
.involve .head p { max-width: 52ch; }

.involve-aside {
  background: var(--white-wash);
  border: 1px solid var(--brass-border);
  border-radius: var(--r-4xl);
  padding: 1.7rem;
  text-align: center;
}
.involve-aside .huge {
  font-family: var(--f-display);
  font-weight: 800;
  font-size: var(--fs-numeral);
  color: var(--brass-lite);
  line-height: 1;
}
.involve-aside p {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--on-pine);
  margin-top: 0.5rem;
}
.involve-aside .divider { height: 1px; background: var(--brass-border); margin: 1.3rem 0; }

/* ── Find us ────────────────────────────────────────────────────────────── */
.find-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; margin-top: 2.6rem; }

.find-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-4xl);
  padding: 1.8rem;
}
.find-card h2 {
  font-family: var(--f-display);
  font-size: var(--fs-xl);
  font-weight: 700;
  margin-bottom: 0.15rem;
}
.find-card .addr { color: var(--ink-soft); margin: 0.15rem 0 1.2rem; line-height: 1.5; }

.detail {
  display: flex;
  gap: 0.85rem;
  padding: 0.65rem 0;
  border-top: 1px solid var(--line);
  align-items: baseline;
}
.detail:first-of-type { border-top: none; }
.detail .k {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--ink-soft);
  flex: 0 0 5.5rem;
}
.detail .v { font-weight: 600; }
.detail .v a { color: var(--pine); text-decoration: none; }
.detail .v a:hover { text-decoration: underline; }

/* ⚠️ WCAG 1.4.10 REFLOW — AA, AND IT WAS FAILING. FJC-40.
   At a 320px viewport the whole document scrolled sideways: 376px wide, with
   .find-grid resolving to a single 363px track. The cause is the email address.
   It is one unbreakable 23-character token, so the grid item's min-content width
   could not go below it, and a grid item defaults to min-width:auto and will not
   shrink past that. Everything else on the page was innocent.

   ⚠️ NOT INTRODUCED HERE. index.html has carried the same .find-card since
   FJC-32 and measured identically — 376px against a 320px viewport. FJC-40
   reuses the component, which is how it surfaced. Fixing it in the shared rule
   fixes both pages at once, which is the point of it being shared.

   ⚠️ `anywhere`, NOT `break-word`, AND THAT IS THE OPPOSITE OF THE CALL MADE FOR
   TABLE CELLS AT FJC-41 — for exactly the same reason. Only `anywhere` is
   counted when the browser computes min-content width, and min-content is
   precisely what is oversized here; `break-word` would wrap the text visually
   and leave the track 363px, i.e. fix the symptom and not the overflow. Table
   prose wanted the opposite, because there `anywhere` split ordinary words. An
   email address is a token with no word boundaries to respect, so breaking it
   anywhere is both necessary and harmless.

   ⚠️ Nothing automated would have caught this. pa11y passes these pages, and it
   does not test reflow at 320px; the four CI gates do not render. Measured with
   documentElement.clientWidth, NOT innerWidth — at devicePixelRatio 2 innerWidth
   read 376 while the layout viewport was genuinely 320, which would have hidden
   the failure behind a passing-looking comparison. */
.detail .v { overflow-wrap: anywhere; }

/* FJC-40. .cta-row is scoped to .involve and does nothing anywhere else, so the
   Contact card's buttons would have stacked as inline anchors with no gap. A
   separate rule rather than widening `.involve .cta-row`: the home page's row
   sits under a full-width section head and carries a 1.8rem lead-in, which is
   too much inside a card. Same component, different spacing, so different rule.
   ⚠️ Found by rendering the page, not by reading the stylesheet — nothing in CI
   lints CSS, and an unstyled class is invisible to every gate. (FJC-168 renders
   at 320px, so it catches a CSS error that breaks layout — but not a dead rule.) */
.find-card .cta-row { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-top: 1.5rem; }

/* ⚠️ Text sits on this gradient-over-pattern. FJC-26 could not measure text
   over client-uploaded photographs and recorded it unresolved; this backdrop
   is ours and is dark enough, but any template that puts text on a REAL
   photograph needs a scrim (--scrim-pine). Do not copy this pattern onto an
   <img> and assume it carries over. */
.find-visual {
  border-radius: var(--r-4xl);
  overflow: hidden;
  position: relative;
  min-height: 260px;
  background:
    linear-gradient(135deg, var(--scrim-pine), var(--scrim-pine-deep)),
    repeating-conic-gradient(var(--sq-light) 0% 25%, var(--sq-dark) 0% 50%) 0 0 / 68px 68px;
  display: grid;
  place-items: center;
  text-align: center;
  color: var(--on-pine);
  padding: 2rem;
}
.find-visual .fknight { font-size: var(--fs-knight); line-height: 1; color: var(--brass-lite); }
.find-visual h2 { font-family: var(--f-display); color: var(--card); font-size: var(--fs-2xl); margin: 0.6rem 0 0.3rem; }
.find-visual p {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--brass-lite);
}
.find-visual .btn { margin-top: 1.3rem; }

/* ── Article masthead ───────────────────────────────────────────────────── */
.masthead {
  background: linear-gradient(160deg, var(--pine) 0%, var(--pine-700) 88%);
  color: var(--on-pine);
  padding: var(--sp-masthead-top) 0 var(--sp-masthead-bottom);
}

/* ⚠️ THE BASE IS THE LIGHT SURFACE, AND THE DARK ONE OVERRIDES. FJC-36 flipped
   this, because the crumb had only ever appeared inside .masthead and was
   written in --on-pine-crumb accordingly. The Learn child pages put a
   breadcrumb on an ordinary .section, and light-on-light measured as a real
   axe colour-contrast violation on all six of them.

   Flipping matches how .eyebrow already works — base tuned for the light
   surface, inverted under .masthead and .section--pine. The article template is
   unaffected: its crumb sits inside .masthead and picks up the override. */
.crumb {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--ink-soft);
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
}
.crumb a { color: var(--pine); text-decoration: none; }
.crumb a:hover { text-decoration: underline; }

.masthead .crumb { color: var(--on-pine-crumb); }
.masthead .crumb a { color: var(--brass-lite); }

.masthead .cat { margin-top: 1.4rem; }
.cat-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  font-weight: 700;
  color: var(--ink);
  background: var(--brass);
  padding: 0.35rem 0.7rem;
  border-radius: var(--r-pill);
}

.masthead h1 { color: var(--card); font-size: var(--fs-h1-article); margin: 1.1rem 0 0; max-width: 20ch; }

.byline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.1rem;
  margin-top: 1.6rem;
  font-size: var(--fs-sm);
  color: var(--on-pine-soft);
}
.byline .who { display: flex; align-items: center; gap: 0.6rem; font-weight: 600; color: var(--card); }
.byline .av {
  width: 2rem;
  height: 2rem;
  border-radius: var(--r-circle);
  display: grid;
  place-items: center;
  flex: none;
  background: radial-gradient(circle at 32% 28%, var(--buff-lite), var(--emblem-lo));
  color: var(--pine-700);
  font-size: var(--fs-md);
}
.byline .sep {
  width: 4px;
  height: 4px;
  border-radius: var(--r-circle);
  background: var(--brass-lite);
  opacity: 0.7;
}
.byline .meta { font-family: var(--f-mono); font-size: var(--fs-label-lg); letter-spacing: 0.04em; }

/* ── Article body ───────────────────────────────────────────────────────── */
.article { padding-bottom: var(--sp-section-sm); }
.lead-fig { width: min(var(--measure-wide), 94%); margin: -4.5rem auto 0; position: relative; z-index: 2; }

figure { margin: 2.4rem 0; }
figure.inline { width: min(var(--measure), var(--gutter)); margin-inline: auto; }
figure.inline--wide { width: min(var(--measure-wide), 94%); }   /* · INLINED · art:592 */

figcaption {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.03em;
  color: var(--ink-soft);
  margin-top: 0.7rem;
  text-align: center;
}

.fig-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.fig-duo--wide { width: min(var(--measure-wide), 94%); margin: 2.4rem auto; }  /* · INLINED · art:500 */

/* FJC-59: three illustrations read as a SEQUENCE, so they sit in a row — the
   opening-tree stages only teach anything when you can see one against the next.
   ⚠️ Collapses at 560px like .fig-duo above, deliberately the same breakpoint:
   three columns on a phone would be ~90px each, which passes Reflow (the images
   are max-width:100%) while being useless. Passing the gate and being legible
   are different things. */
.fig-trio { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; width: min(var(--measure-wide), 94%); margin: 2.4rem auto; }
.fig-trio figure { margin: 0; }

.photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--r-4xl);
  display: block;
  border: 1px solid var(--line);
  box-shadow: var(--sh-card);
  background: var(--pine-tint);
}
.photo--16x9 { aspect-ratio: 16 / 9; overflow: hidden; border-radius: var(--r-4xl); }  /* · INLINED · art:593 */
.lead-fig .photo { aspect-ratio: 16 / 9; border-radius: var(--r-4xl); overflow: hidden; }
.lead-fig .photo img { border-radius: var(--r-4xl); }
.fig-duo .photo { aspect-ratio: 3 / 2; overflow: hidden; border-radius: var(--r-4xl); }

/* Masonry, so varied portrait heights flow without cropping faces. */
.gallery-wrap { width: min(var(--measure-wide), 94%); margin: 2.4rem auto 0; }
.gallery-wrap > h3 {
  font-family: var(--f-display);
  font-size: var(--fs-xl);
  font-weight: 700;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-bottom: 1.3rem;
}
.gallery-wrap > h3 .glyph { color: var(--brass-ink); font-size: var(--fs-xl); }
.gallery { column-count: 4; column-gap: 1rem; }
.gallery figure { break-inside: avoid; margin: 0 0 1rem; }
.gallery img {
  width: 100%;
  border-radius: var(--r-3xl);
  display: block;
  border: 1px solid var(--line);
  box-shadow: var(--sh-card);
  background: var(--pine-tint);
}
.gallery figcaption {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  margin-top: 0.55rem;
  text-align: left;
  line-height: 1.4;
}
.gallery figcaption b { color: var(--pine); font-weight: 700; }

.prose { font-size: var(--fs-prose); line-height: var(--lh-prose); color: var(--ink-prose); }
.prose > p { margin: 1.35rem 0; }
/* ── The lede — FJC-199 ───────────────────────────────────────────────────────
   ⚠️⚠️ THIS WAS `.prose > p:first-of-type` AND IT RENDERED 171 DROP CAPS. Counted
   in a browser by asking whether ::first-letter actually floats, not by grepping
   the stylesheet. 38 on tactics.html, 31 on endgames, 26 on opening-lines, 21 on
   getting-started, 16 on blunders, 5 on about.

   The cause is that prose is authored as one .measure.prose block per section —
   173 of them — so a rule meaning "the start of an article" fired at the start of
   every fragment. It is a 57.6px floated capital and an enlarged paragraph, so
   each section opened as though it were a new article.

   ⛔⛔ NO STRUCTURAL SELECTOR CAN EXPRESS THIS, WHICH IS WHY IT IS AN OPT-IN.
   :first-of-type matches the first <p> AMONG SIBLINGS wherever it sits, so it
   already fired under an <h2> (register.html) and under a <blockquote>
   (endgames.html) — not just at the top of a block. And "the first .prose on the
   page" takes tactics.html from 38 caps to ZERO, because its first .prose is the
   section index, which contains no <p> at all. Which paragraph is a page's lede
   is an editorial fact, not a positional one, so an author states it.
   ⛔ Do not reach for :has() here either. It would be this stylesheet's first,
   nothing in CI lints CSS, and it buys nothing a class does not. */
.prose--lede > p:first-of-type { font-size: var(--fs-lg); line-height: 1.6; color: var(--ink); }
.prose--lede > p:first-of-type::first-letter {
  font-family: var(--f-display);
  font-weight: 800;
  float: left;
  font-size: var(--fs-dropcap);
  line-height: 0.82;
  padding: 0.3rem 0.6rem 0 0;
  color: var(--pine);
}
.prose h2 { font-size: var(--fs-h2-prose); margin: 2.8rem 0 0; color: var(--ink); }
.prose h2 + p { margin-top: 0.9rem; }
.prose strong { font-weight: 700; color: var(--ink); }
.prose a { color: var(--pine); text-decoration: underline; text-underline-offset: 2px; }

.pull {
  width: min(760px, 94%);
  margin: 2.8rem auto;
  padding-left: 1.5rem;
  border-left: 4px solid var(--brass);
}
.pull p {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: var(--fs-pull);
  line-height: 1.28;
  color: var(--pine);
  letter-spacing: -0.01em;
}

/* ── Game slot — FJC-33. THIS IS NOT THE VIEWER. ──────────────────────────────
   ADR-005 builds a replayable board on vendored chess.js, and FJC-30 deliberately
   withheld the mockup's prototype styles (.game-head, .gb-*, .moves, .annot,
   .controls, .pgn-*) so epic FJC-4 is not boxed in by an implementation that
   works well enough to become permanent by accident. These class names are
   distinct from all of those on purpose — FJC-78 can build alongside and swap in.

   What FJC-33 owns is the SLOT: a <figure data-pgn> carrying the moves in a form
   that needs no JavaScript, with a <noscript> immediately after it. That pairing
   is consistency check 9's contract, and it is what takes the check off SKIP. */
.game-slot {
  width: min(760px, 94%);
  margin: 2.6rem auto;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-5xl);
  overflow: hidden;
  box-shadow: var(--sh-card);
}
.game-slot-head {
  background: var(--pine);
  padding: 1.1rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  flex-wrap: wrap;
  font-family: var(--f-display);
  font-weight: 700;
  font-size: var(--fs-md);
  color: var(--card);
}
.game-slot-head .result {
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  letter-spacing: 0.08em;
  color: var(--brass-lite);
}
/* The moves are readable with JavaScript off and with CSS off — they are page
   content, not a styled enhancement. When FJC-78 lands, the board replaces this
   block and the <noscript> below stops being belt-and-braces. */
.game-moves {
  margin: 0;
  padding: 1.2rem 1.5rem;
  background: var(--pine-900);
  color: var(--on-pine-code);
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  line-height: 1.7;
  overflow-x: auto;
  white-space: pre-wrap;
}
.game-slot figcaption {
  padding: 0.9rem 1.5rem;
  margin: 0;
  text-align: left;
  border-top: 1px solid var(--line);
}
/* ── The viewer — FJC-76. THIS IS the thing the block above is not. ──────────
   ⚠️⚠️ THESE RULES WERE WRITTEN AS A SEPARATE assets/css/chess-viewer.css AND
   MOVED HERE ON MEASUREMENT. Technical spec §8's repo tree names that file and
   the block above says the viewer's classes are deliberately absent — both were
   right when written, and AMD-015 corrects the tree.

   FJC-30 withheld .game, .gb-*, .moves, .mv, .controls and .sq.hl so that epic
   FJC-4 would not be boxed in by the mockup's prototype "working well enough to
   become permanent by accident". That reason is spent: this is the real
   implementation, built on vendored chess.js per ADR-005.

   The measurement that decided it, on the site's worst LCP page:

                          FCP      LCP
     a third stylesheet   1442ms   1959ms
     folded in here       1364ms   1959ms

   ⚠️ IT BOUGHT 78ms OF FCP AND NOTHING AT ALL ON LCP, so do not cite this as
   the reason that page passes FR-X13 — it does not carry that weight. What it
   removes is a real third RENDER-BLOCKING request, which Lighthouse named
   itself under `render-blocking-resources` at 306ms. ⭐ That 306ms is a
   HYPOTHETICAL the audit reports per resource, not a saving anyone collected;
   the honest figure is the 78ms above. A separate file for two pages of
   twenty-three still looks frugal and is not: 21 pages save ~1kB of shared,
   already-cached CSS while the two that matter pay a round trip on the
   critical path. .annot and .pgn-* are still absent — FJC-77's.

   ⚠️ Motion: nothing below declares a transition, animation or transform, so
   FR-V10 ("no transitions under prefers-reduced-motion") holds by absence
   rather than by an override. Stepping through a game is navigation, not
   decoration — an animated piece slide would put motion on the one control a
   reader uses forty times in a row. Do not add one. */

/* ⚠️⚠️ `is-live` IS ADDED BY JAVASCRIPT ONLY AFTER A BOARD HAS RENDERED, and
   that ordering is the whole of FJC-78's rule holding under failure. A PGN
   chess-viewer.js cannot parse never gets the class, so the authored <pre>
   above stays visible and the game is still readable. Do not rewrite this as
   `.game-slot .game-moves`, and do not put the class in the HTML. */
.game-slot.is-live .game-moves { display: none; }

/* Board and panel side by side, stacking at the site's existing 820px collapse
   point rather than a new breakpoint of this component's own.
   ⚠️ BOTH TRACKS ARE minmax(0, …), NOT 1fr. A grid item's default
   `min-width: auto` refuses to shrink below its content — that is how the
   footer overflowed at 320px on CI and passed on Windows (FJC-168). */
.game {
  display: grid;
  grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
  gap: 1.4rem;
  align-items: start;
  padding: 1.5rem;
  background: var(--foot-tint);
  border-top: 1px solid var(--line);
}
.game .board-frame { padding: 4%; }
.gb-panel { display: grid; gap: 1rem; align-content: start; min-width: 0; }

/* The move just played. An inset ring rather than a fill, so the square keeps
   its colour and the piece keeps its contrast against it. --sq-hl is dark by
   MEASUREMENT: FJC-26 recorded that white reaches only 1.35:1 against
   --sq-light, so no lightening of any hue clears 3:1 against BOTH squares.
   ADR-005 makes this the mechanism by which a move is shown, so it is a 1.4.11
   surface, not decoration. ::after rather than a border — a border would move
   the piece, because .sq is a grid whose single cell is the <svg>. */
.sq.hl::after {
  content: "";
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 3px var(--sq-hl);
  pointer-events: none;
}

/* ⚠️ NO tabindex ON THE SCROLL CONTAINER, deliberately. .table-wrap--wide gets
   one because its off-screen columns hold nothing focusable. Here every move IS
   a button, so tabbing already scrolls the list — and a focusable <ol> would
   add a stop that announces nothing. */
.moves {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.55rem;
  margin: 0;
  padding: 0;
  max-height: 14rem;
  overflow-y: auto;
}
.mv-pair { display: flex; align-items: baseline; gap: 0.25rem; }
.mv-no { font-family: var(--f-mono); font-size: var(--fs-micro); color: var(--ink-soft); }

/* min-height and min-width meet WCAG 2.2 SC 2.5.8's 24x24px target. FJC-79 owns
   that criterion for the widget as a whole; there is no reason to ship this half
   of it undersized and let that ticket fix it later. */
.mv {
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  min-height: 24px;
  min-width: 2.6rem;
  padding: 0.2rem 0.4rem;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
}
.mv:hover { background: var(--pine-tint); }
.mv.is-current { background: var(--pine); color: var(--card); font-weight: 700; }

/* ⚠️ .btn IS NOT REUSED HERE, DELIBERATELY. It carries transform:translateY(-2px)
   on hover — right for a call to action pressed once, wrong for a Next button
   pressed forty times, which would twitch under the cursor. It is also sized for
   a CTA: five .btn controls do not fit at 320px.
   ⚠️ AND .btn--ghost WOULD HAVE BEEN AN OUTRIGHT AA FAILURE — a dark-surface-only
   variant that only the TOKEN name marks as such. Dropped onto a light panel at
   FJC-44 it measured 1.07:1. This panel is light. */
.controls { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.ctl {
  font-family: var(--f-body);
  font-weight: 700;
  font-size: var(--fs-label-lg);
  min-height: 40px;
  padding: 0.5rem 0.85rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
}
.ctl:hover:not(:disabled) { background: var(--pine-tint); }

/* WCAG 1.4.3 exempts inactive controls and axe skips them for contrast, but
   --ink-soft on --chalk measures ~7:1 anyway; no reason for the exemption to be
   load-bearing. */
.ctl:disabled {
  background: var(--chalk);
  color: var(--ink-soft);
  border-color: var(--line);
  cursor: default;
}

/* Flip reports pressed state rather than being a fifth momentary button that
   looks identical whichever way the board faces. */
.ctl[aria-pressed="true"] {
  background: var(--pine);
  color: var(--card);
  border-color: var(--pine);
}

/* ── FJC-77 — the teaching note, the NAGs and the variations ──────────────────
   ⚠️⚠️ THE COMMENTARY IS THE CONTENT. FJC-77's own words: anyone can replay a
   game on Lichess; nobody else has Graham explaining why the knight move was a
   mistake to a nine-year-old. So the note gets the reading measure and the
   typography of prose, not the mono treatment the moves have.

   ⚠️ NO transition, animation or transform anywhere below — the same rule the
   block above this one records. Stepping through a game is navigation, and a note
   that fades in on every one of forty presses is motion on a control. */
.annot {
  display: grid;
  gap: 0.35rem;
  padding: 0.8rem 1rem;
  border-left: 3px solid var(--brass);
  background: var(--card);
  border-radius: 0 var(--r-lg) var(--r-lg) 0;
}
/* ⚠️ VISIBILITY, NOT display:none, and not a removed element. The panel keeps its
   box so the board and the move list do not jump every time the reader steps onto
   or off an annotated move — which on a heavily annotated game is most presses.
   FJC-81's assertion 17 measures exactly that kind of shift. */
.annot.is-empty { visibility: hidden; }

.annot-nag {
  margin: 0;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  font-weight: 700;
  color: var(--brass-ink);
}
.annot-nag:empty { display: none; }

.annot-note {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--ink);
}
.annot-note:empty { display: none; }

/* The glyph on a move button. ⚠️ aria-hidden in the markup — the words are in the
   button's accessible name, so this is decoration over the top of them. */
.mv-nag {
  font-weight: 700;
  color: var(--brass-ink);
  margin-left: 0.1em;
}
.mv.is-current .mv-nag { color: var(--brass-lite); }

/* A move carrying Graham's words gets a mark a reader can scan for. ⚠️ It is a
   BORDER, not a colour change: colour alone would be the only cue, which is
   WCAG 1.4.1. The NAG glyph beside it is the other, non-colour cue. */
.mv.has-note { border-bottom-color: var(--brass); }

/* ── Variations ──────────────────────────────────────────────────────────────
   Indented and ruled, so a branch reads as subordinate to the move above it
   without needing a colour to say so. */
.mv-var-host {
  flex-basis: 100%;
  margin: 0.15rem 0 0.35rem 0.6rem;
  padding-left: 0.7rem;
  border-left: 2px solid var(--line-strong);
}
.mv-var {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem 0.45rem;
  margin: 0.1rem 0 0;
  padding: 0;
}
.mv-var-lead {
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-mono);
  color: var(--ink-soft);
}
/* Nested lines get progressively smaller move text, which is the only cue that
   says "this is a branch of a branch" without another colour. */
.mv-var .mv { font-size: var(--fs-label); }

/* Its own row, and wider than the others because it carries a sentence. */
.ctl--main { flex-basis: 100%; }

.game-fallback {
  width: min(760px, 94%);
  margin: -1.6rem auto 2.6rem;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.03em;
  color: var(--ink-soft);
}

/* ── Results card ───────────────────────────────────────────────────────── */
.results {
  width: min(760px, 94%);
  margin: 2.6rem auto;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-5xl);
  overflow: hidden;
  box-shadow: var(--sh-card);
}
.results-head {
  background: var(--pine);
  padding: 1.1rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--f-display);
  font-weight: 700;
  font-size: var(--fs-md);
  color: var(--card);
}
.results-head .glyph { color: var(--brass-lite); font-size: var(--fs-xl); }
.results-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line); }
.ladder { background: var(--card); padding: 1.2rem 1.5rem; }
.ladder h3 {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 0.7rem;
}
.ladder .row { display: flex; align-items: baseline; gap: 0.7rem; padding: 0.28rem 0; }
.ladder .pos {
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  font-weight: 700;
  color: var(--brass-ink);
  width: 1.6rem;
  flex: none;
}
.ladder .row.first .pos { color: var(--pine); }
.ladder .name { font-weight: 600; }
.results-foot {
  background: var(--buff-lite);
  padding: 1rem 1.5rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--ink);
  border-top: 1px solid var(--line);
}
.results-foot .trophy { color: var(--brass-ink); font-size: var(--fs-lg); }

/* ── Download a poster — FJC-42 ─────────────────────────────────────────────
   Poster beside its own details, so the facts printed in the image are readable
   as text next to it rather than under a fold. ⚠️ The text half is not optional
   decoration: check 10 cannot read pixels, so those paragraphs are what keep
   this page's club facts inside the gate. See the comment in poster.html. */
.poster-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 2.2rem;
  align-items: start;
  margin-top: 2.4rem;
}
.poster-figure { margin: 0; }
/* Intrinsic 792×1024 is declared on the element for CLS; this caps the rendered
   size. `height: auto` is what stops the attributes forcing the aspect ratio
   once the width is constrained. */
.poster-figure img {
  display: block;
  width: 100%;
  max-width: 360px;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.poster-figure figcaption {
  margin-top: 0.6rem;
  font-size: var(--fs-sm);
  color: var(--ink-soft);
  max-width: 360px;
}
.poster-actions { margin-bottom: 0.7rem; }
.poster-meta {
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  color: var(--ink-soft);
}
.poster-note { font-size: var(--fs-sm); color: var(--ink-soft); margin-top: 0.8rem; }
.poster-detail h2 {
  font-family: var(--f-display);
  font-size: var(--fs-2xl);
  font-weight: 700;
  color: var(--pine);
  margin-top: 2rem;
}
.poster-facts { margin-top: 1rem; }
.poster-facts dt {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--brass-ink);
  margin-top: 0.9rem;
}
.poster-facts dd { margin: 0.15rem 0 0; color: var(--ink); }
/* ⚠️ The email is one unbreakable token and this is a grid item, so its
   min-content width would set the track and overflow at 320px — the exact FJC-40
   and FJC-168 failure, twice measured. `anywhere` rather than `break-word`
   because only `anywhere` counts toward min-content. */
.poster-facts dd a[href^="mailto:"] { overflow-wrap: anywhere; }

/* ── Prize winners — FJC-38 ─────────────────────────────────────────────────
   The three renderings FJC-49 requires. ⚠️ The privacy work is done by the
   MARKUP, not by these rules: an award list has no declared length, so absence
   cannot be counted. See the comment block in winners.html before changing the
   shape of this grid — a numbered list or a placing series would undo it while
   passing every gate.

   ⚠️ .winner--name is the one to be careful with. It is the "first name, no
   photograph" state, and it must read as a DESIGNED CARD beside a portrait,
   never as a picture that failed to load. Hence buff with a brass edge — the
   board's light square, a surface used elsewhere for things that are finished —
   rather than the neutral card white plus an empty box. If you find yourself
   adding a placeholder frame, a grey fill or an icon standing in for a missing
   photo, that is the failure this variant exists to prevent. */
.winners-season {
  font-family: var(--f-display);
  font-size: var(--fs-2xl);
  font-weight: 700;
  color: var(--pine);
  margin-top: 2.8rem;
}
.winners-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.2rem;
  margin-top: 1.4rem;
  list-style: none;
}
.winners-grid > li { display: flex; }
.winner {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-4xl);
  padding: 1.4rem;
}
.winner--name { background: var(--buff); border-color: var(--brass-border); }
/* Square, so a real photograph swapped in later keeps the row heights it has
   now. ⚠️ There is no photograph and there cannot be one — ADR-004 keeps them
   out of the repository and check 8 fails on any undeclared raster under site/,
   so this holds a Cburnett piece as a stand-in. */
.winner-portrait {
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  background: var(--pine-tint);
  border-radius: var(--r-md);
  margin-bottom: 0.35rem;
}
.winner-pc { width: 62%; height: 62%; }
.winner-award {
  font-family: var(--f-display);
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--pine);
}
/* The first name is the largest thing on the card on purpose: on .winner--name
   it is carrying the card alone, and a name set small beside a portrait card
   reads as a caption with its picture missing. */
.winner-name {
  font-family: var(--f-display);
  font-size: var(--fs-display-sm);
  line-height: 1.15;
  color: var(--ink);
}
.winner-note { font-size: var(--fs-sm); color: var(--ink-soft); margin-top: auto; padding-top: 0.5rem; }
.winners-foot {
  width: min(var(--measure), var(--gutter));
  margin: 2.2rem auto 0;
  font-size: var(--fs-sm);
  color: var(--ink-soft);
}

/* ── Notice callout ─────────────────────────────────────────────────────── */
.notice {
  width: min(760px, 94%);
  margin: 2.6rem auto;
  background: var(--pine-tint);
  border: 1px solid var(--panel-edge-cool);        /* FJC-26 [J]: was 1.17:1 */
  border-radius: var(--r-4xl);
  padding: 1.5rem 1.7rem;
  display: flex;
  gap: 1.1rem;
}
/* --fs-display-sm (1.6rem), not --fs-glyph (1.7rem). Those are separate steps:
   1.6px apart, above the 1px collapse tolerance, so they were not merged in
   tokens.css and must not be merged here either. */
.notice .glyph { font-size: var(--fs-display-sm); color: var(--pine); flex: none; line-height: 1; }
/* FJC-38 added the h2 arm. The FJC-41 policy pages put their notice after an h1
   and before an h2, so its heading is an h3 there and the document outline
   already skips a level — `heading-order` is advisory, so it is reported and not
   failed. winners.html has no such constraint, and a page built new had no reason
   to inherit the skip: its notice heading is an h2, so the outline runs
   h1 → h2 → h2 → h3 cleanly. Same rule, so the two look identical; do not "tidy"
   this back to one selector without checking which heading level each page
   actually uses.
   ⚠️ THIS COMMENT SAID "the four FJC-41 policy pages" AND "17 pages" UNTIL
   11 AUGUST 2026, THEN "THREE pages use the h3 arm now" UNTIL 15 AUGUST — in a
   comment whose own last line says not to write a number here. It was wrong again
   within days: register.html shipped with an h3 notice on 15 August, and FJC-173
   then moved accessibility.html's and safeguarding.html's to the h2 arm.
   ⭐ THE NUMBER IS GONE FOR GOOD THIS TIME. Both arms are live and which page uses
   which is a property of that page's outline, not a fact worth restating here.
   ⛔ FJC-173 took `heading-order` to ZERO, so a new notice heading is no longer
   free: put it at the level its page's outline actually needs, and re-run
   `npm run check:a11y` — the advisory list is empty now, so a regression shows. */
.notice h2,
.notice h3 { font-family: var(--f-display); font-size: var(--fs-md); font-weight: 700; margin-bottom: 0.35rem; }
.notice p { color: var(--ink-panel); font-size: var(--fs-base); margin: 0.35rem 0; }
.notice strong { color: var(--pine); }

/* ── Tags, share, author, post navigation ───────────────────────────────── */
.tags {
  width: min(var(--measure), var(--gutter));
  margin: 2.8rem auto 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  align-items: center;
}
.tags .lbl,
.postnav .dir {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* FJC-26 [B]: --line-strong, not --line. Where the border IS the component
   boundary, 1.4.11 applies at 3:1 and --line manages only 1.28:1. FJC-26 listed
   five such components; .share a is gone (see below) and .controls button and
   .pgn-toggle belong to the viewer in epic FJC-4, so .tag and .postnav a are the
   two that exist today. */
.tag {
  font-family: var(--f-mono);
  font-size: var(--fs-label-lg);
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  color: var(--pine);
  text-decoration: none;
  background: var(--card);
}
/* ⚠️ Hover and the transition are scoped to the ANCHOR form only. FJC-33 renders
   tags as <span> because no tag index exists to link to, and a hover affordance
   on something that cannot be clicked is a lie the pointer tells. When a tag
   index lands, switching the span to an <a> picks the interaction back up with
   no other change. */
a.tag { transition: border-color var(--t-base), background var(--t-base); }
a.tag:hover { border-color: var(--brass); background: var(--buff-lite); }

/* ⚠️ THE SHARE ROW IS DELIBERATELY ABSENT, not missing. The approved mockup
   carried four share buttons on href="#". Dropped by Peter as a supplier
   decision on 27/07/2026 (FJC-33): real share targets would add third-party
   origins to a site whose whole posture — ADR-003's no-analytics, the cookieless
   position, the CSP work — is built on having none. Its CSS is removed rather
   than left orphaned. Reinstating it is a design decision, not a restoration. */

.author {
  width: min(var(--measure), var(--gutter));
  margin: 2.5rem auto 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-5xl);
  padding: 1.6rem;
  display: flex;
  gap: 1.2rem;
  align-items: flex-start;
}
.author .av {
  width: 3.6rem;
  height: 3.6rem;
  border-radius: var(--r-circle);
  flex: none;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 32% 28%, var(--buff-lite), var(--emblem-lo));
  color: var(--pine-700);
  font-size: var(--fs-glyph-lg);
  box-shadow: inset 0 0 0 2px var(--brass);
}
/* FJC-55: the avatar slot can hold a photograph instead of a piece glyph. The
   gradient and the inset ring both go — an inset box-shadow does not paint over
   replaced content, so the brass edge is a real border on the image instead, and
   box-sizing: border-box keeps it inside the 3.6rem circle. ⚠️ Only the modifier
   is new; .av keeps its size, shape and flex behaviour. */
.author .av--photo { background: none; box-shadow: none; }
/* ⚠️ THE <picture> HAS TO BE SIZED OR THE IMAGE COLLAPSES TO ~4px. `picture` is
   display:inline by default, so the img's height:100% resolves against an inline
   box with no height. Caught in the browser — it renders as a tiny dot and no
   gate says a word: html-validate, axe, the CSP check and Reflow all pass on a
   4px avatar, because it is valid, accessible, same-origin and narrow. */
.author .av--photo picture { display: block; width: 100%; height: 100%; }
.author .av--photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--r-circle);
  border: 2px solid var(--brass);
}
.author .n { font-family: var(--f-display); font-weight: 700; font-size: var(--fs-md); }
.author .r {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--brass-ink);
  margin: 0.15rem 0 0.6rem;
}
.author p { color: var(--ink-soft); font-size: var(--fs-sm); }

.postnav {
  width: min(var(--measure), var(--gutter));
  margin: 2.5rem auto 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}
.postnav a {
  text-decoration: none;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-3xl);
  padding: 1.1rem 1.3rem;
  background: var(--card);
  transition: border-color var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast);
}
.postnav a:hover { border-color: var(--brass); transform: translateY(-2px); box-shadow: var(--sh-lift); }
.postnav .ttl { font-family: var(--f-display); font-weight: 700; margin-top: 0.4rem; color: var(--ink); line-height: 1.2; }
.postnav .next { text-align: right; }

/* ── Related ────────────────────────────────────────────────────────────── */
.related { background: var(--pine-tint); padding: var(--sp-section-sm) 0; }
.related .head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  max-width: none;
}
.related h2 { font-size: var(--fs-h2-related); color: var(--ink); }

/* ── Policy pages — FJC-41 ────────────────────────────────────────────────────
   The shared shell behind /privacy.html, /accessibility.html and /cookies.html,
   in the house style FR-P1 asks for: numbered § sections, plain English, tables,
   a "Last updated" line. FJC-48's Safeguarding page is the fourth user and is
   deliberately NOT built here — see the note in those pages' markup.

   ⚠️ THIS TICKET BUILDS THE SHELL, NOT THE POLICIES. The words are owned by
   FJC-108 (privacy), FJC-109 (cookies) and FJC-98 (accessibility), the same
   layout/copy split as FJC-35 and FJC-55. FJC-161 states it in terms: "FJC-33 to
   FJC-41 are templates and functional structure, not content."

   ── THIS IS THE SITE'S FIRST TABLE ──────────────────────────────────────────
   There was no <table> anywhere under site/ before this ticket and no table CSS
   at all, so everything below is new rather than adapted. Two behaviours, and
   the distinction is the whole of the "tables reflow on mobile" criterion:

     .table-wrap          the table fills the column and wraps its cells. Two and
                          three-column tables genuinely REFLOW at 320px — no
                          horizontal scrolling, which is what WCAG 1.4.10 wants.
     .table-wrap--wide    four columns or more. Crushing those into 294px makes
                          a column about 70px wide and the table unreadable, so
                          this holds a min-width and lets the wrapper scroll.

   ⚠️ A scroll container must be reachable by keyboard, or a keyboard-only user
   cannot see the columns that are off-screen. The --wide wrapper therefore
   carries tabindex="0" role="region" aria-labelledby=<caption id> in the markup.
   The plain wrapper deliberately does NOT: it never scrolls, and a focusable
   element that does nothing is just an extra tab stop. If you add a fourth
   column to a plain table, move it to --wide and add those three attributes.

   ⚠️ Reflow was NOT done by restyling the table's parts as blocks. display:block
   on <tr>/<td> strips the row and cell semantics a screen reader announces, and
   the usual repair is to paint role="row"/role="cell" back on by hand — fragile,
   and invisible when it rots. Nothing here changes a table element's display.
   ============================================================================ */

/* ⚰️ `.policy > p:first-of-type` AND ITS ::first-letter RESET LIVED HERE AND WERE
   REMOVED BY FJC-199, BECAUSE THE THING THEY CANCELLED IS NOW OPT-IN. The reason
   they existed outlives them and is the point: the drop cap and enlarged opening
   paragraph are right for a news post and wrong for a legal notice, because the
   first line of a privacy notice is not a lede. That is still true — it is now
   expressed by these pages simply not asking for .prose--lede.
   ⚠️ Verified redundant rather than assumed: the three declarations were
   byte-identical to what .prose (line 1251) already gives, and the reset had
   nothing left to reset. Measured on privacy.html before and after — first
   paragraph 17.92px both ways, drop caps 0 both ways.
   ⛔ Do not reinstate this as a defence-in-depth. A cancel rule for a rule that no
   longer fires is a rule nobody can tell is dead. */

/* The status strip: "Last updated", and what state the document is in. */
.policy-meta {
  width: min(var(--measure), var(--gutter));
  margin: 1.8rem auto 0;
  padding: 0.9rem 1.1rem;
  background: var(--buff-lite);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.6rem;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}
.policy-meta b { color: var(--brass-ink); font-weight: 700; }

/* Section index. A policy is scanned, not read, so the § list is the entry
   point — and it doubles as the check that the numbering has not drifted.
   ⚠️ THE NAME IS HISTORICAL: THIS IS THE SITE'S INDEX COMPONENT, NOT A POLICY
   ONE. It is used on all four policy pages AND on four Learn pages, where the
   heading reads "On this page" and there are no § markers. FJC-199 brought
   learn/tactics.html in — it was the last page rendering its own index by hand,
   as a bare .measure.prose with a <ul>, so it had no landmark, no accessible
   name and none of this styling. ⛔ There is deliberately no `.policy-toc ul`
   rule: an index must be an <ol>, and a <ul> would silently keep its discs and
   lose the grid gap. Renaming the class to .page-toc was considered and declined
   — eight files touched for no rendered change. */
.policy-toc {
  width: min(var(--measure), var(--gutter));
  margin: 1.6rem auto 0;
  padding: 1.2rem 1.4rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.policy-toc h2 {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0 0 0.7rem;
}
.policy-toc ol { list-style: none; display: grid; gap: 0.3rem; }
.policy-toc a { color: var(--pine); text-decoration: none; font-size: var(--fs-sm); }
.policy-toc a:hover { text-decoration: underline; }
.policy-toc .s { font-family: var(--f-mono); color: var(--brass-ink); margin-right: 0.5rem; }

/* Numbered § headings. The marker is written out in the markup rather than
   generated by a CSS counter: the section index above repeats these numbers as
   link text, and a counter would renumber the heading while the index kept the
   old value — silently, and only on the page nobody reread. */
.policy h2 {
  font-family: var(--f-display);
  font-size: var(--fs-2xl);
  line-height: var(--lh-heading);
  margin: 2.6rem 0 0;
  color: var(--ink);
  /* ⚠️⚠️ 1.5rem UNTIL FJC-199, AND IT DID NOT CLEAR THE HEADER IT WAS WRITTEN
     FOR. The header is position:sticky and measures 69px at 1280 / 80px at 375
     against this 24px, so every § anchor on all four policy pages landed BEHIND
     it — measured -45px at 1280 and -57px at 375, on 27 of 27 links. A scroll
     margin that is present but too small is worse than none, because it reads as
     having been considered. Measure the header; do not pick a round number. */
  scroll-margin-top: 5.5rem;
}
.policy h2 .s { font-family: var(--f-mono); font-size: var(--fs-md); color: var(--brass-ink); margin-right: 0.55rem; }
.policy h3 { font-size: var(--fs-md); font-weight: 700; color: var(--ink); margin: 1.8rem 0 0; }
.policy ul { margin: 1.1rem 0; padding-left: 1.3rem; }
.policy li { margin: 0.45rem 0; }
/* ⭐⭐ THIS RULE WAS ABSENT ON A CORRECT MEASUREMENT, AND THE MEASUREMENT WAS
   INCOMPLETE. FJC-202, 18/08/2026. Read both halves before touching it.

   ── THE ORIGINAL FINDING, WHICH STANDS ──────────────────────────────────────
   FJC-108 §7 prints the Azure default hostname, and the obvious reading is that a
   45-character token must overflow a narrow column — FJC-168's footer failure
   exactly. A `break-word` rule was written on that reasoning, and mutation-testing
   it found it INERT.

   Measured in Chromium at a 320px viewport, 11 August 2026:
     unbroken token   379px
     column           271px
     line boxes       2, with overflow-wrap: NORMAL
     document         scrollWidth 320 = clientWidth 320, no horizontal scroll

   The token does not fit, and it did not need this rule to wrap: the hyphens in
   "polite-ground-…" are ordinary line-break opportunities under the standard line
   breaking algorithm, so the browser breaks there unaided. `break-word` changed
   nothing, and a CSS rule that changes nothing is invisible to every gate here —
   nothing in CI lints CSS. ⭐ All of that is still true at default spacing.

   ── WHAT THAT MEASUREMENT COULD NOT SEE ─────────────────────────────────────
   ⛔⛔ IT WAS TAKEN AT THE DEFAULT LETTER-SPACING, AND WCAG SC 1.4.12 IS THE
   CRITERION THAT LETS A READER CHANGE EXACTLY THAT. Add the 0.12em per character
   a reader is entitled to set and the longest hyphen-to-hyphen segment no longer
   fits the 271px column ON ITS OWN, so there is nowhere left to break and the
   token pushes the document wide.

   Measured 18/08/2026 under the 1.4.12 override at a 320px viewport:
     span.hostname    333px wide, right edge 357px
     document         scrollWidth 364 > clientWidth 320 — horizontal scroll

   ⭐ The old comment ended "DO NOT ADD ONE BACK WITHOUT MEASURING FIRST". That was
   the right instruction and this is that measurement — so the rule returns, on
   evidence rather than on the reasoning that was rejected in August.

   ⚠️ `break-word`, NEVER `anywhere`, for the reason the table rule below gives:
   `anywhere` counts toward min-content width and splits ordinary words, which is
   wrong for prose. reflow-check.test.js asserts that distinction executably.

   ⚠️⚠️ THE REFLOW GATE IS NOT THE INSTRUMENT FOR THIS ONE, and the old comment's
   closing line said it was. `check:reflow` measures the page AS AUTHORED and
   passes privacy.html at all three viewports either way. The instrument is
   `npm run evidence:spacing`, which applies the reader's override first. */
.policy .hostname { overflow-wrap: break-word; }

/* ── Tables ─────────────────────────────────────────────────────────────── */
.table-wrap { margin: 1.5rem 0; overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--card); }
.table-wrap:focus-visible { outline: 3px solid var(--focus-light); outline-offset: 2px; }
.table-wrap--wide table { min-width: 34rem; }

table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
caption {
  caption-side: top;
  text-align: left;
  padding: 0.9rem 1.1rem 0.6rem;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}
/* ⚠️ break-word, NOT anywhere. `anywhere` also lets the browser count mid-word
   break points when it computes the column's min-content width, so columns
   shrink harder and ordinary words split: at 320px this rendered "Not yet
   listed" as "Not yet liste / d". `break-word` breaks only when a word genuinely
   cannot fit, which still contains a long URL without splitting English.
   Caught in the browser at 320px — no linter here would have said anything. */
th,
td { text-align: left; padding: 0.75rem 1.1rem; border-top: 1px solid var(--line); vertical-align: top; overflow-wrap: break-word; }
thead th {
  background: var(--pine-tint);
  color: var(--ink);
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  border-top: 0;
}
tbody th { color: var(--ink); font-weight: 700; }
td { color: var(--ink-prose); }

/* ── The Chess Skills syllabus table (FJC-60) ────────────────────────────────
   /learn/skills.html only. The live WordPress page carried this syllabus as five
   SCREENSHOTS of a spreadsheet; it is a real table here, so it needed the two
   things a picture of a table never has to declare — how a list sits inside a
   cell, and how the video link sits inside a row header.

   ⚠️ THE VIDEO LINKS DELIBERATELY DO NOT USE --fs-label. Every other small label
   on this site does, and it is 0.72rem ≈ 11.5px — under Lighthouse's 12px
   legible-text threshold. There are thirteen of these links, so at label size
   they would have moved this page's legible-text percentage the way games.html's
   PGN move text moved its own (56.5%, the site's worst, and the only page the
   font-size advisory names). They inherit the table's --fs-sm instead, which
   costs nothing and looks the same at a glance. ⛔ Do not "tidy" them onto the
   label token to match the caption.

   ⚠️ font-weight is reset because tbody th is 700 — without it a link inside a
   row header renders bold, which reads as part of the section name rather than
   as something you can click.

   The colour and underline mirror .prose a exactly rather than inventing a
   variant; the table sits on --card, which is the surface .prose links are
   already contrast-checked against. */
.syllabus .vid {
  display: block;
  margin-top: 0.5rem;
  font-weight: 400;
  color: var(--pine);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.syllabus .vid + .vid { margin-top: 0.3rem; }
.syllabus td ul { margin: 0; padding-left: 1.15rem; }
.syllabus td li + li { margin-top: 0.3rem; }

/* ── Migrated artwork (FJC-61) ───────────────────────────────────────────────
   A single centred illustration in a prose column. Graham's trap and blunder
   pages are built around his own cartoons, and there was no rule for one
   picture on its own: .fig-trio is a three-up row, .photo sets height:100% and
   needs a sized parent, .poster-figure is the poster.

   ⚠️ NO height ON THE IMAGE, deliberately — it inherits the base `img` rule's
   `height: auto`. That rule only arrived on 11 August 2026, after a 440x546
   image constrained to 263px wide kept its 546px height and stretched. The
   lesson had been recorded on .poster-figure since FJC-42 and did not protect
   the class, so nothing here re-states it per-component. ⛔ Do not add a fixed
   height back.

   The width/height ATTRIBUTES stay on the markup — they reserve the box and
   keep CLS at zero, which is FJC-81's rootMargin doing the same job for boards. */
.fig-art { width: min(var(--measure), var(--gutter)); margin-inline: auto; }
.fig-art img { display: block; margin-inline: auto; border-radius: var(--r-lg); }

/* ⚠️⚠️ min(20rem, 100%), NOT 20rem — AND THE PLAIN VERSION SHIPPED AND WAS
   CAUGHT BY THE Reflow GATE. A cap written to make an image SMALLER made it
   bigger: `max-width: 20rem` is 320px, it overrides the base `img` rule's
   `max-width: 100%` by specificity, and at a 320px viewport the prose column is
   271px — so the 300px landmine and the 400px bomb painted straight through the
   right-hand edge and both pages scrolled horizontally. Measured: img 320px,
   right edge 345px, containing figure 271px.
   ⭐ Note which way it failed. The rule reads as a constraint and behaves as one
   at every width above about 350px, so it looks correct on a desktop and breaks
   only on a phone — which is the one place these pages will mostly be read.
   `min()` keeps the intent and cannot outrun the column. */
.fig-art--sm img { max-width: min(20rem, 100%); }

/* ⚠️⚠️ 20rem IS 320px CSS, WHICH IS ~560 DEVICE PIXELS ON THE PHONE LIGHTHOUSE
   EMULATES (412x823 at DPR 1.75). Several of Graham's migrated cartoons are
   smaller than that at source — the landmine is 300px, the ticking bomb 400,
   the treasure chest 262, the bishop 198 — and they were deliberately NOT
   upscaled, because upscaling a 300px drawing invents detail it never had.
   ⭐⭐ BOTH DECISIONS ARE RIGHT ON THEIR OWN AND WRONG TOGETHER: the cap asks
   for more pixels than the file has, and Lighthouse reports
   `image-size-responsive` — "Serves images with low resolution" — on two pages.

   ⛔ ACCEPTED, NOT MISSED. Director's decision, 11 August 2026. The fix is
   regenerating those images at roughly twice their display size, which spends
   LCP margin — and 324ms is the binding constraint on this site, on a budget
   only widened to 2.5s two days earlier because the margin had got tight.
   Trading measured headroom for an advisory on a handful of cartoons is the
   wrong way round. It is reported, not failing: outside FR-X12's floors.

   ⚠️ If you ever DO fix it, raise the source widths — do not lower this cap.
   Dropping to, say, 12rem would silence the audit by making every small figure
   too small to read, which is the audit getting its way rather than the reader. */

/* FJC-213 — the middle size, 30rem / 480px, for a news post's own artwork.
   Graham asked for the 21 August posters "a little smaller"; 480px is the
   director's choice off a rendered four-way comparison, not a computed
   optimum. It sits between --sm's 320px, which the director rejected in
   terms as "narrower than the text and a little lost", and the uncapped
   640px it replaces.

   ⚠️ min(30rem, 100%), NOT 30rem — the same trap as --sm above, and the
   reason that one is written this way. A bare rem cap beats the base img
   rule's `max-width: 100%` on specificity and paints through the right-hand
   edge of a 271px prose column at a 320px viewport. `Reflow` caught it there
   and would catch it here.

   ⭐⭐ THIS IS A CSS CAP AND NOT A RE-ENCODE, WHICH IS A DECISION, NOT A
   SHORTCUT. The recorded rule is that an <img> never upscales past its
   natural width, so WIDENING is an encode — the converse does not hold, and
   narrowing is free. Three reasons it is the right way round here:
     · Lighthouse measures a 412x823 phone at DPR 1.75, where this column is
       379px. The posters already render 379px there, so no cap at or above
       400px changes the measured page at all — FR-X13's 172ms of headroom,
       bought with seven measured variants at FJC-210, is untouched.
     · That phone wants ~758 device pixels and the 640px files already
       under-serve it. Re-encoding to 480 would make the one viewport that
       matters most WORSE in order to fix a desktop complaint.
     · Six DECLARED_RASTERS entries, the exiftool re-read and the
       avif < webp < jpg ordering all stay exactly as they were measured.
   ⛔ So do not "finish the job" by re-encoding these to 480px. The oversize
   file is deliberate headroom for high-density displays.

   ⚠️ The width/height ATTRIBUTES stay at the files' intrinsic 640x1065 and
   640x960. They are the aspect-ratio hint, not a display size — the box is
   reserved at the capped width from that ratio, and CLS stays at zero.
   Measured 0% distortion at 1280, 412 and 320. */
.fig-art--md img { max-width: min(30rem, 100%); }

/* ── Footer ─────────────────────────────────────────────────────────────── */
.site-footer { background: var(--pine-900); color: var(--on-pine); padding: 3.5rem 0 2rem; }
.footer-top { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 2rem; }
.footer-brand .brand { color: var(--on-pine); }
.footer-brand p { margin-top: 1rem; color: var(--on-pine-crumb); max-width: 34ch; font-size: var(--fs-sm); }

/* ⛔⛔ THIS WAS `.fcol h4` UNTIL FJC-173, 15 AUGUST 2026, AND THE SELECTOR HAD TO
   MOVE IN THE SAME EDIT AS THE MARKUP. An h4 after a page whose <main> stopped at
   h2 skipped a level, and axe reported it on 21 of 29 pages. h2 clears 14 of them;
   h3 only 12, because 404.html and games.html carry nothing but an h1.
   ⚠️ If the tag and this selector ever separate, the footer headings match nothing
   here, inherit no size, and lose the mono face, the uppercase and the brass — and
   EVERY GATE STILL PASSES. Nothing in CI lints CSS. Verified by capturing the
   computed style before and after and requiring it identical, which is the only
   instrument that can see it. */
.fcol h2 {
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-mono);
  text-transform: uppercase;
  color: var(--brass-lite);
  margin-bottom: 0.9rem;
}
.fcol a,
.fcol p { display: block; color: var(--on-pine-link); text-decoration: none; padding: 0.28rem 0; font-size: var(--fs-sm); }
.fcol a:hover { color: var(--card); }

/* ⚠️ THE SAME BUG AS `.detail .v`, IN THE FOOTER, AND IT HID FOR LONGER. FJC-168.
   `graham7morris@gmail.com` is a 22-character token with no break opportunity,
   so the Get in touch column's min-content width IS that address and the footer
   grid cannot shrink below it.

   ⭐ IT PASSED ON WINDOWS AND FAILED ON LINUX, which is the whole reason this
   was worth automating. Local font metrics rendered the address just inside the
   limit and all twenty pages measured exactly 320px — zero headroom, passing by
   luck. On the fonts CI renders with, the column's min-content is 212px, the
   footer grid overflows `.wrap`, and every page scrolls at 320px. The site uses
   `system-ui, sans-serif` with no webfont, so this is not a CI artefact: a
   visitor on Android or Linux got the horizontal scroll a Windows author could
   not see. FJC-40 fixed the contact card and had no way to know to look here.

   Scoped to the mailto: link rather than every `.fcol a`, so ordinary link text
   ("About the club") keeps normal word breaking — `anywhere` would let those
   split mid-word too. `anywhere`, not `break-word`: only `anywhere` is counted
   when the browser computes min-content, which is the measurement at fault. */
.fcol a[href^="mailto:"] { overflow-wrap: anywhere; }

/* ⛔⛔ THE SAME UNBREAKABLE-TOKEN FAILURE, IN A `.notice`, AND FOUND ON THE
   ACCESSIBILITY STATEMENT ITSELF. FJC-207, 19 August 2026.

   `/accessibility.html` §5 tells a reader how to report a problem, and FJC-98
   put `info@thecodeabides.co.uk` in a `.notice` to do it. A `.notice` is
   narrower than the prose column, and under the letter-spacing SC 1.4.12 lets a
   reader set, that address measures 235px inside it — unbreakable, because
   nothing gave it a break rule. The document goes to 327px in a 320px viewport.

   ⚠️ MEASURED IN BOTH ENGINES, and it is NOT the cross-engine difference it was
   found while looking for: Chromium +2032px and Gecko +2033px. FJC-205 went
   hunting for an engine disagreement on `privacy.html` — which holds — and
   turned this up on a different page instead.

   ⚠️⚠️ Scoped to `.notice`, NOT to every `a[href^="mailto:"]`. Measured: two
   mailto links on this site have the link text "Email Graham", and `anywhere`
   would let those two words split mid-word. The house pattern is to scope by
   container — `.fcol` above, `.poster-facts dd` earlier — and this follows it.

   `anywhere`, not `break-word`, for the reason the footer rule gives: only
   `anywhere` counts toward min-content, and min-content is the measurement at
   fault. ⛔ Do not "tidy" the three rules into one global selector. */
.notice a[href^="mailto:"] { overflow-wrap: anywhere; }

.footer-bottom {
  margin-top: 2.6rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--brass-border);
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1.5rem;
  justify-content: space-between;
  align-items: center;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.06em;
  color: var(--on-pine-faint);
}
.footer-bottom a { color: var(--on-pine-crumb); text-decoration: none; }
.footer-bottom a:hover { color: var(--brass-lite); }

/* FJC-41. Spec §5 groups Privacy, Accessibility and Cookies with the TCA credit
   rather than with the Explore column, so they belong on this row and not in a
   fourth .fcol. That also leaves the footer Explore column an exact mirror of
   the primary nav, which FJC-163 established and which is worth keeping: a
   future divergence then shows up as a difference rather than hiding in a
   longer list. Poster and Contents are named in spec §5 too and are absent —
   neither page exists, and linking a 404 would fail the Link check. */
/* Sized and coloured to match .footer-bottom, which is the row spec §5 groups
   these with. Left to inherit, they rendered at 17.2px — larger than every other
   link in the footer, because nothing in .site-footer sets a size and the .fcol
   rules do not reach outside their column. Measured, not eyeballed. */
.footer-legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1.2rem;
  margin-top: 2.4rem;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.06em;
}
.footer-legal a { color: var(--on-pine-crumb); text-decoration: none; }
.footer-legal a:hover { color: var(--brass-lite); text-decoration: underline; }
.footer-legal + .footer-bottom { margin-top: 1.2rem; }

/* ── Responsive ─────────────────────────────────────────────────────────── */
@media (max-width: 940px) {
  .hero-inner { grid-template-columns: 1fr; gap: 2.5rem; }
  .hero-board { order: -1; width: min(400px, 80vw); }
  .involve-inner { grid-template-columns: 1fr; gap: 1.8rem; }
  .footer-top { grid-template-columns: 1fr 1fr; }
  .footer-brand { grid-column: 1 / -1; }
}

@media (max-width: 820px) {
  /* FJC-76. The viewer stacks at the site's existing collapse point rather than
     one of its own. The board is capped so it does not fill a phone screen edge
     to edge, which would push the controls below the fold on every step. */
  .game { grid-template-columns: minmax(0, 1fr); padding: 1.1rem; }
  .game .board-frame { width: min(320px, 100%); margin-inline: auto; }

  /* ⚠️ visibility, not just transform. Verified in a browser on 27/07/2026:
     with transform alone the closed panel sits at top:-427px with
     visibility:visible, and all six links STAY IN THE TAB ORDER. A keyboard
     user tabbing past the toggle lands on "Home", "About" and the rest one by
     one, entirely off-screen, with the focus ring somewhere they cannot see.
     visibility:hidden removes them from the tab order; the delayed transition
     holds it until the slide-out finishes so the panel still animates away. */
  .nav {
    position: fixed;
    inset: 0 0 auto 0;
    top: 0;
    flex-direction: column;
    align-items: stretch;
    background: var(--pine-700);
    padding: 5rem 1.5rem 2rem;
    gap: 0.2rem;
    transform: translateY(-110%);
    visibility: hidden;
    transition: transform var(--t-slow) var(--ease),
                visibility 0s linear var(--t-slow);
    box-shadow: var(--sh-nav-panel);
    height: auto;
    /* ⚠️⚠️ WITHOUT THESE THREE LINES THE LAST NAV LINKS ARE UNREACHABLE AT 400%
       ZOOM, AND NO GATE ON THIS PROJECT COULD SEE IT. FJC-45, 13/08/2026.

       `height: auto` on a fixed panel means it grows to its content — measured
       at 453px — and WCAG SC 1.4.10 at 400% zoom on a 1280x1024 screen is a CSS
       viewport of 320 x 256. Measured in a browser at that size, with the panel
       open: last link "Find us" at bottom 421px, 165px below the fold,
       `overflow-y: visible`, the panel not internally scrollable, and
       `scrollIntoView` UNABLE to reach it — the document scrolls but a fixed
       panel does not move with it. That is loss of functionality, which is an
       AA failure, not a cosmetic one.

       ⛔ `tools/reflow-check.js` RUNS AT 320 x 800 AND IS A REQUIRED GATE, so it
       is green on this and always was. It measures the WIDTH half of 1.4.10; the
       height half had never been tested. ⚠️ Do not read a green Reflow run as
       "1.4.10 is covered".

       ⚠️ Two declarations on purpose: `100dvh` accounts for a mobile browser's
       collapsing address bar, and the `100vh` above it is what an engine without
       `dvh` takes. Order matters — the fallback must come first. */
    max-height: 100vh;
    max-height: 100dvh;
    overflow-y: auto;
  }
  .nav.is-open {
    transform: translateY(0);
    visibility: visible;
    transition: transform var(--t-slow) var(--ease), visibility 0s;
  }
  .nav a { padding: 0.85rem 1rem; font-size: var(--fs-base); border-bottom: 1px solid var(--brass-rule); }
  .nav a[aria-current="page"]::after { display: none; }
  .nav-toggle { display: inline-flex; z-index: 120; }
  .strip-inner { grid-template-columns: 1fr 1fr; }
  .sessions { grid-template-columns: 1fr; }
  .reassure-grid { grid-template-columns: 1fr; }
  .activities { grid-template-columns: 1fr 1fr; }
  .news-grid { grid-template-columns: 1fr; }
  .find-grid { grid-template-columns: 1fr; }
  /* FJC-38. Collapses with the others rather than at its own breakpoint — three
     award cards at 820px give a track too narrow for a first name set at
     --fs-display-sm, and a wrapped name is exactly the "looks like something
     went wrong" reading .winner--name exists to avoid. */
  .winners-grid { grid-template-columns: 1fr; }
  /* FJC-42. The poster keeps its aspect ratio, so two columns squeeze the text
     half long before the image stops being legible. */
  .poster-layout { grid-template-columns: 1fr; }
  .postnav { grid-template-columns: 1fr; }
  .gallery { column-count: 3; }
}

@media (max-width: 560px) {
  .fig-duo { grid-template-columns: 1fr; }
  .fig-trio { grid-template-columns: 1fr; }   /* FJC-59, same reason as .fig-duo */
  .gallery { column-count: 2; }

  /* FJC-168. 1.25rem each side is 40px of a ~150px track, and the strip's
     headings are bold display type that will not hyphenate. Measured at 320px:
     the worst item ("Completely free") had a min-content of 153px in a 152px
     track — passing by MINUS one pixel, which the +1px tolerance was absorbing.
     Trimming the padding is not the whole fix; see the 520px block. */
  .strip-item { padding: 1.25rem 0.85rem; }

  /* FJC-41. Cell padding is the whole budget on a narrow screen: 1.1rem each
     side costs ~35px of a 294px column, which a three-column table cannot
     spare. Measured at 320px, not guessed — see the ticket. */
  th,
  td { padding: 0.65rem 0.6rem; }
  caption { padding: 0.8rem 0.6rem 0.55rem; }
  .policy-meta { flex-direction: column; gap: 0.35rem; }
}

@media (max-width: 520px) {
  .activities { grid-template-columns: 1fr; }
  .hero-cta { flex-direction: column; align-items: stretch; }
  .btn { justify-content: center; }

  /* FJC-168. The facts strip was the last page still scrolling at 320px on the
     CI runner's fonts, and trimming its padding alone left only ~10% headroom —
     less than the ~13% by which those fonts render wider than the author's, so
     it would have been passing on the margin again rather than fixed.

     One column takes the worst item from a 152px track to 294px: headroom goes
     from MINUS 1px to +162px, 55%. That survives a font change, a translation,
     or somebody writing a longer fact — which is the point, because this strip
     carries real club copy that will change.

     ⚠️ Note this grid was the outlier: .reassure-grid, .news-grid, .find-grid
     and .sessions all collapse to one column at 820px already. The strip stayed
     at two all the way down to 320px, and nothing measured whether it fitted. */
  .strip-inner { grid-template-columns: 1fr; }
}

/* ⚠️ Motion is opt-out, and the hero reveal is the one that matters. It starts at
   opacity 0 under `@media (scripting: enabled)`, and nav.js checks THIS preference
   FIRST and adds `is-in` immediately, rather than waiting for an intersection a
   reader who does not scroll may never trigger. The reveal is one mechanism in
   three files (FJC-200) and this block is not part of it.

   ⛔⛔ THERE IS DELIBERATELY NO `.hero .hero-art` RESET HERE, AND THE ABSENCE IS
   MEASURED. This block carried `.hero .board { opacity: 1; transform: none; }`
   until 18/08/2026. FJC-200 renamed that component to `.hero-art` on 17/08 and its
   record says the dead `.hero .board` rules were "removed as DEAD, verified rather
   than assumed" — this third instance, inside the media query, survived that
   cleanup and matched 0 elements on all 29 pages. Deleted at FJC-97.

   ⛔ IT WAS NOT RE-POINTED AT `.hero-art`, AND THAT WAS THE DECISION RATHER THAN
   AN OVERSIGHT. Such a rule would be a CSS safety net for ONE GROUP of readers
   against a failure that blanks the hero for EVERYONE: if nav.js does not run, the
   artwork stays at opacity 0 whatever this preference says. A net that catches
   only reduced-motion readers would make the gap look handled while leaving it
   open for the majority — and it would have to be maintained as if it did
   something. FJC-200 already records the real exposure and where it lives.

   ⭐ Measured 18/08/2026 by `npm run evidence:motion-target`: under this
   preference the hero is at opacity 1 on all 29 pages, and 0 elements animate
   while 283 do without it. Re-run that before changing anything here. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *,
  *::before,
  *::after { transition: none !important; animation: none !important; }
}

/* FJC-159: the piece sprite defines <symbol>s only; nothing in it renders.
   Not display:none — that can stop <use> instantiating the symbol in some
   engines. Taken out of flow instead, which is inert and safe. */
.piece-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ══ PRINT — FJC-46 ═════════════════════════════════════════════════════════
   ⚠️⚠️ THE TICKET SAYS "carry the print styles from the presentation mockup into
   production" AND THERE ARE NONE TO CARRY. Measured 13/08/2026: the word "print"
   appears ZERO times in ferndown-junior-chess.html and once in
   ferndown-news-article.html, where it is the prose word "printed". No
   @media print, no @page, no media="print" link; the only external stylesheet in
   either mockup is Google Fonts. So this is AUTHORED, not ported — the FJC-181
   shape, and it is recorded because the ticket's provenance claim is false.

   ⭐⭐ WHAT IS ACTUALLY BROKEN ON PAPER, AND IT IS ONE THING: THE CHESS BOARDS
   PRINT BLANK. Every square is a CSS background (.sq.light / .sq.dark), and
   browsers drop backgrounds when printing unless the reader ticks "Background
   graphics", which is OFF by default. 2,304 authored squares across 9 pages, plus
   the runtime viewer's boards on 9 more, currently print as empty grids with
   pieces floating on them. On a chess club's site that is the whole game.

   ⭐ MEASURED RATHER THAN ASSUMED — printed to PDF in headless Chromium and the
   PDF's own fill operators counted, on /learn/getting-started.html (4 boards):

       default user print, CSS as shipped        0 light,   0 dark
       "Background graphics" ticked            240 light, 236 dark
       default print + print-color-adjust      240 light, 236 dark   <- this rule

   So the declaration genuinely overrides the reader's default; it is not merely
   a hint. ⚠️ The first detector reported 0 in ALL THREE cases because Chrome
   omits the leading zero in PDF colour operators — it writes `.9216`, not
   `0.9216`. A detector returning 0 for a hypothesis you already hold reads
   exactly like confirmation, and it nearly justified this change on false
   evidence.

   ⚠️⚠️ NOTHING IN CI HOLDS ANY OF THIS TRUE. No gate renders print — not
   check:a11y, not Reflow, not check:csp. A future component can break printing
   silently and no signal will say so. That is accepted, not overlooked: it is
   why this block is deliberately small and why the reasoning is here rather than
   on a ticket.

   ⚠️ Tokens only, never raw colour — consistency check 11 forbids a hex value
   anywhere outside tokens.css and would fail the build. */
@media print {
  @page { margin: 15mm; }

  /* ⛔ SCOPED, NOT GLOBAL. `* { print-color-adjust: exact }` is the tempting
     one-liner and it is wrong: it would faithfully reproduce every decorative
     surface, so a news article prints as a slab of full-bleed dark green and
     empties a toner cartridge. "Backgrounds honoured" has to mean the ones that
     CARRY INFORMATION — the board, and the photographs. */
  .board,
  .sq,
  .board .piece,
  img,
  picture,
  .photo {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* Site furniture. The brand stays: a printed sheet should say which club it
     came from, and it is the only provenance a piece of paper carries. */
  .skip,
  .nav,
  .nav-toggle,
  .site-footer { display: none; }

  .site-header { position: static; background: none; box-shadow: none; }

  /* ⚠️ The decorative sections are neutralised rather than reproduced, and the
     text inside them has to come with — several descendants set an on-pine
     colour explicitly, which would otherwise print white on white. */
  .section--pine,
  .section--tint,
  .section--buff { background: none; }
  .section--pine,
  .section--pine * { color: var(--ink); }

  /* The viewer's buttons do nothing on paper. ⚠️ `.moves` is deliberately NOT
     hidden: with the viewer live the authored <pre> is display:none, so the
     move list is the only readable record of the game left on the page. */
  .controls { display: none; }

  /* A diagram split across a page break is unreadable, and a position is the
     one thing on these pages that cannot be reassembled by the reader. */
  figure,
  .board-frame,
  .game-slot,
  .notice,
  .winner,
  table { break-inside: avoid; }
}
