/** base.css — shared structural rules for all titles.
    Do not add @import or brand colours here.
    Override with a {title}-overrides.css file that redefines the variables below. **/


/** COLOR AND FONT VARIABLES — override in {title}-overrides.css **/

:root {
    /* Semantic text colors */
    --main-color:              #1a1a1a;
    --second-color:            #2e2e2e;
    --third-color:             #1a1a1a;
    --forth-color:             #777777;
    --boxout-color:            #f0f0f0;
    --main-color-dark:         #f0f0f0;
    --second-color-dark:       #e0e0e0;
    --third-color-dark:        #e0e0e0;
    --forth-color-dark:        #aaaaaa;
    --boxout-color-dark:       #1e1e1e;

    /* Brand accent — must be defined in the override file */
    --accent:                  #e5001c;
    --accent-dark:             #ff2d45;

    /* Boxout border colour follows accent unless overridden */
    --boxout-border-color:     var(--accent);
    --boxout-border-color-dark:var(--accent-dark);

    /* Font scale */
    --font-smallest:           13px;
    --font-small:              15px;
    --font-standard:           17px;
    --font-medium:             22px;
    --font-large:              26px;
    --font-x-large:            32px;
    --font-xx-large:           42px;

    /* Font weights */
    --font-weight-exlight:     200;
    --font-weight-light:       300;
    --font-weight-standard:    400;
    --font-weight-bold:        600;
    --font-weight-x-bold:      700;
    --font-weight-xx-bold:     900;

    /* Font families — default: Barlow / Barlow Condensed (FFT).
       Override in title file. The chosen fonts' @font-face are INLINED at the bottom of the
       override file (the reader's importer can't follow @import — see /* FONTS */ block there). */
    --font-family-main:         "Barlow", "Helvetica Neue", Arial, sans-serif;
    --font-family-secondary:    "Barlow", "Helvetica Neue", Arial, sans-serif;
    --font-family-condensed:    "Barlow Condensed", "Arial Narrow", sans-serif;
    --font-family-main-display: "Barlow Condensed", "Arial Narrow", sans-serif;

    /* Layout */
    --side-pad:                1.25rem;
    --link-color:              #000000;

    /* ── STANDFIRST — default: NEUTRAL accent kicker, NO colour pill (Patrick, cycp 13-1
       2026-07-21: "standfirsts with a colour behind — unless it's in the mag"). The pill is
       per-title OPT-IN. Mechanism: phase-4 bakes --standfirst-bg/--standfirst-fg INLINE
       (!important, per-article accent) into every article — those two names are ALSO the
       generic "solid accent surface" pair used by widgets (quiz numbers, carousel arrows),
       so the rule below no longer reads them for the pill. It reads --standfirst-pill-bg /
       --standfirst-text instead, which the inline style never sets: default = no pill,
       accent-on-light text. A pill title (FFT / RecordCollector / SlimmingWorld) maps them
       back (--standfirst-pill-bg: var(--standfirst-bg) etc.) at BODY scope in its override
       so the per-article inline accent still drives the pill colour. ── */
    --standfirst-display:      inline-block;
    --standfirst-font:         var(--font-family-condensed);
    --standfirst-size:         var(--font-smallest);
    --standfirst-weight:       500;
    --standfirst-fg:           #fff;
    --standfirst-bg:           var(--accent);
    --standfirst-tracking:     0.14em;
    --standfirst-transform:    uppercase;
    --standfirst-padding:      0;      /* pill titles restore their own padding/radius */
    --standfirst-radius:       0;
    /* Align the standfirst's LEFT EDGE with the body/headline text column (both self-pad
       with --side-pad). A plain kicker's text then sits exactly on the column; a pill's box
       edge sits on the column (its small internal padding insets only the pill text). An
       earlier flush-0 experiment pinned the pill to the screen edge in-app (Patrick, FFT
       retrofit 2026-07-22 — measured: box at x=0 vs text column at x=20). */
    --standfirst-margin:       1rem 0 0.75rem var(--side-pad);

    /* ── HEADLINE — default: FFT condensed, no transform ── */
    --headline-weight:         var(--font-weight-x-bold);
    --headline-transform:      none;
    --headline-tracking:       -0.01em;
    --headline-line-height:    1.02;

    /* ── LEAD / DECK ── */
    --lead-font:               var(--font-family-main);
    --lead-weight:             var(--font-weight-standard);
    --lead-style:              normal;
    --lead-line-height:        1.4;
    --lead-color:              var(--second-color);

    /* ── PULL QUOTE — default: FFT condensed bold italic, left-aligned ── */
    --pull-quote-font:         var(--font-family-condensed);
    --pull-quote-weight:       var(--font-weight-xx-bold);
    --pull-quote-style:        italic;
    --pull-quote-color:        var(--accent-on-light, var(--accent));   /* luminance-safe on white */
    --pull-quote-align:        left;
    --pull-quote-padding:      1.25rem var(--side-pad);
    --pull-quote-margin:       1.75rem 0 1.5rem;
    /* NO rules above/below by default (Patrick, cycp 13-1 2026-07-21: accent lines around
       every pull quote read as borrowed furniture — a title whose print HAS them opts in,
       e.g. TheWeek sets 3px in its override). */
    --pull-quote-border-width: 0;
    --pull-quote-border-color: var(--accent);

    /* ── DROPCAP ── */
    --dropcap-size:            4.4em;
    --dropcap-weight:          var(--font-weight-xx-bold);

    /* ── BOXOUT BORDER — default: top-only (FFT style) ── */
    --boxout-border:           0;
    --boxout-border-top:       3px solid var(--boxout-border-color);

    /* ── TOC SECTION HEADER — default: thin bottom rule + colored span bg (FFT) ── */
    --toc-section-border-bottom:  1px solid var(--main-color);
    --toc-section-border-pb:      0.6rem;
    --toc-section-span-display:   inline-block;
    --toc-section-span-font:      var(--font-family-condensed);
    --toc-section-span-size:      var(--font-small);
    --toc-section-span-weight:    var(--font-weight-x-bold);
    --toc-section-span-tracking:  0.2em;
    /* Contents section headers default to PLAIN accent text — the blocky accent pill is the
       per-title EXCEPTION, not the norm (Patrick, TNR QA 2026-08-10: print is just red text;
       same ruling as the standfirst pill, and TEG + TheWeek had already opted out per-title
       before the default caught up). A title whose print DOES box its contents sections sets
       these three vars back in its override (bg: var(--accent); fg: #fff; padding: 5px 12px 6px). */
    --toc-section-span-bg:        transparent;
    --toc-section-span-fg:        var(--accent-on-light, var(--accent));
    --toc-section-span-padding:   0;

    /* ── TOC TYPOGRAPHY ── */
    --toc-title-font:          var(--font-family-condensed);
    --toc-title-weight:        var(--font-weight-x-bold);
    --toc-title-transform:     none;   /* match the mag's printed case (phase-2 captures the title
                                          verbatim); was uppercase — WCM contents is sentence case in
                                          print (Patrick 2026-07-24). A title wanting caps sets it in
                                          its own override. */
    --toc-title-tracking:      0;
    --toc-sell-font:           var(--font-family-main);
    --toc-sell-weight:         var(--font-weight-standard);
    --toc-sell-style:          normal;
    --toc-placeholder-bg:      #f2f2f2;
}


/** STANDARD SOURCE STYLING **/

.masthead   { display: none; }
.backtotop  { display: none; }
.ld_bold    { font-weight: 700; }
.ld_italic  { font-style: italic; }


/** MAGCLONER STYLING **/

.flexy { display: flex; flex-direction: column; }
.flexy > * { order: 2; }
.flexy .article-standfirst { order: 1; }

/* Top clearance for the app chrome (Mark + Patrick, 2026-07-22): top bar + iOS status bar
   measured in-app 2026-07-22: 90px sat ~8px low, so 82px (the app can dynamically ADD a small
   nudge if a device needs more — never subtract — which keeps old issues safe). Was 64px.
   margin: 0 kills the UA's default 8px body margin — with the overflow-x clip, full-bleed
   heroes were clipping at the body box 8px short of the screen edge (Patrick's RM screenshot:
   "the article doesn't go fully edge to edge"). Text is unaffected (elements self-pad with
   --side-pad); heroes now bleed to the true frame edge. */
/* padding-bottom matches the base body rule above (was 64px, which is less than the reader's own
   bottom bar leaves room for) — see the BOTTOM CLEARANCE note there. */
body.connect-mobile-edition { margin: 0; padding-top: 90px; padding-bottom: max(20vh, 96px); }
/* COVER pages deliberately have NO connect-mobile-edition (reading padding would inset the
   artwork), so the artwork slid under the app's opaque top bar — "a white bar above every
   cover" on every issue (Patrick, cyp 2026-08-06). Give the cover body the SAME top clearance
   so the artwork starts exactly where the chrome ends. One value, tunable: 90 matches the
   app-bar clearance above (82 is where a scrim's dark panel aligns — nudge down if it reads
   low in-app). !important: the app's injected title-level CSS also pads plain body. */
body.interactive-cover { margin: 0 !important; padding-top: 90px !important; }
/* NON-scrim articles open with TEXT, which sat flush under the app bar (a scrim's dark
   panel aligns perfectly at 82px — text needs breathing room; Patrick, RM Welcome
   2026-07-22). :has is additive-safe: an old webview without it keeps 82px. */
body.connect-mobile-edition:not(:has(.article-hero--scrim)) { padding-top: 114px; }
/* In-app LANDSCAPE reading width (Mark ceded viewport control, 2026-07-22): the app no
   longer insets the webview in landscape — we reproduce his exact insets in CSS so the
   reading column stays narrow while full-bleed art (scrims, heroes, gallery strips — the
   negative --side-pad margins) keeps the WHOLE frame. His values: phones 120pt each side,
   iPad 10% each side; portrait keeps the stock --side-pad (+ the ≤2.49dppx 730px text cap
   above already guards tablet/desktop line length). Scoped to the app class — QA/web
   reader/EPUB readers unaffected. */
/* PHONES only (DPR≥2.5): the app gives full viewport in landscape, so we inset heavily to keep a
   narrow column while art bleeds full-frame. TABLETS (iPad, DPR 2) are deliberately excluded — the
   max-reading-width cap (640px, below) does the narrowing there; a 120px/10vw pad ON TOP of the cap
   would eat the column to a sliver (Patrick, iPad mini landscape 2026-07-24). */
@media (orientation: landscape) and (max-width: 1023px) and (min-resolution: 2.5dppx) {
  body.connect-mobile-edition { --side-pad: 120px; }
}
@media (orientation: landscape) and (min-width: 1024px) and (min-resolution: 2.5dppx) {
  body.connect-mobile-edition { --side-pad: 10vw; }
}
/* TABLET PORTRAIT (iPad, DPR≤2.49): a bit more left breathing room than the phone 1.25rem so the
   column doesn't hug the edge (Patrick: BBC-style — more left pad, right gap; the 640 cap + this
   give ~76% width on an iPad mini). Images stay full-bleed (negative --side-pad margin). */
@media (min-width: 640px) and (orientation: portrait) and (max-resolution: 2.49dppx) {
  body.connect-mobile-edition { --side-pad: 2.25rem; }
}
/* In LANDSCAPE the reading column indents via --side-pad (120px/10vw), but three elements carry
   their OWN side geometry and stayed at the screen edge (Patrick, iPhone 16 Pro Max, WCM):
   - the standfirst's margin comes from a title override with a LITERAL left value (WCM 20px);
   - pull quotes span full width by design on a portrait phone (--pull-quote-margin sides 0);
   - boxouts use fixed 1.5rem margins + a width calc.
   Re-key all three to --side-pad in the app's landscape only. GATED TO PHONES (min-resolution:2.5dppx):
   on TABLET the reading-measure cap + each element's own inset already align them, and this override
   fought that (pull-quote pushed off-centre, +0.75rem) — Patrick, iPad 2026-07-24. Phone landscape
   (120px side-pad) still needs it. Pull quotes get a fractional extra indent on phone. */
@media (orientation: landscape) and (min-resolution: 2.5dppx) {
  body.connect-mobile-edition .article-standfirst {
      margin-left: var(--side-pad); margin-right: var(--side-pad); }
  body.connect-mobile-edition .article-pull-quote,
  body.connect-mobile-edition .article-pull-quote-credit {
      margin-left: calc(var(--side-pad) + 0.75rem);
      margin-right: calc(var(--side-pad) + 0.75rem); }
  body.connect-mobile-edition .article-boxout {
      margin-left: var(--side-pad); margin-right: var(--side-pad); width: auto; }
}
/* Full-bleed elements (.article-hero / .article-hero-landscape / gallery strips) use negative
   --side-pad margins, but the body is FLUSH (elements self-pad) — so they overhang the viewport
   by up to side-pad − the UA's 8px body margin (~12px), which reads in-app as a slight horizontal
   WOBBLE (cycp mobile, 2026-07-21). Clip sideways overflow at the root: the bleed look is kept
   (edges crop symmetrically), the phantom scroll is gone. `clip` avoids creating a scroll
   container (safe for sticky); `hidden` is the fallback for older webviews. */
html, body { overflow-x: hidden; overflow-x: clip; }

/* ⚠️ BOTTOM CLEARANCE lives on the BODY, not on a wrapper class.
   *Triggered by:* Patrick, 2026-08-11 — "the web reader is chopping off the bottom of a lot of
   articles… I'm not sure if that's because we don't have enough, or it's doing its own."
   Measured: we had almost none. The generous 20vh clearance below was written for `.flexy`, and
   NO template emits that class any more — not the pipeline article, not the retrofit, not the ad
   page — so the rule has been dead. What was actually left was `body.connect-mobile-edition`'s
   64px, which only applies where the APP adds that class; on the web reader, and on an ad page
   (whose body carries no class at all), the last line ended flush against the document edge with
   ~0 clearance, straight under the reader's own bottom chrome.
   Put it on `body` so every surface and every template gets it, with a px floor for short
   viewports. The cover is exempt — it is a full-screen picture page and trailing space there
   would show as a white band under the artwork. */
body { padding-bottom: max(20vh, 96px); }
body.interactive-cover { padding-bottom: 0 !important; }
.flexy { margin: 0; padding: 0; padding-top: 64px; width: 100%; word-wrap: break-word;
         /* generous bottom clearance so the last content always clears the reader's bottom nav
            bar (Patrick 2026-07-24, all devices). viewerv2 zeroes this (no nav there). */
         padding-bottom: 20vh; margin-left: auto; margin-right: auto; box-sizing: border-box; }

/* Advertisement image: centre it (a natural-size ad sat hard-left, odd on desktop/tablet —
   Patrick 2026-07-24). Scoped to .advertisement so editorial images are untouched. */
/* Advert image centring. The ad image is a display:block <img> inside an INLINE <a class="annotation">
   (the click-through link) — a block-in-inline that defeats margin:auto, so the image sat hard-left.
   Making the <a> + <img> inline-block lets the container's text-align:center actually centre them.
   (Patrick "adverts left aligned not centred, as is the link below an ad", iPad 2026-07-24.) */
.advertisement .article-in-image { text-align: center; }
.advertisement .article-in-image a { display: inline-block; max-width: 100%; }
.advertisement .article-in-image img { display: inline-block; margin: 0 auto; max-width: 100%;
    height: auto; vertical-align: top; }
/* An advert is NOT editorial reading — it must never inset into the reading column or shift with
   --read-inset. These 3-class selectors out-specify the reading-column cap (2 classes) so an ad
   image (incl. .solo) + the link/body below it stay full-width centred. */
.connect-mobile-edition .advertisement .article-in-image,
.connect-mobile-edition .advertisement .article-in-image.solo,
.connect-mobile-edition .advertisement .article-full-body {
    max-width: 100% !important; width: auto !important;
    margin-left: auto !important; margin-right: auto !important;
    padding-left: 0 !important; padding-right: 0 !important; text-align: center; }
.connect-mobile-edition .advertisement .article-in-image img {
    display: inline-block; margin: 0 auto; max-width: 100%; }
/* Robust ad-image centring that does NOT depend on the .advertisement wrapper (viewerv2 strips that
   class, and a reader might too). Target the ad image by its _ad.webp src: centre its container +
   make the <a> wrapper and <img> inline-block so text-align:center actually centres them (a
   block <img> inside the inline <a> can't be centred by margin:auto). Ad-only ⇒ editorial images
   untouched. (Patrick "still not convinced adverts are centering on tablet landscape".) */
.article-in-image:has(img[src$="_ad.webp"]) { text-align: center; }
.article-in-image a:has(> img[src$="_ad.webp"]) { display: inline-block; max-width: 100%; }
.article-in-image img[src$="_ad.webp"] { display: inline-block; max-width: 100%; vertical-align: top; }

/* A large LANDSCAPE solo body image (phase4-tagged .full-bleed) spans edge-to-edge instead of the
   reading-column cap — mirrors how a full-bleed hero bleeds (margin: 0 -side-pad cancels the
   container inset), so it ignores --read-inset and reaches both screen edges. Its caption re-insets
   to the reading column. A portrait/small/low-res solo is NOT tagged, so it stays column width.
   (Patrick 26-1 "if the image can reach full bleed it does, otherwise column width".) */
.article-in-image.solo.full-bleed {
    display: block !important;   /* .article-in-image is shrink-to-fit; force block so width fills */
    max-width: none !important; width: auto !important;
    margin-left: calc(-1 * var(--side-pad)) !important;
    margin-right: calc(-1 * var(--side-pad)) !important;
    padding-left: 0 !important; padding-right: 0 !important; }
.article-in-image.solo.full-bleed img { width: 100%; max-width: 100%; display: block;
    height: auto !important; max-height: 90vh !important; object-fit: cover; object-position: center; }
    /* height:auto keeps the true aspect at full width (a base max-height + object-fit:fill was
       squashing it — FFT 74-1). max-height:90vh is the SAFETY belt so a full-bleed image is never
       taller than the screen (Patrick 2026-07-24); object-fit:cover fills the width at that cap with
       a minimal centre crop rather than letterboxing. The phase4 aspect gate (>=1.7) means qualifying
       images are wide/short enough that the belt rarely bites — a less-wide landscape stays column. */
.article-in-image.solo.full-bleed .articleImageCaption {
    padding-left: calc(var(--side-pad) + var(--read-inset, 0px)) !important;
    padding-right: var(--side-pad); box-sizing: border-box; margin-left: 0 !important; }
/* a .solo that ended up INSIDE a gallery/carousel must NOT bleed out of its slide (it's trapped in
   a fixed-width slide) — neutralise full-bleed there so it can't break the carousel layout. */
.gallery-viewer .article-in-image.solo.full-bleed,
.feature-gallery .article-in-image.solo.full-bleed {
    margin-left: auto !important; margin-right: auto !important; max-width: 100% !important; }

/* RETROFIT full-bleed images (vendor class .photo, full page width): same caption re-inset as the
   pipeline's solo.full-bleed above — the caption aligns with the READING column, not the screen
   edge (cyp 14-1 "Jack urges…" sat at the bare screen-left in iPad landscape while the body copy
   started at --read-inset; the right-floated credit hugged the screen's right edge — Patrick,
   iPad 2026-08-10). max-width 640 matches the body cap so the credit's right edge lands on the
   column's right too. Portrait (--read-inset 0) keeps today's --side-pad inset. */
.article-in-image.photo > .articleImageCaption {
    padding-left: calc(var(--side-pad) + var(--read-inset, 0px));
    max-width: 640px; }
/* …but NOT when the .photo div is a gallery/scroller CELL or tucked in a boxout — there the
   caption belongs to its cell/box, whose container already carries the inset. Each context gets
   its own prior padding back explicitly: this (0,4,0) rule would otherwise ALSO beat the
   scroller caption's `padding: 4px 2px 0` shorthand at (0,3,0) — measured: scroller captions
   grew a 20px left pad and overflowed their cells by it. */
.gallery-viewer .article-in-image.photo > .articleImageCaption,
.article-boxout .article-in-image.photo > .articleImageCaption {
    padding-left: var(--side-pad); max-width: none; }
.gallery-viewer.gallery-scroller .article-in-image.photo > .articleImageCaption {
    padding-left: 2px; }

a { color: var(--link-color); }

.article-standfirst .ld_bold  { font-weight: unset; }
.article-full-lead .ld_bold   { font-weight: var(--font-weight-x-bold); }
.articleImageCredit .ld_bold  { font-weight: var(--font-weight-standard); }
.article-full-byline .ld_bold { font-weight: var(--font-weight-bold); }
.article-full-subhead .ld_bold{ font-weight: var(--font-weight-standard); }
.article-full-lead .ld_italic { font-style: italic; }


/** HEADER STYLES **/

.article-standfirst {
    display: var(--standfirst-display);
    font-family: var(--standfirst-font);
    font-size: var(--standfirst-size);
    font-weight: var(--standfirst-weight);
    /* Pill is opt-in: --standfirst-pill-bg / --standfirst-text are NOT baked inline by
       phase-4 (unlike --standfirst-bg/-fg), so the default is a plain accent kicker; a pill
       title's override maps them back at body scope (see the :root STANDFIRST comment). */
    color: var(--standfirst-text, var(--accent-on-light, var(--accent)));
    background: var(--standfirst-pill-bg, none);
    line-height: 1;
    letter-spacing: var(--standfirst-tracking);
    text-transform: var(--standfirst-transform);
    text-decoration: none;
    padding: var(--standfirst-padding);
    margin: var(--standfirst-margin);
    border-radius: var(--standfirst-radius);
    text-align: left;
}

/* ── Retrofit vendor-colour guard (Patrick's QA pass 2026-07-31: cyp article_106-1's "RACING
   NOTES" standfirst rendered dark/illegible text on its own red accent pill).
   `phase_retrofit.py`'s `_tint` clobbers the VENDOR's inline colour with our own measured read
   on subheads and pull-quotes, but the standfirst/eyebrow was never in that sweep — a vendor's
   own inline `style="color:…"` on that run survives compilation as an externalized
   `.s-XXXXXXXX{color:…!important}` utility class (see `_externalize_assets`). That class shares
   the SAME (0,1,0) specificity as the plain `.article-standfirst` rule above and is emitted
   LATER in the compiled stylesheet (retrofit CSS is appended after base.css + the title
   override), so it wins by source order — same as the accent FILL underneath it, or close
   enough to read as one (Empire 113-1/114-1: a vendor olive run on an almost-identical olive
   pill). Nothing on the retrofit side computes the standfirst foreground against the ACCENT —
   it's derived from the kicker's own printed colour, which was never designed for OUR pill.

   Fix, CSS-only: re-assert the SAME var() chain the base rule already uses, at higher
   specificity, so it always wins over a same-specificity vendor class regardless of file order.
   Scoped to `body[class*="retro-"]` — the wrapper class only a retrofit build's own <body>
   carries — so this can NEVER engage on a normal pipeline title (e.g. TheWeek-overrides.css's
   OWN `!important` `.article-standfirst{color:var(--accent)!important}`, which stays untouched
   because it never runs inside a `retro-*` body). A future retrofit of a title with its own
   standfirst-colour override would want the same review this entry gives — see CLAUDE.md. */
body[class*="retro-"] .article-standfirst {
    color: var(--standfirst-text, var(--accent-on-light, var(--accent))) !important;
}

.article-full-headline {
    font-family: var(--font-family-main-display);
    font-size: var(--font-xx-large);
    font-weight: var(--headline-weight);
    color: var(--third-color);
    line-height: var(--headline-line-height);
    letter-spacing: var(--headline-tracking);
    text-transform: var(--headline-transform);
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 0 0 0.875rem;
    text-align: left;
}

/* Headline typographic fidelity (phase-3c reads it from the print spans, tags the <h2>):
   .hl-caps  — the print headline is a SHORT all-caps display line → reproduce the caps (long caps
               "shout" on digital, so phase-3c only flags ≤25-char headlines).
   .hl-serif / .hl-sans — the print headline's FACE. A title can offer a serif AND a sans display
               variant (--headline-font-serif / --headline-font-sans); the var() fallback chain means
               a title that defines only one (or neither) falls back to its single display font, so
               this is a safe no-op until a title opts in. (WCM: most heads are serif/Playfair, but
               features like "The boy who broke cricket" are a chunky sans → --headline-font-sans.) */
.article-full-headline.hl-caps, .hero-headline.hl-caps { text-transform: uppercase; }
/* A headline line that DROPS in size below the previous line (phase-4 emits hl-run-drop on the
   first run of such a line — CSS cannot compare the sizes, the emitter can): display headlines run
   TIGHT leading (VL's hero-headline is 0.96 — a line box shorter than its glyphs), which reads
   fine between equal-size lines but let "problem,"'s descenders overhang the 0.52em sub-line
   (Patrick, Vertical Life 26-1). inline-block so the margin applies; 0.42 of the SMALL run's own
   em ≈ the big line's descender depth at typical drop ratios. */
.hl-run-drop { display: inline-block; margin-top: 0.42em; }
/* .hl-italic — the print headline is an ITALIC display line (TEG "This Month") → reproduce it.
   3c reads the face from the PDF font name, same as the subhead italic read. */
.article-full-headline.hl-italic, .hero-headline.hl-italic { font-style: italic; }
/* `.article-hero .hero-headline` (below) sets the hero font with specificity (0,2,0) AND is defined
   later, so the hero variants must out-specify it — qualify with `.article-hero` → (0,3,0). The
   non-scrim `.article-full-headline` is (0,1,0) so a single compound class already wins there. */
.article-full-headline.hl-serif,
.article-hero .hero-headline.hl-serif {
    font-family: var(--headline-font-serif, var(--font-family-main-display));
}
.article-full-headline.hl-sans,
.article-hero .hero-headline.hl-sans {
    font-family: var(--headline-font-sans, var(--font-family-main-display));
}

.article-full-lead {
    font-family: var(--lead-font);
    font-size: var(--font-medium);
    font-weight: var(--lead-weight);
    font-style: var(--lead-style);
    color: var(--lead-color);
    line-height: var(--lead-line-height);
    text-decoration: normal;
    padding: 0 var(--side-pad);
    margin: 0 0 1.25rem;
    text-align: left;
}

.article-full-subhead {
    font-family: var(--font-family-condensed);
    font-size: var(--font-medium);
    font-weight: var(--font-weight-x-bold);
    color: var(--second-color);
    line-height: 1.2;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    text-decoration: normal;
    padding: 0 var(--side-pad);
    margin: 0 0 0.875rem;
    text-align: left;
}

.article-full-byline {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-standard);
    color: var(--forth-color);
    line-height: 1.3;
    letter-spacing: 0.04em;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 0 0 1.25rem;
    text-align: left;
}
.article-full-byline .ld_bold {
    color: var(--main-color);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}


/** STANDARD BODY STYLING **/

.article-full-body {
    font-family: var(--font-family-main);
    font-size: var(--font-standard);
    font-weight: var(--font-weight-standard);
    color: var(--main-color);
    line-height: 1.65;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 0 0 1.25rem;
    text-align: left;
}

.article-headline {
    font-family: var(--font-family-main-display);
    font-size: var(--font-x-large);
    font-weight: var(--font-weight-xx-bold);
    color: var(--main-color);
    line-height: 1.05;
    letter-spacing: -0.01em;
    text-transform: uppercase;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 1.5rem 0 0.625rem;
    text-align: left;
}

.article-subhead {
    font-family: var(--font-family-condensed);
    font-size: var(--font-medium);
    font-weight: var(--font-weight-x-bold);
    color: var(--main-color);
    line-height: 1.2;
    /* Render the sub-head's CAPTURED (printed) case — phase-3 captures exact case, so forcing
       uppercase was unfaithful (APM/WBG print sentence-case section heads). A title that genuinely
       wants caps can re-add it via its own override. */
    text-transform: none;
    letter-spacing: 0.04em;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 1.25rem 0 0.5rem;
    text-align: left;
}

.article-subsubhead {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-x-bold);
    color: var(--main-color);
    line-height: 1.3;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 1.25rem 0 0.5rem;
    text-align: left;
}

.article-superheader {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-x-bold);
    color: var(--main-color);
    line-height: 1.3;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 0 0 0.75rem;
    text-align: left;
}

.article-crosshead {
    font-family: var(--font-family-condensed);
    font-size: var(--font-large);
    font-weight: var(--font-weight-xx-bold);
    color: var(--main-color);
    line-height: 1.1;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 2rem 0 0.625rem;
    text-align: left;
}

.article-byline {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-standard);
    color: var(--forth-color);
    line-height: 1.3;
    letter-spacing: 0.04em;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 0 0 0.625rem;
    text-align: left;
}

.article-body {
    font-family: var(--font-family-main);
    font-size: var(--font-standard);
    font-weight: var(--font-weight-standard);
    color: var(--main-color);
    line-height: 1.65;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 0 0 1.25rem;
    text-align: left;
}

/* CALLOUT STAT — an isolated printed figure set at display size ("83%", "$215B", "$386 Billion" +
   its explanatory line). *Triggered by:* Patrick's Fortune QA — "In our treatment they just go in as
   body, but i wonder whether we could identify these sort of isolated figures / stats and make them
   large ala print. This would benefit lots of titles." Emitted by phase-4 ONLY for a <p> that 3c
   measured as printing at >=3x the page's median size, so a title whose figures print at body size
   never gets this. Sized with the display font and the article's own --accent-on-light, so it is
   per-title by construction; a captured print colour overrides inline when it reads on white. */
.article-callout-stat {
    margin: 1.1em var(--side-pad) 1.2em;
    padding-left: 0.6em;
    border-left: 3px solid var(--accent);
    text-align: left;
}
/* `p.article-callout-stat` is the OPERATOR-tagged form (QA reclass → callout): a reclass can only
   swap a class on the existing paragraph, so the paragraph itself becomes the figure. The automatic
   path emits a <div> with a .callout-figure/.callout-label pair. */
.article-callout-stat .callout-figure,
p.article-callout-stat {
    display: block;
    font-family: var(--font-family-main-display);
    /* clamped so a 3-character figure reads as display type without a 13-character one overflowing */
    font-size: clamp(2.1rem, 9vw, 3.4rem);
    font-weight: 800;
    line-height: 1.02;
    letter-spacing: -0.02em;
    color: var(--accent-on-light);
    text-wrap: balance;
}
.article-callout-stat .callout-label {
    display: block;
    margin-top: 0.35em;
    font-family: var(--font-family-main);
    font-size: var(--font-small);
    line-height: 1.35;
    color: var(--second-color);
}
@media (min-width: 640px) {
    /* beside the body column rather than breaking its flow, the way print sets these */
    .article-callout-stat { float: right; width: 38%; margin: 0.2em 0 0.9em 1.4em; }
    .article-callout-stat .callout-figure { font-size: 3.1rem; }
}
@media (orientation: landscape) and (max-width: 1023px) and (min-resolution: 2.5dppx) {
    /* the in-app landscape reading column is already narrow — don't also float into it */
    .article-callout-stat { float: none; width: auto; margin: 1.1em 0 1.2em; }
}
/* A callout nested in a boxout gets its OWN WHITE panel rather than sitting on the boxout's grey.
   *Triggered by:* Patrick on Fortune 58-1 — "I think the hue on 58-1 would be fine on white but is
   maybe struggling on grey of the boxout." Measured: 7 of Fortune's 8 callouts are inside a boxout,
   so this is the normal case, not an edge one. The colour read is gated against WHITE, so giving the
   figure a white ground is what makes that gate honest rather than loosening the gate itself. */
.article-boxout .article-callout-stat {
    background: #fff;
    padding: 0.5em 0.7em;
    border-radius: 3px;
}

.article-pull-quote {
    font-family: var(--pull-quote-font);
    font-size: var(--font-large);
    font-weight: var(--pull-quote-weight);
    font-style: var(--pull-quote-style);
    color: var(--pull-quote-color);
    line-height: 1.2;
    text-decoration: none;
    padding: var(--pull-quote-padding);
    margin: var(--pull-quote-margin);
    text-align: var(--pull-quote-align);
    border-top: var(--pull-quote-border-width) solid var(--pull-quote-border-color);
    border-bottom: var(--pull-quote-border-width) solid var(--pull-quote-border-color);
    overflow: hidden; /* BFC: prevents border lines from bleeding through adjacent floats */
}

/* Boxed pull-quote (phase-3 style="boxed"): by DEFAULT it renders the SAME as a clean pull-quote —
   NO accent block. The solid-block treatment (each line on an accent fill, FFT feature = red blocks)
   is an OPT-IN per title: a title that wants it adds the `.pull-quote-boxed` / `.pq-text` block rules
   in its own overrides file (see FourFourTwo-overrides.css). Most titles don't want a background, so
   boxed-follows-clean is the safe default (Patrick: "unless customised specifically, boxed should
   follow clean"). The `.pq-text` span phase-4 wraps the boxed text in is just inert inline text here,
   inheriting the clean pull-quote styling. */

/* Desktop/tablet: a left-aligned pull-quote leaves a big right-hand gap on a wide column, so CENTRE
   it (NOT justified — no edge-fill) and balance line lengths for even-ish rows. Mobile keeps the
   left alignment (--pull-quote-align) and full width. Applies to clean + boxed; auto side margins
   centre the block. *Triggered by:* FFT 392 Alex Scott (left-ragged boxed quote, lots of whitespace). */
@media (min-width: 640px) and (max-resolution: 2dppx) {
    .article-pull-quote {
        text-align: center;
        text-wrap: balance;          /* even-ish line lengths (graceful no-op where unsupported) */
        margin-left: auto;
        margin-right: auto;
    }
}

.article-pull-quote-credit {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-bold);
    color: var(--forth-color);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: -0.75rem 0 1.75rem;
    text-align: left;
}

.article-callout {
    font-family: var(--pull-quote-font);
    font-size: var(--font-large);
    font-weight: var(--pull-quote-weight);
    font-style: var(--pull-quote-style);
    color: var(--accent);
    line-height: 1.2;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 0.5rem 0 1rem;
    text-align: left;
}


/** DROPCAP **/

.dropcap::first-letter {
    float: left;
    font-size: var(--dropcap-size);
    font-family: var(--font-family-main-display);
    font-weight: var(--dropcap-weight);
    line-height: 0.82;
    margin-right: 0.06em;
    margin-top: 0.08em;
    width: auto;
    font-style: normal !important;
    color: var(--accent);
    text-transform: uppercase;
}
.dropcap { text-indent: unset; }


/** IMAGE & VIDEO STYLING **/

.article-in-image {
    max-width: 100%;
    display: block;
    height: auto;
    padding: 0;
    margin: 1rem 0 0.5rem;
    text-align: center;
}
.article-in-image > img {
    max-width: 100%;
    height: auto;
    display: inline-block;
    margin: 0 0 0.5rem;
}

video[poster] { height: 100%; width: 100%; }
.article-in-video { display: block; margin-left: 5%; margin-right: 5%; text-align: center;
                    padding-bottom: 50%; width: 90%; margin-bottom: 10px; height: auto; position: relative; }
.article-in-video > iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; }

.articleImageCaption {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-standard);
    color: var(--forth-color);
    line-height: 1.4;
    text-transform: none;
    padding: 0 var(--side-pad);
    margin: 0 0 1rem;
    text-align: left;
}
.articleImageCaption .ld_bold {
    font-weight: var(--font-weight-x-bold);
    color: var(--main-color);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.articleVideoCaption {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-standard);
    color: var(--forth-color);
    line-height: 1.4;
    padding: 0 var(--side-pad);
    margin: 0 0 1rem;
    text-align: left;
}
.articleImageCredit {
    font-family: var(--font-family-condensed);
    font-size: var(--font-smallest);
    font-weight: var(--font-weight-standard);
    color: var(--forth-color);
    line-height: 1.2;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    padding: 0 var(--side-pad);
    margin: 0 0 0.75rem;
    text-align: right;
}


/** HERO IMAGES — pipeline additions **/

.article-hero-landscape,
.article-hero-portrait,
.article-hero-stitched {
    margin: 0 0 1.25rem;
}
.article-hero-landscape img,
.article-hero-portrait img,
.article-hero-stitched img {
    max-width: 100%;
    height: auto;
    display: block;
}
.article-hero-landscape img {
    max-height: 75vh;
    object-fit: cover;
    object-position: center top;
}
/* Narrow hero images: full-bleed container with grey fill; image centres within it.
   Flex is used rather than margin: 0 auto on the img because margin: auto on a replaced element
   doesn't reliably centre in all viewer contexts. flex-direction: COLUMN so a hero <img> + its
   <div class="articleImageCaption"> sibling STACK (image, then caption beneath) — the previous
   row layout pushed the caption off to the right, largely off-page (Boxing News 12-1/74-1 and
   others, cross-title). */
.article-hero-landscape { background: #f2f2f2; margin-left: calc(-1 * var(--side-pad)); margin-right: calc(-1 * var(--side-pad)); display: flex; flex-direction: column; align-items: center; }
.article-hero-landscape .articleImageCaption { align-self: stretch; text-align: center; margin-top: 0.5rem; }

/* ── Hero: single-DOM responsive scrim (Option 2, 2026-06-05) ──────────────────
   ONE copy of the title text + ONE image; CSS repositions the SAME nodes per
   breakpoint. No duplicate DOM, nothing hidden — so a reader that ignores the
   media queries (e.g. an epub editor) still shows the hero exactly once.
   DOM order is text-then-media. Mobile-first = scrim (dark block, image on top
   via flex order, white text below). Desktop (>=640) flips a plain --scrim hero
   to header-above-image, dark-on-light; --desktop keeps the scrim look on desktop
   too; --portrait lays a tall hero beside the text on desktop.

   ── DECISION TABLE (single source of truth, 2026-06-30) ──────────────────────
   The treatment is chosen by viewport width × DPR × hero-shape, partitioned with
   a SINGLE DPR boundary at 2.5dppx so the cells are gap-free AND non-overlapping
   (no half-apply possible across the light↔dark boundary):

     width < 640px (any DPR) ............... T0 base mobile scrim (dark, stacked)
     width ≥ 640 AND DPR < 2.5 ............. T1 desktop FLIP (light, dark-on-light)
       · plain → header-above-image
       · --portrait / --very-tall → image beside text (light, uncropped)
     DPR ≥ 2.5 (real phone OR in-app webview, which misreports width≈980):
       · orientation landscape ............ T2 side-by-side (dark, image right)
       · orientation portrait + --tall .... T3 top-anchored immersive (dark)
       · --very-tall (squarish+ viewport) . T4 side-by-side (dark)
       · orientation portrait + plain ..... T0 base scrim (dark, stacked)

   WHY DPR (not width) splits desktop from phone: the in-app webview reports a
   WIDE innerWidth (~980) at DPR 3 — DPR is the only reliable "really a phone"
   signal, so the flip's upper bound MUST stay below 2.5 or the webview wrongly
   flips to desktop. The 2.49/2.5 split closes the old DPR-2.0–2.5 gap (a Windows
   monitor at ~225% scaling fell through every query → base scrim black bars on a
   wide pane, RM 62/66/72). Self-containment is achieved by the non-overlapping
   partition — do NOT duplicate bg/text colours into T2/T3/T4: the dark treatments
   correctly inherit base white text + the v11 --panel-fg override; hardcoding
   colours there would break v11 white-panel heroes. */

.article-hero { margin: 0 calc(-1 * var(--side-pad)) 1.5rem; }

/* Base (mobile) = scrim. The panel colour is var(--panel-bg) with a #0a0a0a fallback, so a hero WITHOUT
   a captured panel renders exactly as before; phase-4 sets --panel-bg/--panel-fg + .article-hero--panel
   only when print supplies a colour (see _decide_hero_panel). */
.article-hero--scrim { display: flex; flex-direction: column; background: var(--panel-bg, #0a0a0a); }
.article-hero--scrim .article-hero__media { order: 1; position: relative; overflow: hidden; }
.article-hero--scrim .article-hero__media > img { width: 100%; display: block; height: auto;
                             max-height: 68vh; object-fit: cover; object-position: top;
                             /* a legacy hero <img> carries .article-in-image's margin (16px
                                top/8px bottom) — inside the scrim media that gap shows the
                                dark panel through as a 'black bar above the photo' (retrofit
                                64/66/74, Patrick). Reset it. */
                             margin: 0; }
.article-hero--scrim .article-hero__media::after { content: ''; position: absolute; bottom: 0;
                             left: 0; right: 0; height: 44%; pointer-events: none;
                             background: linear-gradient(to bottom, transparent, var(--panel-bg, #0a0a0a)); }
.article-hero--scrim .article-hero__text { order: 2; background: var(--panel-bg, #0a0a0a); color: #fff;
                             padding: 8px calc(2 * var(--side-pad)) 28px; margin-top: -1px; }
/* coloured panel → HARD edge (no fade between a solid colour and the photo) */
.article-hero--scrim.article-hero--hard .article-hero__media::after { display: none; }
/* panel furniture text uses the luminance-correct panel fg (white panels need dark eyebrow/lead/byline).
   ⚠️ The HEADLINE belongs in this list too. It was omitted — presumably because a v11 panel usually
   carries per-word captured colours — so on a WHITE panel the headline fell through to the scrim's
   blanket `color:#fff` and rendered white-on-white, while the lead beside it correctly took #111
   (cyp 36-1 "Are you sitting comfortably?", Patrick 2026-08-11; he spotted it in dark mode, but it
   was equally broken in light — the headline was simply invisible). The per-word `.hl-run` spans
   carry their colour INLINE on the span, so they still win over this and the captured look is
   unaffected; this only supplies the base colour for a headline that has none.
   Every other --panel-fg rule sits inside `@media (min-width: 640px)`, so a PHONE — the primary read
   — had the panel background with no matching foreground at all. */
.article-hero--scrim.article-hero--panel .hero-eyebrow,
.article-hero--scrim.article-hero--panel .hero-headline,
.article-hero--scrim.article-hero--panel .hero-lead,
.article-hero--scrim.article-hero--panel .hero-byline { color: var(--panel-fg, #fff); opacity: 1; }

.article-hero .hero-eyebrow { font-size: 10px; font-weight: 700; letter-spacing: 0.14em;
                             text-transform: uppercase; margin: 0 0 8px; opacity: 0.7;
                             font-family: var(--font-family-condensed); }
/* NO blanket text-transform:uppercase (Patrick, CM 56-1): case renders AS CAPTURED from print.
   A genuinely all-caps SHORT print headline still uppercases via .hl-caps (the 3c read); a LONG
   captured-caps headline is sentence-cased by phase-4 (caps that long is a print-font artefact
   that shouts on digital). */
.article-hero .hero-headline { font-family: var(--font-family-main-display); font-size: 2.6em;
                             font-weight: 700; line-height: 0.96; margin: 0 0 12px;
                             letter-spacing: -0.01em; }
.article-hero .hero-lead { font-family: var(--font-family-main); font-size: 1em; line-height: 1.5; margin: 0; }
.article-hero .hero-byline { font-family: var(--font-family-condensed); font-size: 11px;
                             font-weight: 400; margin: 8px 0 0; }
/* scrim mode → white text on the dark panel */
.article-hero--scrim .hero-eyebrow,
.article-hero--scrim .hero-headline,
.article-hero--scrim .hero-lead { color: #fff; }
.article-hero--scrim .hero-lead { opacity: 0.88; }
.article-hero--scrim .hero-byline { color: rgba(255,255,255,0.6); }

/* Desktop: a plain --scrim hero flips to header-above-image, dark-on-light.
   Upper bound is 2.49dppx (not 2dppx) so a Windows monitor at ~225% scaling
   (DPR 2.0–2.5, a wide DESKTOP pane) flips instead of falling into the old gap
   below the 2.5dppx phone treatments. Stays below 2.5 so the in-app webview
   (DPR 3, misreported width≈980) does NOT flip — it gets the phone treatments. */
@media (min-width: 640px) and (max-resolution: 2.49dppx) {
  .article-hero--scrim:not(.article-hero--desktop) { display: block; background: transparent; }
  .article-hero--scrim:not(.article-hero--desktop) .article-hero__text { background: transparent;
                             /* .article-hero is full-bleed (margin: 0 calc(-1*--side-pad)). The text
                                must re-inset by 2×--side-pad: one --side-pad cancels the negative
                                margin (back to the page edge), the second matches the BODY column's
                                --side-pad inset. With only 1×, the head block aligned to the page edge,
                                sitting one --side-pad LEFT of the body copy (NGK 48-1 headline; the
                                earlier FFT 14-1/30-1/31-1 fix stopped the clip but under-inset by one). */
                             /* top pad: the flip is header-above-image (like a non-scrim hero), so
                                give the eyebrow/standfirst the same breathing room from the app bar —
                                scrim articles get body padding-top:90px (not the non-scrim 114px), and
                                a short/imageless-top head sat flush under the bar (Patrick, iPad tablet
                                2026-07-24: 12-1/14-1/18-1/20-1/42-1/58-1/70-1/82-1/94-1). */
                             padding: 1.5rem calc(2 * var(--side-pad)) 0; margin: 0 0 1rem; }
  /* On desktop a plain scrim hero flips to the SAME header-above-image, dark-on-light treatment as a
     non-scrim hero, so its head text must match the non-scrim sizes — the small scrim-overlay sizes
     (hero-lead 1em, hero-byline 11px) made the lead/byline look shrunken vs articles with a non-scrim
     hero (FFT 14-1). Mirror .article-full-lead (--font-medium) / .article-full-byline (--font-small)
     / .article-standfirst (--standfirst-size). Mobile keeps the compact overlay sizes (unchanged). */
  .article-hero--scrim:not(.article-hero--desktop) .hero-eyebrow { color: var(--accent-on-light, var(--accent)); opacity: 1; font-size: var(--standfirst-size); }
  .article-hero--scrim:not(.article-hero--desktop) .hero-headline { color: var(--main-color); }
  /* (A DARK PANEL hero's per-word headline colours USED to be reverted to main-color here, for the
     old desktop flip-to-white. Captured-colour panels now KEEP their panel on tablet/desktop — the
     block below — so those light hl-run colours stay legible on the kept panel and the revert is gone
     (98-1's yellow kicker was rendering black-on-black). The non-panel light-flip revert still lives
     in the landscape side-by-side rule, now gated :not(--panel).) */
  .article-hero--scrim:not(.article-hero--desktop) .hero-lead { color: var(--second-color); opacity: 1; font-size: var(--font-medium); line-height: 1.4; }
  .article-hero--scrim:not(.article-hero--desktop) .hero-byline { color: var(--third-color); font-size: var(--font-small); }
  .article-hero--scrim:not(.article-hero--desktop) .article-hero__media { background: #f2f2f2;
                             display: flex; justify-content: center; }
  .article-hero--scrim:not(.article-hero--desktop) .article-hero__media::after { display: none; }
  .article-hero--scrim:not(.article-hero--desktop) .article-hero__media > img { max-height: 70vh; object-fit: contain; }

  /* CAPTURED-COLOUR panel heroes (.article-hero--panel, a real print colour) keep their coloured
     panel + luminance-correct text across the WHOLE tablet/desktop flip — BOTH portrait and landscape
     (Patrick, 2026-07-24 "same coloured header on tablet + desktop portrait too"). Sits in the flip
     block so it overrides the dark-on-light rules above; a DEFAULT-dark scrim (no --panel) still
     flips to light, avoiding 'overkill' on big screens. More specific than the landscape side-by-side
     transparent-panel rule, so it wins there too. */
  .article-hero--scrim.article-hero--panel:not(.article-hero--desktop) { background: var(--panel-bg); }
  .article-hero--scrim.article-hero--panel:not(.article-hero--desktop) .article-hero__text {
      background: var(--panel-bg); color: var(--panel-fg, #fff); }
  .article-hero--scrim.article-hero--panel:not(.article-hero--desktop) .article-hero__media {
      background: var(--panel-bg); }
  .article-hero--scrim.article-hero--panel:not(.article-hero--desktop) .hero-eyebrow { color: var(--panel-fg, #fff); opacity: 1; }
  .article-hero--scrim.article-hero--panel:not(.article-hero--desktop) .hero-headline,
  .article-hero--scrim.article-hero--panel:not(.article-hero--desktop) .hero-lead,
  .article-hero--scrim.article-hero--panel:not(.article-hero--desktop) .hero-byline { color: var(--panel-fg, #fff); }
  /* per-word hl-run colours: leave the phase-4 INLINE captured colour untouched so the kept panel
     shows the same head as phone (98-1 white headline + yellow kicker). --panel-fg is the fallback
     base colour for un-coloured words via .hero-headline above. */

  /* Portrait hero on desktop: image beside the text (text left, image right). The head block is
     vertically CENTRED beside the image (align-items:center) so a short head doesn't sit at the top
     with a big empty gap below it next to a tall image (RM 72-1: head ~223px vs a 691px very-tall
     image left ~468px of white). The float image is also capped (max-height below) so a very-tall
     image can't tower into an overkill hero. */
  .article-hero--scrim.article-hero--portrait:not(.article-hero--desktop) { display: flex;
                             flex-direction: row; gap: 20px; align-items: center; }
  .article-hero--scrim.article-hero--portrait:not(.article-hero--desktop) .article-hero__text { order: 1; flex: 1; }
  .article-hero--scrim.article-hero--portrait:not(.article-hero--desktop) .article-hero__media { order: 2;
                             flex: 0 0 42%; max-width: 42%; margin: 0; background: transparent; }
  /* cap a portrait/very-tall float image so it doesn't dominate the column (overrides the plain-flip 70vh) */
  .article-hero--scrim.article-hero--portrait:not(.article-hero--desktop) .article-hero__media > img { max-height: 56vh; }
}

/* LANDSCAPE TABLET/DESKTOP — hero to the RIGHT of the head block (Patrick, iPad landscape 2026-07-24).
   The default tablet flip puts a big first image BELOW the head block, so you scroll past it before
   reading. In LANDSCAPE, lay it SIDE-BY-SIDE instead: head block left (dark-on-light, inherits the
   flip's text sizes/colours above), image a 40%-cover panel right — see the image AND read immediately.
   Applies to ALL scrim heroes EXCEPT --wide (a wide-short banner, e.g. 20-1, reads fine stacked) and
   --desktop (the deliberate dark-scrim variant). Unifies the portrait/square (already right via the
   block above) and the LANDSCAPE-shaped heroes (0.64–0.78, which previously flipped to header-above-
   image) onto one cover panel. 40% cap tunable. */
@media (min-width: 640px) and (orientation: landscape) and (max-resolution: 2.49dppx) {
  /* INCLUDES --desktop (the "keep dark scrim on desktop" variant, e.g. 78-1): in landscape it too
     goes right — the side-by-side fills the gutter the dark scrim was protecting, so its purpose is
     met the lighter way. Only --wide (banner) is excluded. */
  .article-hero--scrim:not(.article-hero--wide) {
      display: flex; flex-direction: row; align-items: stretch; gap: 0; min-height: 40vh;
      background: transparent; }
  .article-hero--scrim:not(.article-hero--wide) .article-hero__text {
      order: 1; flex: 1 1 0; min-width: 0; margin: 0; background: transparent; display: flex;
      flex-direction: column; justify-content: center;
      /* left padding carries the chunky reading inset (+ --read-inset) so the head block
         (eyebrow/headline/lead/byline/Listen) left-aligns with the body column, not 76px left of
         it — Patrick's "floated-right head block should line up with the body", iPad 2026-07-24. */
      padding: 1.5rem calc(2 * var(--side-pad)) 1.5rem calc(2 * var(--side-pad) + var(--read-inset, 0px)); }
  /* image on the RIGHT (order 2), text left (order 1) — matches 16-1/26-1 regardless of DOM order
     (a --desktop hero has no order set elsewhere, so it would otherwise fall to media-first/left). */
  .article-hero--scrim:not(.article-hero--wide) .article-hero__media {
      order: 2; flex: 0 0 40%; max-width: 42%; margin: 0; background: #f2f2f2; overflow: hidden;
      display: flex; align-items: stretch; }
  .article-hero--scrim:not(.article-hero--wide) .article-hero__media > img {
      width: 100%; height: 100%; max-height: 72vh; object-fit: cover; object-position: 50% 28%; }
  .article-hero--scrim:not(.article-hero--wide) .article-hero__media::after { display: none; }
  /* dark-on-light head text (the head block is now on the light page, not a dark panel). The flip
     block sets these for :not(--desktop); repeat here so a --desktop hero (whose scrim text is
     WHITE) doesn't render white-on-light. --panel-light keeps its captured colours (legible on
     white); a dark --panel's light per-word colours revert to the header colour. */
  .article-hero--scrim:not(.article-hero--wide) .hero-eyebrow { color: var(--accent-on-light, var(--accent)); opacity: 1; }
  .article-hero--scrim:not(.article-hero--wide) .hero-headline { color: var(--main-color); }
  .article-hero--scrim:not(.article-hero--wide):not(.article-hero--panel-light):not(.article-hero--panel) .hero-headline .hl-run { color: var(--main-color) !important; }
  .article-hero--scrim:not(.article-hero--wide) .hero-lead { color: var(--second-color); opacity: 1; }
  .article-hero--scrim:not(.article-hero--wide) .hero-byline { color: var(--third-color); }
  /* CAPTURED-COLOUR panel heroes keep their coloured panel + light-on-colour text on tablet/desktop
     too, instead of the light flip (Patrick, iPad 2026-07-24 — "the yellow ones should get the
     treatment"; a scrim with only the DEFAULT dark panel still flips to light to avoid 'overkill' on
     big screens). --panel is set by phase4 only when a real print colour was captured; --panel-fg is
     the luminance-correct text colour for that panel, so the head block stays legible. */
  .article-hero--scrim.article-hero--panel:not(.article-hero--wide) .article-hero__text {
      background: var(--panel-bg); color: var(--panel-fg, #fff); }
  .article-hero--scrim.article-hero--panel:not(.article-hero--wide) .article-hero__media { background: var(--panel-bg); }
  .article-hero--scrim.article-hero--panel:not(.article-hero--wide) .hero-eyebrow { color: var(--panel-fg, #fff); opacity: 1; }
  .article-hero--scrim.article-hero--panel:not(.article-hero--wide) .hero-headline,
  .article-hero--scrim.article-hero--panel:not(.article-hero--wide) .hero-lead,
  .article-hero--scrim.article-hero--panel:not(.article-hero--wide) .hero-byline { color: var(--panel-fg, #fff); }
  /* hl-run kept as the phase-4 INLINE captured colour (line 839's main-color revert is gated
     :not(--panel)), so a panel hero's kicker shows its real colour on the kept panel (98-1 yellow). */
  /* WIDE banner hero (20-1) stays a full-width stacked banner (image above, text below), but its
     text panel's LEFT padding carries --read-inset so the head block aligns with the body column,
     not the screen edge (Patrick "20-1 head block still up against left edge", iPad 2026-07-24). */
  .article-hero--scrim.article-hero--wide .article-hero__text {
      padding-left: calc(2 * var(--side-pad) + var(--read-inset, 0px)); }
}

/* LANDSCAPE PHONE scrim hero: a short, wide viewport can't fit the stacked image-over-text scrim, so
   lay it SIDE-BY-SIDE — the image FILLS the hero (cover) and the title sits over a scrim that fades in
   from the LEFT edge (the mobile bottom-fade rotated 90°). Targets landscape + high DPR, so desktop
   (DPR<=2, incl. landscape tablet) and portrait phones are untouched. CSS-only; reuses the single-DOM
   nodes. Tunables: text side (left), object-position (center — lean 'right center' if the subject sits
   left), and the scrim width/stops. NOTE: needs the dev to serve our CSS, and it renders zoomed until
   the app's viewport is fixed (the LAYOUT is already correct). */
/* + short-landscape alternative (<=500px tall = a phone on its side at any DPR). */
@media (orientation: landscape) and (min-resolution: 2.5dppx), (orientation: landscape) and (max-height: 500px) {
  /* Side-by-side: the FULL portrait image hugs the RIGHT EDGE (shown UNCROPPED, object-fit: contain), and
     the text panel GROWS to fill everything to its left, with a soft scrim fading into the image's LEFT
     edge WHEREVER it falls. So there's never a black gap to the right of the image, nor a hard black line
     on both sides (the old centred-in-60% layout left a big black band on the right). No crop ⇒ subject
     never cut. min-height (not fixed) so a LONG headline/lead GROWS the hero instead of clipping (FFT 44). */
  .article-hero--scrim { display: flex; flex-direction: row; align-items: stretch; min-height: 72vh;
                             overflow: hidden; background: var(--panel-bg, #0a0a0a); }
  /* text panel GROWS to fill the space left of the image (flex:1), so the image sits flush right. */
  .article-hero--scrim .article-hero__text { order: 0; flex: 1 1 0; min-width: 0;
                             background: var(--panel-bg, #0a0a0a); margin: 0; z-index: 1;
                             display: flex; flex-direction: column; justify-content: center;
                             padding: 28px var(--side-pad) 28px calc(2 * var(--side-pad)); }
  /* media SHRINKS to the image (flex:0 1 auto + img height:72vh ⇒ the box width == the image width), so
     the box's LEFT edge IS the image's left edge ⇒ the fade always meets it. Capped at 65% so a wide
     landscape image can't crowd out the text. height:72vh ≈ the app's usable landscape height (dev Q F). */
  .article-hero--scrim .article-hero__media { order: 1; flex: 0 1 auto; min-width: 0; max-width: 65%;
                             position: relative; overflow: hidden; display: flex;
                             align-items: center; justify-content: flex-end; }
  .article-hero--scrim .article-hero__media > img { width: auto; max-width: 100%; height: 72vh;
                             max-height: 72vh; object-fit: contain; object-position: right center; display: block; }
  /* scrim fade: a FIXED-WIDTH dark edge at the image's LEFT → transparent (box-left == image-left here). */
  .article-hero--scrim .article-hero__media::after { content: ''; position: absolute; inset: 0 auto 0 0;
                             width: 90px; height: auto;
                             background: linear-gradient(to right, var(--panel-bg, #0a0a0a), transparent); }
  .article-hero--scrim .hero-headline { font-size: 2.2em; }   /* a touch smaller so it fits the short height */
  /* Contributor headshot: keep it OUT of the LANDSCAPE side-by-side scrim only. In this short, narrow
     side-by-side head column a head-shot floats awkwardly at the text/image fade boundary (RM 62-1).
     On the PORTRAIT scrims (text-at-top --tall / text-at-bottom base) the head block is full-width on
     the panel and the head-shot sits fine beside the headline, so it's left alone there — and the
     desktop flip keeps it too. Hiding (not relocating) because the hero is one responsive DOM. */
  .article-hero--scrim .header-headshot { display: none; }
  /* TALL/PORTRAIT hero: `contain` at 72vh made it a POSTAGE STAMP (WCM 26-1: 162px wide beside a
     907px text panel; 31-1/46-1/74-1/78-1/98-1 same). Give the image a REAL right-side panel and
     accept a mild face-safe crop (Patrick: "on a pro max there'd be room for them to come over a
     little"). The img fills its box, so the fade still meets the image's left edge. */
  .article-hero--scrim.article-hero--tall .article-hero__media,
  .article-hero--scrim.article-hero--portrait .article-hero__media {
      flex: 0 0 40%; max-width: 46%; }
  .article-hero--scrim.article-hero--tall .article-hero__media > img,
  .article-hero--scrim.article-hero--portrait .article-hero__media > img {
      width: 100%; height: 72vh; object-fit: cover; object-position: 50% 28%; }
  /* WIDE + short hero (phase-4 --wide, w/h >= 1.5): side-by-side squeezed the text into a narrow
     TALL column with the image a letterboxed strip beside it (WCM 20-1). A wide short image reads
     as a BANNER — stack it above the text, full width; no left fade (there's no side join). */
  .article-hero--scrim.article-hero--wide { flex-direction: column; min-height: 0; }
  .article-hero--scrim.article-hero--wide .article-hero__media {
      order: 0; flex: 0 0 auto; max-width: 100%; justify-content: center; }
  .article-hero--scrim.article-hero--wide .article-hero__media > img {
      width: 100%; height: auto; max-height: 52vh; object-fit: cover; object-position: center 30%; }
  .article-hero--scrim.article-hero--wide .article-hero__media::after { display: none; }
  .article-hero--scrim.article-hero--wide .article-hero__text {
      order: 1; padding: 22px calc(2 * var(--side-pad)) 26px; }
}

/* PORTRAIT PHONE, TALL hero (h/w ≥ 1.2, set by phase4 as .article-hero--tall): top-anchored IMMERSIVE
   scrim — a solid black text area at the top, then the image STARTS where the black ends (its top/face
   is never hidden behind the black), with a soft scrim fading the join. Gated to portrait orientation +
   high DPR, so it never clashes with the desktop flip (DPR≤2) or the landscape side-by-side (orientation
   landscape). A square/landscape hero is NOT --tall, so it keeps the text-below scrim (variety + the
   bottom-nav overlap only happens with tall images that push text to the very bottom). */
/* + narrow-portrait alternative: a portrait viewport <=639px wide is a PHONE SHAPE regardless of
   DPR — budget Androids report devicePixelRatio 2.0 and were falling through to the base scrim
   (Patrick, TEG web-reader test at DPR 2.0). A desktop pane is never narrow-portrait, so the
   Windows-scaling (DPR 2.0-2.49, WIDE window) collision this floor guards against stays excluded. */
@media (orientation: portrait) and (min-resolution: 2.5dppx), (orientation: portrait) and (max-width: 639px) {
  /* Hero grows to fit the WHOLE image — text on top, image at its natural height below. NO
     max-height:100vh / overflow:hidden cap: that squeezed a very-tall image (RM 72-1, 824×1400, h/w 1.70)
     into the leftover space and CLIPPED its bottom — it could never show in full. The user scrolls
     through a tall image; nothing is cropped. */
  .article-hero--scrim.article-hero--tall { display: flex; flex-direction: column;
                             background: var(--panel-bg, #0a0a0a); }
  .article-hero--scrim.article-hero--tall .article-hero__text { order: 0; position: relative; z-index: 2;
                             background: var(--panel-bg, #0a0a0a); margin: 0;
                             /* 28px = the app's measured top gap on portrait (landscape's is 40px → ~12px
                                tighter there, acceptable). Adjust if the app changes its chrome inset. */
                             padding: 28px calc(2 * var(--side-pad)) 14px; }
  .article-hero--scrim.article-hero--tall .article-hero__media { order: 1; position: relative; }
  .article-hero--scrim.article-hero--tall .article-hero__media > img { width: 100%; height: auto;
                             max-height: none; object-fit: contain; display: block; }
  /* the image fades IN from the black above (top edge) — overrides the base bottom-fade ::after */
  .article-hero--scrim.article-hero--tall .article-hero__media::after { content: ''; position: absolute;
                             top: 0; left: 0; right: 0; bottom: auto; height: 72px;
                             background: linear-gradient(to bottom, var(--panel-bg, #0a0a0a) 0%, transparent 100%); }
}

/* VERY TALL + THIN hero (phase4 `.article-hero--very-tall`, h/w ≥ VERY_TALL_HW 1.4) → SIDE-BY-SIDE
   (text scrim left, image contained right) so the tall image shows in full instead of being cropped.
   GATED to HIGH-DPR (min-resolution > 2) so it's a PHONE-LANDSCAPE treatment ONLY: on the LOW-DPR
   desktop/tablet/web-reader branch the established desktop flip's `--portrait` sub-rule already lays a
   portrait hero beside the text (light, no crop), so applying this dark side-by-side there fought the
   flip — black panel (this rule) + dark-on-light text (the flip) = illegible (RM 72-1), plus the
   headshot+image both floated right (66-1). High-DPR-only defers desktop to the flip and keeps the
   immersive dark side-by-side for phone landscape. Aspect gate excludes a tall portrait phone (keeps the
   stacked immersive scrim). 3-class selector beats the base scrim; image hugs the right, fade meets its left.
   DPR floor is 2.5 (was 2.01) so it aligns with the other phone gates: in band L (DPR < 2.5) a very-tall
   hero defers to the flip's --portrait sub-rule (light, image-beside-text, uncropped) — matching this
   rule's documented "phone-only, defer desktop to the flip" intent and leaving NO overlap with the flip
   (the overlap was the 72-1 black-bg+dark-text half-apply). */
@media (min-aspect-ratio: 3/5) and (min-resolution: 2.5dppx), (min-aspect-ratio: 3/5) and (max-height: 500px) {
  /* No forced min-height: the IMAGE drives the hero height (it's the tallest item), so the panel ends
     exactly where the image does — no letterbox. The image is its natural size, capped at 80vh tall and
     60% wide (so the text always has room on the left). */
  .article-hero--scrim.article-hero--portrait.article-hero--very-tall {
                             display: flex; flex-direction: row; align-items: stretch;
                             overflow: hidden; background: var(--panel-bg, #0a0a0a); }
  .article-hero--scrim.article-hero--portrait.article-hero--very-tall .article-hero__text {
                             order: 0; flex: 1 1 0; min-width: 0; background: var(--panel-bg, #0a0a0a);
                             margin: 0; z-index: 1; display: flex; flex-direction: column;
                             justify-content: center; padding: 28px var(--side-pad) 28px calc(2 * var(--side-pad)); }
  .article-hero--scrim.article-hero--portrait.article-hero--very-tall .article-hero__media {
                             order: 1; flex: 0 1 auto; min-width: 0; max-width: 60%; position: relative;
                             overflow: hidden; display: flex; align-items: center; justify-content: flex-end; }
  .article-hero--scrim.article-hero--portrait.article-hero--very-tall .article-hero__media > img {
                             width: auto; max-width: 100%; height: auto; max-height: 80vh;
                             object-fit: contain; object-position: right center; display: block; }
  /* :not(--hard) → a coloured panel (96-1) keeps its HARD vertical edge (the --hard display:none rule wins);
     other panels get a soft fade from the panel colour into the image's left edge. */
  .article-hero--scrim.article-hero--portrait.article-hero--very-tall:not(.article-hero--hard) .article-hero__media::after {
                             content: ''; position: absolute; inset: 0 auto 0 0; width: 90px; height: auto;
                             background: linear-gradient(to right, var(--panel-bg, #0a0a0a), transparent); }
}

/* LANDSCAPE PHONE beats T4 (must follow it — equal specificity, cascade order decides): a
   very-tall hero contained at 80vh in the short landscape strip is a postage stamp (WCM 26-1:
   162px beside a 907px panel) — give it the same 40% cover panel as the other tall heroes.
   T4's contain stays for its real audience (iPad PORTRAIT / squarish desktop panes). */
@media (orientation: landscape) and (min-resolution: 2.5dppx), (orientation: landscape) and (max-height: 500px) {
  .article-hero--scrim.article-hero--portrait.article-hero--very-tall .article-hero__media {
      flex: 0 0 40%; max-width: 46%; }
  .article-hero--scrim.article-hero--portrait.article-hero--very-tall .article-hero__media > img {
      width: 100%; height: 72vh; max-height: 72vh; object-fit: cover; object-position: 50% 28%; }
}

/* Tag styling retained for the (separate, currently-unused) spread-hero path. */
.hero-top-tags { position: absolute; top: 14px; left: calc(2 * var(--side-pad)); right: calc(2 * var(--side-pad)); z-index: 1; display: flex; }
.hero-tag { background: var(--accent); color: #fff; font-size: 10px; font-weight: 700;
            padding: 4px 10px; letter-spacing: 0.1em; text-transform: uppercase;
            font-family: var(--font-family-condensed); }

/* Portrait float hero — tall image right, text left */
.article-hero-portrait-float { display: flex; flex-direction: row; gap: 20px;
                                margin-bottom: 1.5rem; align-items: flex-start; }
.portrait-float-img { flex: 0 0 42%; max-width: 42%; }
.portrait-float-img img { width: 100%; height: auto; display: block; }
.portrait-float-text { flex: 1; padding: 4px 0; }

/* Spread / stitched hero */
.article-spread-hero { margin: -64px -1.5rem 0; }
.spread-hero-img-wrap { position: relative; overflow: hidden; background: #0a0a0a; }
.spread-hero-img-wrap img { width: 100%; display: block; max-height: 52vw;
                             object-fit: cover; object-position: center; }
.spread-hero-img-wrap .hero-top-tags { position: absolute; top: 14px; left: 14px; }
.spread-hero-text { padding: 20px 1.5rem 0; }


/** INLINE IMAGE SHAPES **/

.article-in-image.shape-circle { display: flex; flex-direction: column; align-items: center; }
.article-in-image.shape-circle img { border-radius: 50%; width: 180px; height: 180px; object-fit: cover; }
.article-in-image.shape-rounded img { border-radius: 12px; }
.article-in-image.portrait-float { float: right; max-width: 38%; margin: 4px 0 16px 14px;
                                    clear: right; }
.article-in-image.portrait-float img { display: block; max-height: 380px;
                                        width: auto; max-width: 100%; height: auto;
                                        margin: 0 auto; }

.article-in-image.contained { float: right; width: 44%; margin: 4px 0 16px 14px; clear: right; }
.article-in-image.contained img { width: 100%; height: auto; }
.article-in-image.inline-float-right { float: right; max-width: min(44%, 400px); margin: 4px 0 16px 14px; clear: right; }
.article-in-image.inline-float-right img { display: block; width: auto; max-width: 100%; height: auto;
                                           max-height: 380px; object-fit: contain; margin: 0 auto; }

/* Lone float at end of article: center instead of hanging right */
.article-in-image.portrait-float:last-child,
.article-in-image.contained:last-child,
.article-in-image.inline-float-right:last-child { float: none; width: 50%; margin: 16px auto; display: block; }

/* A contributor byline headshot floats LEFT (beside its byline), on the OPPOSITE side from a
   right-floating lead image, so the two don't stack vertically and drop the headshot away from the
   name (WCM 23-1: the trophy lead-float pushed Ben Gardner's headshot below his byline). Its left
   edge aligns with the body text (margin-left = --side-pad). The headshot shows in its NATURAL shape
   (square photos render square) — overriding shape-circle's forced 50% crop, which the AI's
   display_shape often gets wrong (WCM 23-1's are clearly square in print). Overrides portrait-float. */
.article-in-image.contributor-byline-float {
    float: left; clear: left;
    width: auto; max-width: 88px; margin: 2px 14px 6px var(--side-pad);
}
.article-in-image.contributor-byline-float img {
    width: auto; height: auto; max-width: 88px; max-height: 88px;
    border-radius: 4px; object-fit: contain;   /* natural shape, not a forced circle */
}
.article-in-image.contributor-byline-float:last-child {
    float: left; width: auto; margin: 2px 14px 6px var(--side-pad);
}

/* Photo-feature card: an Image_Asset carrying a substantial attached panel (nested Boxout or
   rich caption). The image goes full-width (never floated) and the panel renders as a readable
   block beneath. Portrait images are height-capped (max-height) so they don't fill the screen. */
.article-feature-card { clear: both; width: 100%; margin: 20px 0; }
.article-feature-card img { display: block; width: auto; max-width: 100%; height: auto;
                            max-height: 70vh; margin: 0 auto; }
/* Inside a feature CAROUSEL (one card at a time), a tall/portrait image made its slide much taller
   than the landscape ones, leaving a big white gap below the shorter slides (Patrick RM 47-1 "one
   massive image + text"). Cap the carousel's feature images to ~a landscape height so the slides
   are near-uniform; standalone feature cards keep the looser 70vh. Aspect preserved (no crop). */
.feature-gallery .article-feature-card img,
.feature-gallery .article-feature-card.portrait > img { max-height: 420px; }
.article-image-feature { border-top: 3px solid var(--accent, #333); background: #f6f6f6;
                         padding: 14px 18px 16px; font-family: var(--font-family-main); }
.article-image-feature .feature-heading { font-family: var(--font-family-main-display, var(--font-family-main));
                                          color: var(--accent-on-light, var(--accent, #1a1a1a)); font-weight: 700;
                                          font-size: 1.15rem; line-height: 1.15; margin: 0 0 8px;
                                          text-transform: uppercase; letter-spacing: .02em; }
.article-image-feature .feature-body { margin: 0 0 8px; line-height: 1.5; }
.article-image-feature .feature-body:last-of-type { margin-bottom: 0; }
.article-image-feature .feature-credit { font-style: italic; font-size: 0.8rem; color: #777;
                                         margin: 8px 0 0; }
/* Portrait image-led feature: a tall image leaves horizontal room, so on desktop place the panel
   BESIDE it (image left, text right) — matches the print layout (e.g. FFT 'Pin badge heaven').
   Mobile keeps the default stacked image-over-text. Only the accent rule moves from top to left. */
@media (min-width: 640px) and (max-resolution: 2dppx) {
  .article-feature-card.portrait { display: flex; flex-direction: row; gap: 0; align-items: stretch; }
  .article-feature-card.portrait > img { max-width: 45%; max-height: 80vh; margin: 0; flex: 0 0 auto; }
  .article-feature-card.portrait .article-image-feature { flex: 1 1 0; }   /* keeps the accent top-rule, sits flush against the image */
}

/* Lone image at a page break (set by phase4 _handle_page_break_images): centred block, never
   floated — stops an end-of-page image wrapping the next page's unrelated text. */
.article-in-image.page-centered { float: none; clear: both; width: auto; max-width: 60%;
                                  margin: 16px auto; display: block; }
.article-in-image.page-centered img { display: block; width: auto; max-width: 100%; height: auto;
                                      max-height: 60vh; margin: 0 auto; }


/* Solo: lonely float demoted to centred block by phase4 post-processing */
.article-in-image.solo {
    float: none; clear: both; display: block; width: fit-content; max-width: 65%; margin: 16px auto;
}
/* Tall-image ceiling (Patrick, 2026-07-13): no inline image runs taller than ~two-thirds of the
   viewport — a tall portrait SHRINKS (width narrows, centred in the column) instead of becoming an
   endless scroll. width:auto is essential: the old width:100% + max-height pair DISTORTS a portrait
   once the cap bites. Self-gating — images under the ceiling render exactly as before. */
.article-in-image.solo img { display: block; width: auto; max-width: 100%; height: auto;
    max-height: 66vh; margin-left: auto; margin-right: auto; }
.gallery-viewer.gallery-inset .article-in-image img { max-height: 62vh; }

/* Boxout images: float right within the boxout; overflow:hidden on boxout creates BFC to contain float */
.article-boxout .article-in-image.portrait-float {
    float: right; max-width: 42%; margin: 4px 0 8px 12px; clear: right;
}
.article-boxout .article-in-image.portrait-float img {
    display: block; max-height: 220px; width: auto; max-width: 100%; height: auto; margin: 0 auto;
}
.article-boxout .article-in-image.contained {
    float: right; max-width: 44%; margin: 4px 0 8px 12px; clear: right;
}
.article-boxout .article-in-image.contained img {
    display: block; max-height: 220px; width: auto; max-width: 100%; height: auto; margin: 0 auto;
}
.article-boxout .article-in-image.inline-float-right {
    float: right; max-width: min(44%, 300px); margin: 4px 0 8px 12px; clear: right;
}
.article-boxout .article-in-image.inline-float-right img {
    display: block; max-height: 220px; width: 100%; height: auto;
}
/* Lone image at bottom of boxout: centre instead of hanging right */
.article-boxout .article-in-image.portrait-float:last-child,
.article-boxout .article-in-image.contained:last-child,
.article-boxout .article-in-image.inline-float-right:last-child {
    float: none; width: 55%; margin: 8px auto; display: block; clear: both;
}
.article-boxout .article-in-image.portrait-float:last-child img,
.article-boxout .article-in-image.contained:last-child img,
.article-boxout .article-in-image.inline-float-right:last-child img {
    width: 100%; height: auto; max-height: 220px;
}
/* Boxout with table + image: CSS Grid so image + caption align top with subhead/table.
   minmax(0, 1fr) lets the table column shrink below content min-width so the image
   column (min 80px) is never crowded out by a wide multi-column table. */
.article-boxout:has(table):has(.article-in-image) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(80px, 35%);
    column-gap: 12px;
    align-items: start;
}
.article-boxout:has(table):has(.article-in-image) .article-in-image.portrait-float,
.article-boxout:has(table):has(.article-in-image) .article-in-image.contained,
.article-boxout:has(table):has(.article-in-image) .article-in-image.inline-float-right {
    float: none; clear: none; margin: 0; width: 100%; max-width: 100%;
    grid-column: 2; grid-row: 1 / span 10;
}
.article-boxout:has(table):has(.article-in-image) .article-in-image.portrait-float img,
.article-boxout:has(table):has(.article-in-image) .article-in-image.contained img,
.article-boxout:has(table):has(.article-in-image) .article-in-image.inline-float-right img {
    display: block; width: auto; max-width: 100%; max-height: 220px; height: auto; margin: 0 auto;
}
/* WIDE table (phase-4 `boxout-wide-table`, ≥6 cols): side-by-side would overflow the grid column and
   overlap the image (WCM 64-1), so STACK — table full-width, image centred BELOW it. The extra class
   out-specifies the grid rule above ((0,3,1) > (0,2,1)). Narrow table+image boxouts keep side-by-side. */
.article-boxout.boxout-wide-table:has(table):has(.article-in-image) { display: block; }
.article-boxout.boxout-wide-table:has(table):has(.article-in-image) .article-in-image {
    grid-column: auto; grid-row: auto; float: none; clear: both;
    width: 100%; max-width: 100%; margin: 14px auto 0;
}
.article-boxout.boxout-wide-table:has(table):has(.article-in-image) .article-in-image img {
    max-height: 320px; margin: 0 auto;
}


/** CAROUSEL — multi-image gallery **/

/* Explicit gallery container — phase 4 wraps gallery runs in .gallery-viewer (decision in the
   markup). carousel.js upgrades it to .img-carousel (the full arrow carousel) in browser / web-reader
   / QA. Where that JS can't run (the in-app webview strips injected DOM + leaves .style undefined,
   so carousel.js restore-on-strip removes .img-carousel) this :not(.img-carousel) state IS the
   gallery: a one-image-per-frame swipeable snap strip. Everything below is scoped to that fallback
   state, so it is a provable no-op once the JS carousel takes over. */
.gallery-viewer:not(.img-carousel) { display: flex; overflow-x: auto; gap: 8px;
    scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; clear: both;
    align-items: stretch; /* slides share the tallest height so a short image centres in a grey frame */
    touch-action: pan-x pan-y;   /* horizontal swipe drives the gallery; vertical still scrolls the PAGE */
    overscroll-behavior-x: auto; /* at the LAST image a further horizontal swipe chains to the reader's
                                    page-turn (so you can't get stuck); the snap keeps mid-gallery swipes here */
    margin: 1.25rem calc(-1 * var(--side-pad)); padding: 0 var(--side-pad); }
/* PEEKING slides (88%, centre-snapped): the neighbouring image visibly pokes in at the edge — the
   strongest "there's more, swipe" signpost (standard carousels bury content; <1% click-through).
   Each slide is a grey frame that vertically centres its image, so a short image leaves no white
   gap below it. Fallback only — the JS .carousel-slide centres on its own dark box. */
/* TOP-aligned, not centred (Patrick, SAM_Aug 45-1: "there's a different starting position for both
   the image and the text"). Slides share the tallest slide's height, so centring each image pushed
   a slightly shorter one down and its caption with it — every frame started somewhere different.
   Top-aligning lines the photos up on one edge; any slack now falls below the caption, and the
   caption is clamped (below) so the frames end level too. A set whose shapes differ enough for that
   slack to be ugly is a .gallery-scroller instead — see _scroller_gate. */
/* ⚠️ Scoped :not(.gallery-scroller): these peeking declarations SILENTLY OWNED scroller cells too —
   `width: auto !important` (0,3,0) beat the emitter's per-cell (0,1,0) width classes in EVERY
   engine (Chrome's shrink-wrap masked it by coincidence — the shrink result equals the aspect
   width — while Safari's caption-inclusive shrink-wrap hit the flex-basis 88% capped at 432px:
   Patrick's "fixed width with gaps", cyp 6-1 iPad, proven in real WebKit via Playwright). */
.gallery-viewer:not(.img-carousel):not(.gallery-scroller) > .article-in-image { flex: 0 0 88%; scroll-snap-align: center;
    display: flex; flex-direction: column; justify-content: flex-start; align-items: center;
    background: #f1f1f1; border-radius: 2px;
    float: none !important; width: auto !important; height: auto !important; margin: 0 !important;
    aspect-ratio: unset !important; }
.gallery-viewer:not(.img-carousel):not(.gallery-scroller) > .article-in-image img { width: auto; max-width: 100%; height: auto;
    display: block; max-height: 60vh; object-fit: contain; }
/* Clamp the strip's captions too, so one wordy caption can't make its frame (and therefore EVERY
   frame, since they share a height) enormous. Full text on tap, via the lightbox. */
.gallery-viewer:not(.img-carousel):not(.gallery-scroller) > .article-in-image .articleImageCaption {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3;
    overflow: hidden; }
/* Dot indicator — a markup sibling (<div class="gallery-dots">) emitted by phase4, so it survives the
   in-app reader's DOM stripping (a JS-built one would be stripped). carousel.js highlights the current
   dot on scroll where it can; if that doesn't run, the dots still show the image COUNT. Hidden when the
   JS carousel enhances (it builds its own dots) and on desktop (galleries enhance or float there). */
.gallery-dots { display: flex; justify-content: center; gap: 6px; margin: 8px 0 4px; }
.gallery-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(0,0,0,0.22);
    flex: 0 0 auto; transition: background 0.2s; }
.gallery-dot.active { background: var(--accent, #555); }
.img-carousel + .gallery-dots, .feature-carousel + .gallery-dots { display: none; }
/* NO swipe arrow. A single chevron beside the dots read as a broken control rather than an
   affordance (Patrick 2026-07-25: "the single right arrow doesn't look great") — the dots
   already say "there are N of these", and swiping is the native gesture on the surface where
   this fallback runs. Dots only. */
@media (min-width: 640px) and (max-resolution: 2dppx) { .gallery-dots { display: none; } }

/* FIXED-HEIGHT SCROLLER (phase4 marks a qualifying run .gallery-scroller — see _scroller_gate).
   ONE height for the whole gallery, each photo exactly as wide as its aspect needs, horizontal
   scroll-snap. NO width cap on the CELL: the next photo simply sits beside the previous one, so a
   tall/thin first image leaves the second almost fully visible (that IS the pattern — an earlier
   capped version left a dead white gap and defeated the point). A single WIDE-and-short member is
   capped at ~1.8 and cover-cropped rather than becoming an unreadable strip; tap opens the full
   image. carousel.js deliberately skips these, so they are the scroller on every surface — hence
   the dots stay visible at desktop widths too (the sibling rule below). */
.gallery-viewer.gallery-scroller { --sc-h: 240px; align-items: flex-start; gap: 6px;
    scroll-snap-type: x mandatory;
    /* WebKit's initial snap scrolls the container's own left padding out of view (the first cell
       measured at x=0 with 36px of padding — cyp 6-1 iPad, "the gallery starts on the very left
       hand edge"). scroll-padding re-anchors the snap positions to the padded content edge, so
       the strip opens aligned with the body column in both orientations. */
    scroll-padding: 0 var(--side-pad);
    /* The scroller's shape is COLUMN-LEFT, SCREEN-RIGHT everywhere: kill the base gallery's
       full-bleed LEFT margin so the first photo aligns with the body text on phones too (Patrick,
       mobile 2026-08-10: "the sweep swipe has lost its left padding" — the strip's box started
       24px off-screen and its first image sat at the bare screen edge). The right bleed stays —
       a part-visible photo peeking off the right edge is the swipe affordance. On iPad the in-app
       cap block's --read-inset margin comes later in the file and still wins. */
    margin-left: 0; }
.gallery-viewer.gallery-scroller:not(.img-carousel) > .article-in-image {
    flex: 0 0 auto; scroll-snap-align: start; background: transparent;
    max-width: calc(var(--sc-h) * 1.8); align-items: flex-start; justify-content: flex-start;
    /* Self-sufficient now the peeking block is scoped away (see :not(.gallery-scroller) above):
       the column layout + float/margin normalizations these cells used to inherit from it.
       Deliberately NON-important — beats portrait-float/contained base rules on specificity,
       and still LOSES to the desktop .gallery-float block's !importants, so a float-scroller
       run keeps its desktop float column. The cell's WIDTH comes from the emitter's per-cell
       class (calc(var(--sc-h) * aspect)); no width is declared here so an unknown-shape cell
       keeps the shrink-wrap. */
    display: flex; flex-direction: column; float: none; height: auto; margin: 0;
    aspect-ratio: unset;
    /* Floor so a very narrow photo's caption isn't one word per line. (The cell tracks its IMAGE
       rather than its caption — see the `width: 0` on the caption below.) */
    min-width: 160px; }
.gallery-viewer.gallery-scroller:not(.img-carousel) > .article-in-image img {
    height: var(--sc-h); width: auto; max-width: 100%; max-height: none;
    object-fit: cover; border-radius: 7px; display: block; }
/* CAPTIONS: every image is the same height and top-aligned, so every caption starts at the same
   line — but a long one would still make its cell far taller than the rest (Patrick, FFT 68-1:
   "will the tall thin one end up with a super long box below because of caption… will we need to
   truncate perhaps, or do they swallow the caption and it's visible only when tapped"). Clamped to
   two lines, so the gallery has ONE bottom edge; the full text is in the tap-to-enlarge lightbox,
   which reads textContent and is unaffected by the clamp. */
.gallery-viewer.gallery-scroller .articleImageCaption { font-size: 11px; opacity: .75;
    padding: 4px 2px 0; text-align: left;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
    overflow: hidden;
    /* The CELL must size to its image, not its text: a flex item takes its children's max-content,
       so a wordy caption stretched a 134px tall-thin photo's cell to the full 432px cap and pushed
       the next photo off screen. `width: 0` removes the caption from that calculation; `min-width:
       100%` then lays it out across whatever width the image gave the cell. (Sizing the CELL to
       min-content instead collapses the image, whose percentage max-width has no intrinsic
       contribution — it squashed the wide photo to the 160px floor.) */
    width: 0; min-width: 100%; }
@media (min-width: 640px) and (max-resolution: 2dppx) {
    .gallery-scroller { --sc-h: 300px; }
    .gallery-scroller + .gallery-dots { display: flex; }
}

/* MINI-GRID (Stage-2 inline anchoring): 2–3 page-anchored images at one stop render as a compact
   grid — every photo visible with its page's text, no swiping. Text-width, rounded cells; the
   third image spans the full row. */
.article-mini-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
    margin: 1.1rem var(--side-pad); clear: both; }
.article-mini-grid .mini-grid-cell { display: flex; flex-direction: column; position: relative; }
/* Subtle tap-to-enlarge chip (carousel.js adds it to each grid's FIRST cell only). */
.mg-zoom-hint { position: absolute; top: 6px; left: 6px; width: 24px; height: 24px;
    display: flex; align-items: center; justify-content: center; border-radius: 50%;
    background: rgba(0,0,0,0.38); color: rgba(255,255,255,0.9); pointer-events: none; }
.article-mini-grid .mini-grid-cell img { width: 100%; height: 150px; object-fit: cover;
    border-radius: 7px; display: block; }
.article-mini-grid .mini-grid-cell:nth-child(odd):last-child { grid-column: 1 / -1; }
.article-mini-grid .mini-grid-cell:nth-child(odd):last-child img { height: 210px; }
/* Cells are UNIFORM height (mixed shapes ragged the grid); each image's crop window sits on its
   subject via the phase-4 focal object-position, and the tap-to-open lightbox shows the full
   uncropped image. */
.article-mini-grid .articleImageCaption { font-size: 11px; opacity: .75; padding: 3px 2px 0;
    text-align: left; }
.article-mini-grid .mini-grid-cell img { cursor: pointer; }
@media (min-width: 640px) and (max-resolution: 2.49dppx) {
    .article-mini-grid .mini-grid-cell img { height: 220px; }
    .article-mini-grid .mini-grid-cell:nth-child(odd):last-child img { height: 280px; }
}

/* CELL RATIO FROM THE GROUP (phase4 emits .mg-ratio + --mg-ar = the group's own median source
   ratio). The fixed heights above give an image box of ~1.15 at a real 400px viewport, so a set of
   1.33–1.65 photos was cropped ~20% into a box squarer than its content; sizing the cell from the
   content removes that and the row comes out ~20% shorter. These rules sit AFTER the fixed-height
   block (including its desktop media query) so equal-specificity selectors resolve in their favour.
   Fallback is intact: with no dimensions phase4 emits no class and the fixed heights still apply. */
.article-mini-grid.mg-ratio .mini-grid-cell img { height: auto; aspect-ratio: var(--mg-ar); }
/* The trailing ODD cell spans both columns (~2x wide), so widen its ratio to keep it ONE taller row
   rather than a giant band — 1.43 reproduces the fixed grid's own 210/150 step. */
.article-mini-grid.mg-ratio .mini-grid-cell:nth-child(odd):last-child img {
    height: auto; aspect-ratio: calc(var(--mg-ar) * 1.43); }
/* EVERY cell cover-crops to that shape — Patrick, reviewing the first build: "for a wide pic we fit
   to height, chop the edges and know they are tappable for full… every image in a grid is an even
   panel". The first build letterboxed members outside ±15% on a grey panel; on real pages that
   greyed 3 of 5 cells (FFT 14-1) and read worse than the crop it was avoiding. (No rule needed —
   `object-fit: cover` is already set on the cell image above; this note records the decision.) */
/* WIDE-FORMAT group (median > 2.2 — scale plans, panoramas): one full-width column at natural
   height. Halving the width of a 4.3-ratio profile drawing makes it unreadable; print runs them
   full width, so we do too. */
.article-mini-grid.mg-wide { grid-template-columns: 1fr; }
.article-mini-grid.mg-wide .mini-grid-cell img,
.article-mini-grid.mg-wide .mini-grid-cell:nth-child(odd):last-child img {
    height: auto; aspect-ratio: auto; object-fit: contain; }
/* 3 ACROSS on desktop/tablet for a big group (phase4 .mg-3col at ≥ 7 images): at 2 columns a
   14-image grid runs to ~2400px on desktop. Below 640px it stays 2 across. The odd-last cell does
   not span in a 3-column grid (the remainder isn't reliably 1 short), so it keeps the normal cell. */
@media (min-width: 640px) and (max-resolution: 2.49dppx) {
    .article-mini-grid.mg-3col { grid-template-columns: 1fr 1fr 1fr; }
    .article-mini-grid.mg-3col .mini-grid-cell:nth-child(odd):last-child { grid-column: auto; }
    .article-mini-grid.mg-3col.mg-ratio .mini-grid-cell:nth-child(odd):last-child img {
        height: auto; aspect-ratio: var(--mg-ar); }
}

/* Lightbox (carousel.js enhancement on mini-grid cells): fullscreen dark overlay, the FULL
   uncropped image contained, caption below, tap anywhere / × to close, ‹ › within the grid.
   Enhancement-only — where JS doesn't run the grid stays a static (still complete) layout. */
.mg-lightbox { position: fixed; inset: 0; z-index: 99998; background: rgba(8,8,10,.93);
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 4vh 4vw; }
.mg-lightbox img { max-width: 92vw; max-height: 78vh; width: auto; height: auto;
    object-fit: contain; border-radius: 6px; }
.mg-lightbox .mg-cap { color: #ddd; font-size: 13px; margin-top: 10px; max-width: 80ch;
    text-align: center; }
.mg-lightbox .mg-x { position: absolute; top: 14px; right: 18px; color: #fff; font-size: 26px;
    background: none; border: none; cursor: pointer; line-height: 1; }
.mg-lightbox .mg-prev, .mg-lightbox .mg-next { position: absolute; top: 50%;
    transform: translateY(-50%); color: #fff; font-size: 30px; background: rgba(255,255,255,.12);
    border: none; border-radius: 50%; width: 44px; height: 44px; cursor: pointer; }
.mg-lightbox .mg-prev { left: 10px; }
.mg-lightbox .mg-next { right: 10px; }

/* INSET gallery (scrim → inset; Patrick, image-led-titles design 2026-07-13): the image run after an
   immersive scrim renders TEXT-WIDTH with side padding instead of full-bleed — the BBC-Sport-style
   framed image that breaks the "scrim then endless full-bleed image" wall. The class stays on the
   container through JS enhancement, so it constrains the fallback strip AND the arrow carousel. */
.gallery-viewer.gallery-inset { margin-left: var(--side-pad) !important;
    margin-right: var(--side-pad) !important;
    padding-left: 0 !important; padding-right: 0 !important; border-radius: 6px; }

/* Float gallery (phase 4 marks a 2–3 image run with text around it as .gallery-float). On DESKTOP
   the wrapper becomes display:contents so its float children rejoin the article flow and the body
   copy wraps beside them; each image floats right and clears the previous (a tidy right column).
   On mobile it stays a plain .gallery-viewer (scroll-snap strip / carousel — carousel.js skips
   .gallery-float on desktop, see carousel.js). Selectors match the DOM tree, so the child combinator
   still works under display:contents. */
@media (min-width: 640px) and (max-resolution: 2dppx) {
    .gallery-viewer.gallery-float { display: contents; }
    /* !important to beat the .gallery-viewer:not(.img-carousel) scroll-snap fallback (which forces
       float:none !important on gallery children at all widths) — on desktop these float instead. */
    .gallery-viewer.gallery-float > .article-in-image { float: right !important; clear: right !important;
        width: 38% !important; max-width: 38% !important; margin: 4px 0 16px 18px !important;
        aspect-ratio: unset !important;
        /* undo the mobile fallback's grey centred frame — on desktop these are plain floated images */
        display: block !important; background: transparent !important; border-radius: 0 !important; }
    .gallery-viewer.gallery-float > .article-in-image img { width: 100% !important; height: auto !important;
        display: block !important; max-height: 60vh !important; object-fit: contain !important; }
}

.img-carousel { margin: 1.25rem 0; overflow: hidden; clear: both; min-height: 200px; }
.carousel-img-wrap { position: relative; }
.carousel-track { display: flex; transition: transform 0.3s ease; }
.carousel-slide { flex: 0 0 100%; min-width: 0; background: #111; min-height: 200px;
                  display: flex; align-items: center; justify-content: center; }
.carousel-slide img { width: auto; max-width: 100%; max-height: 52vh; height: auto;
                      display: block; margin: 0 auto; }
.img-carousel .article-in-image { float: none !important; width: 100% !important;
                                   height: auto !important; margin: 0 !important;
                                   aspect-ratio: unset !important; overflow: visible !important; }
.img-carousel .article-in-image img { width: auto !important; max-width: 100% !important;
                                   height: auto !important; max-height: 52vh !important;
                                   object-fit: unset !important; display: block !important;
                                   margin: 0 auto !important; }
/* The enhanced carousel shows each slide's caption in its OWN bottom bar (.carousel-caption), so hide
   the in-slide .articleImageCaption — otherwise the caption rendered TWICE (once on the photo, once in
   the bar). Scoped to .img-carousel, so the JS-off scroll-snap fallback (no bar) still shows it. */
.img-carousel .carousel-slide .articleImageCaption { display: none; }
.carousel-scrim { position: absolute; bottom: 0; left: 0; right: 0;
                  background: linear-gradient(transparent, rgba(0,0,0,0.82) 60%, rgba(0,0,0,0.88));
                  padding: 36px 12px 9px; }
.carousel-caption { font-family: var(--font-family-condensed); font-size: 11px;
                    color: rgba(255,255,255,0.82); text-align: center; margin: 0 0 7px; line-height: 1.4; }
.carousel-ui { display: flex; align-items: center; justify-content: space-between; }
.carousel-btn { background: none; border: 1.5px solid rgba(255,255,255,0.45); border-radius: 50%;
                width: 28px; height: 28px; font-size: 18px; cursor: pointer; color: #fff;
                display: flex; align-items: center; justify-content: center;
                padding: 0 0 2px; line-height: 1; flex-shrink: 0; }
.carousel-btn:hover { background: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.9); }
.carousel-dots { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; justify-content: center; }
.carousel-dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(255,255,255,0.4);
                cursor: pointer; transition: background 0.2s; flex-shrink: 0; }
.carousel-dot.active { background: var(--accent); }


/** FEATURE GALLERY — a run of ≥2 photo-feature cards shown ONE AT A TIME with side arrows, so each
    impact image keeps its breathing room (phase4 _wrap_feature_galleries). Unlike .img-carousel,
    each slide keeps the WHOLE card (image + heading/blurb panel below). carousel.js enhances it to
    .feature-carousel; without JS it degrades to a horizontal scroll-snap strip. The arrow box uses
    the per-article --accent / --standfirst-fg, so it is inherently per-title (FFT Upfront accent is
    yellow → yellow box + black arrow); a {Title}-overrides.css can restyle further. **/
/* Same one-per-frame in-app fallback as .gallery-viewer, but each frame is the WHOLE feature card
   (image + heading/blurb panel). Scoped to :not(.feature-carousel) so the JS arrow carousel is
   untouched wherever it runs. */
.feature-gallery:not(.feature-carousel) { display: flex; overflow-x: auto; gap: 14px;
    scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; clear: both; margin: 1.5rem 0;
    align-items: flex-start; touch-action: pan-x pan-y; overscroll-behavior-x: auto; }
/* One card per frame (the .gallery-dots sibling shows there are more; page-turn chains past the last). */
.feature-gallery:not(.feature-carousel) > .article-feature-card { flex: 0 0 100%; scroll-snap-align: center; margin: 0; }

.feature-carousel { position: relative; clear: both; margin: 1.5rem 0; overflow: hidden; }
.feature-carousel-wrap { position: relative; }
.feature-carousel .carousel-track { display: flex; transition: transform 0.3s ease; align-items: flex-start; }
.feature-carousel-slide { flex: 0 0 100%; min-width: 0; }
.feature-carousel-slide > .article-feature-card { margin: 0; }
/* Side arrows: accent box, luminance-correct arrow. JS centres `top` on the current card's image;
   50% is the pre-measure fallback. */
.feature-carousel-btn { position: absolute; top: 50%; transform: translateY(-50%); z-index: 4;
    width: 44px; height: 44px; border: none; cursor: pointer; padding: 0 0 3px;
    font-size: 1.7rem; line-height: 1; display: flex; align-items: center; justify-content: center;
    background: var(--accent, #222); color: var(--standfirst-fg, #fff);
    box-shadow: 0 1px 4px rgba(0,0,0,0.25); }
.feature-carousel-btn.prev { left: 0; }
.feature-carousel-btn.next { right: 0; }
.feature-carousel-btn:hover { filter: brightness(0.92); }
.feature-carousel-dots { display: flex; gap: 7px; align-items: center; justify-content: center;
    flex-wrap: wrap; padding: 10px 0 0; }
.feature-carousel-dots .carousel-dot { width: 8px; height: 8px; background: rgba(0,0,0,0.22); }
.feature-carousel-dots .carousel-dot.active { background: var(--accent); }


/** PUZZLE WRAPPER — name → blurb → interactive widget, centred + brand font.
   phase-4 wraps each (non-quiz) puzzle in .article-puzzle, pulling the puzzle's title+blurb <body>
   out to render BEFORE the widget (it's emitted after the grid in the XML). The brand font here also
   fixes the crossword clues (which had no font of their own and fell back to a serif default). **/
/* A SQUIGGLE above each puzzle marks where one ends and the next begins (Patrick: "it's easy for them
   all just to run into each other"). Inline SVG data-URI so it needs no asset, tinted with the
   article's accent via currentColor-ish stroke; ::before means no article XML changes. */
.article-puzzle::before {
  content: ""; display: block; height: 12px; margin: 0 auto 1.1rem; max-width: 12em;
  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="12" viewBox="0 0 48 12"><path d="M0 6q6-6 12 0t12 0 12 0 12 0" fill="none" stroke="%23b9b4a8" stroke-width="2" stroke-linecap="round"/></svg>') repeat-x center / 48px 12px;
  opacity: .85;
}
/* NB no :first-of-type suppression — that selector matches by TAG not class, so it never fired here;
   and "above each puzzle" is what was asked for, the first one separating it from the copy above. */
.article-puzzle { font-family: var(--font-family-main); text-align: center; margin: 1.5rem auto;
  clear: both; padding: 0 var(--side-pad); }
.article-puzzle-title { font-family: var(--font-family-main-display); font-weight: var(--font-weight-x-bold, 800);
  font-size: 1.5rem; line-height: 1.15; color: var(--main-color); margin: 0 0 0.3em; }
.article-puzzle-intro { font-weight: 600; color: var(--main-color); margin: 0 auto 0.7em;
  max-width: 34em; line-height: 1.4; }
.article-puzzle .crossword-widget, .article-puzzle .sudoku-widget, .article-puzzle .wordsearch-widget,
.article-puzzle .missingletters-widget, .article-puzzle .jumble-widget, .article-puzzle .spellitout-widget,
.article-puzzle .missingmaths-widget { margin-left: auto; margin-right: auto; text-align: left; }
/* The letters the READER types must be the brand font too. A form control does NOT inherit
   font-family from its container, so every widget's <input> (crossword, sudoku, kriss-kross,
   missing-letters/maths, jumble, spell-it-out, set-square, codeword) was falling back to the
   browser default while the printed grid around it used the title's font — the widgets set
   font-size/weight per puzzle but none of them set a family. One rule fixes them all, and every
   future widget, without touching the per-widget sizing. (Buttons too, so Check/Reveal match.) */
.article-puzzle input, .article-puzzle button, .article-puzzle select { font-family: inherit; }

/** QUIZ — numbered question list (Puzzle type="quiz"); answers render separately as body **/
/* Inset to match body elements (which all use padding: 0 var(--side-pad)); without this the quiz
   numbers/questions sat ~20px wider than the surrounding copy on both edges. The number circle
   (absolute, left:0 of .quiz-q) then aligns with the body text's left edge. */
.article-quiz { margin: 1.25rem 0; padding: 0 var(--side-pad); clear: both; }
/* A quiz nested in a PUZZLE wrapper (Aquila's Brainfeeders — quiz + name heading) inherits the
   wrapper's text-align:center, which centres each QUESTION within the number-indented column while
   the number circle hangs at the left edge — so the whole thing reads as pushed right with dead
   space under the number (Patrick, in-app screenshot). Left-align it: the question then shares a
   left edge with its option cells and the number hang reads as a normal numbered list. Also drop
   the quiz's own side-pad inset there — the wrapper already provides it, so nested quizzes were
   double-indented. Scoped to the wrapper: FFT 22-1's quiz has NO .article-puzzle ancestor
   (verified), so an un-wrapped quiz renders exactly as before. */
.article-puzzle .article-quiz { text-align: left; padding: 0; }
.article-quiz .quiz-list { list-style: none; margin: 0; padding: 0; counter-reset: none; }
.article-quiz .quiz-q { position: relative; padding: 0.55em 0 0.55em 2.6em; line-height: 1.4;
    border-bottom: 1px solid rgba(0,0,0,0.08); font-family: var(--font-family-main); }
.article-quiz .quiz-q:last-child { border-bottom: none; }
/* Number in an accent-filled circle with luminance-correct text — per-article --accent drives the
   colour, so it's inherently per-title (FFT Upfront quiz: yellow accent → yellow circle, black
   number, matching the print; a dark-accent title → coloured circle, white number). */
.article-quiz .quiz-n { position: absolute; left: 0; top: 0.45em;
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.9em; height: 1.9em; border-radius: 50%;
    background: var(--accent); color: var(--standfirst-fg, #fff);
    font-family: var(--font-family-main-display); font-weight: 700; font-size: 0.85em; }
/* Answer reveal: native <details>, no JS. A grey rounded "Reveal" button; on click the button is
   hidden and the answer appears in its place. */
.article-quiz .quiz-a { margin-top: 0.4em; }
.article-quiz .quiz-a summary { display: inline-block; cursor: pointer; list-style: none;
    background: #e8e8e8; color: #333; border-radius: 14px; padding: 3px 14px;
    font-family: var(--font-family-condensed); font-size: 0.78em; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.05em; }
.article-quiz .quiz-a summary:hover { background: #dcdcdc; }
.article-quiz .quiz-a summary::-webkit-details-marker { display: none; }
.article-quiz .quiz-a[open] summary { display: none; }   /* answer replaces the button */
.article-quiz .quiz-answer { font-weight: 700; color: var(--main-color); }
/* Question-associated images (quiz_images.json) under the question — a single photo for "name this
   player", or a face grid for "name these four managers". Count- and width-aware: 2-up on mobile (4
   → clean 2×2), up to 4-up on desktop, natural aspect (no forced crop), each capped so a bigger/
   landscape image from a different design isn't squashed into a tiny fixed cell. A lone image shows
   larger. flex-basis + cap handles any count (1/2/3/4/6…) gracefully — it wraps, never a fixed row. */
.article-quiz .quiz-q-images { display: flex; flex-wrap: wrap; gap: 8px; margin: 0.5em 0 0.2em; }
.article-quiz .quiz-q-img { flex: 1 1 calc(50% - 4px); max-width: 160px; }   /* mobile: 2 per row */
.article-quiz .quiz-q-img img { display: block; width: 100%; height: auto; border-radius: 6px; }
.article-quiz .quiz-q-images.single .quiz-q-img { flex: 0 1 auto; max-width: 280px; }  /* lone image larger */
@media (min-width: 640px) and (max-resolution: 2dppx) {
    .article-quiz .quiz-q-img { flex-basis: calc(25% - 6px); max-width: 132px; }   /* desktop: up to 4 per row */
}

/* Step-by-step (recipe/craft/model how-to) — a numbered sequence consolidated by phase-4's
   _group_step_sequences into one consistent ordered block (NGK 48-1 "Make It"). Mobile: a single
   column of numbered cards; ≥640px: a 2-up grid so the steps read compactly. */
.article-steps { margin: 1.4rem 0; padding: 0 var(--side-pad); clear: both;
    display: grid; grid-template-columns: 1fr; gap: 1.1rem; }
.article-step { position: relative; }
/* Uniform media height so adjacent steps in the 2-up grid align (their source photos vary in aspect).
   A 4:3 cover-crop standardises the tiles; the step action is centred so the crop is safe. */
.article-step-media img { display: block; width: 100%; height: auto; border-radius: 12px; margin: 0;
    aspect-ratio: 4 / 3; object-fit: cover; }
/* Number badge: accent-filled circle, luminance-correct text — per-article --accent drives it, so it
   is inherently per-title (NGK Make It: yellow accent → yellow circle, black number, matching print). */
.article-step-n { position: absolute; top: 8px; left: 8px; z-index: 2;
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.9em; height: 1.9em; border-radius: 50%;
    background: var(--accent); color: var(--standfirst-fg, #fff);
    font-family: var(--font-family-main-display); font-weight: 700; font-size: 1em;
    box-shadow: 0 1px 3px rgba(0,0,0,0.35); }
.article-step-text { margin-top: 0.5em; font-family: var(--font-family-main);
    font-size: var(--font-standard, 16px); line-height: 1.45; }
@media (min-width: 640px) and (max-resolution: 2dppx) {
    .article-steps { grid-template-columns: 1fr 1fr; gap: 1.3rem; }
}

/* Section-opener contents (TEG 69-1 "In Season") — tappable entry cards linking to the section's
   articles: number-matched teaser thumb + accent page-number + title + sell. Per-article --accent
   drives the number colour ⇒ inherently per-title. */
.section-toc { margin: 1em 0; padding: 0 var(--side-pad); }
a.section-toc-entry { display: flex; align-items: center; gap: 14px; padding: 12px 0;
    text-decoration: none; color: var(--main-color);
    border-bottom: 1px solid rgba(127,127,127,.28); }
.section-toc a.section-toc-entry:last-child { border-bottom: none; }
.section-toc-thumb { width: 88px; height: 66px; object-fit: cover; border-radius: 6px;
    flex: 0 0 auto; }
.section-toc-num { font-family: var(--font-family-main-display); font-weight: 700;
    color: var(--accent-on-light); font-size: 1.5rem; min-width: 2ch; text-align: right;
    flex: 0 0 auto; }
.section-toc-text { display: flex; flex-direction: column; gap: 2px; }
.section-toc-title { font-family: var(--font-family-main-display); font-weight: 700;
    font-size: 1.05rem; line-height: 1.25; }
.section-toc-sell { font-family: var(--font-family-main); font-size: .9rem; opacity: .75;
    line-height: 1.3; }

/* Head-to-head DEBATE — two writers each arguing a different pick, laid out as two columns
   (FFT 392 27-1 "Who is the best manager…"). Stacked on mobile, side-by-side on desktop; each
   column carries an accent top rule, a small writer headshot, a display-font pick heading, the
   pick's photo, then that writer's case. Per-article --accent drives it ⇒ inherently per-title. */
.article-debate { margin: 1.4rem 0; padding: 0 var(--side-pad); clear: both;
    display: flex; flex-direction: column; gap: 1.6rem; }
.article-debate .debate-column { flex: 1; min-width: 0; border-top: 3px solid var(--accent);
    padding-top: 0.85rem; }
/* CENTRED header: headshot, name/role/handle and the pick stacked + centred at the top. */
.article-debate .debate-head { text-align: center; margin-bottom: 0.55rem; }
.article-debate .debate-headshot { float: none !important; clear: none !important; display: block;
    width: 72px; height: 72px; margin: 0 auto 0.45rem !important; }
.article-debate .debate-headshot img { width: 72px; height: 72px; object-fit: cover; }
.article-debate .debate-writer { margin: 0; font-family: var(--font-family-main);
    font-size: var(--font-small, 14px); line-height: 1.35; }
.article-debate .debate-pick { margin: 0.3rem 0 0; font-family: var(--font-family-main-display);
    font-weight: 700; font-size: 1.6rem; line-height: 1.12; color: var(--accent-on-light, #1a1a1a); }
/* The pick's photo FLOATS RIGHT; the argument text wraps beside it so the column fills (no white
   space below a block image). Each column is a flex item ⇒ its own BFC ⇒ the float is contained. */
.article-debate .debate-column > .article-in-image:not(.debate-headshot) { float: right !important;
    clear: right; width: 44%; max-width: 300px; margin: 0.1rem 0 0.6rem 1rem !important; }
/* width:auto + max-width keeps RATIO when the inherited max-height (from .portrait-float img) clamps a
   tall photo — plain width:100% there stretched the image wide (e.g. the full-width manager shot in-app). */
.article-debate .debate-column > .article-in-image:not(.debate-headshot) img { width: auto; max-width: 100%;
    height: auto; max-height: 380px; object-fit: contain; margin: 0 auto; display: block; }
.article-debate .debate-column .article-full-body:first-of-type { margin-top: 0; }
@media (min-width: 640px) and (max-resolution: 2dppx) {
    .article-debate { flex-direction: row; gap: 2rem; align-items: flex-start; }
}
/* Narrow phones: a 44% float would crush the text → full-width centred block instead. */
@media (max-width: 520px), (min-resolution: 2.5dppx) {
    .article-debate .debate-column > .article-in-image:not(.debate-headshot) { float: none !important;
        width: 100%; max-width: none; margin: 0.3rem auto 0.6rem !important; }
}

/* "TOP TIP" callout — a friendly rounded accent card echoing the printed tip roundel, distinct from a
   plain rectangular info boxout. Per-article --accent drives it, so it's inherently per-title. The
   leading tip label (bold) is tinted with the accent. */
.article-boxout.article-tip { border: 2px solid var(--accent); border-radius: 18px;
    background: color-mix(in srgb, var(--accent) 12%, #fff); }
.article-boxout.article-tip .ld_bold:first-child,
.article-boxout.article-tip b:first-child { color: var(--accent-on-light, var(--accent));
    text-transform: uppercase; letter-spacing: 0.03em; }


/** TABLE OF CONTENTS **/

.toc-issue-label {
    font-family: var(--font-family-condensed);
    font-size: var(--font-x-large);
    font-weight: var(--font-weight-x-bold);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #888888;
    margin: 1rem 0 1.5rem;
    padding: 0 var(--side-pad);   /* align with the body column, not the screen edge */
    line-height: 1;
}

.toc-wrapper {
    /* side padding: with body margin now 0, the contents furniture (section headers, card grid)
       was touching the screen edge (Patrick, RM in-app 2026-07-22) */
    padding: 0 var(--side-pad) 2rem;
}

.toc-card-grid {
    display: block;
    margin-bottom: 1rem;
}

.toc-section-header {
    display: block;
    border-bottom: var(--toc-section-border-bottom);
    margin: 1.5rem 0 0.6rem;
    padding-bottom: var(--toc-section-border-pb);
    grid-column: 1 / -1;
}
.toc-section-header span {
    display: var(--toc-section-span-display);
    font-family: var(--toc-section-span-font);
    font-size: var(--toc-section-span-size);
    font-weight: var(--toc-section-span-weight);
    letter-spacing: var(--toc-section-span-tracking);
    text-transform: uppercase;
    color: var(--sa-on-bg, var(--toc-section-span-fg));
    background: var(--sa, var(--toc-section-span-bg));
    padding: var(--toc-section-span-padding);
    line-height: 1;
}
/* A section whose printed header is an UNFILLED outlined box (Newsweek's thin white-outlined
   small-caps labels) rather than a solid colour band — border + text in the section's own captured
   hue, no fill. Only emitted (phase4.generate_toc_html) when that hue is a deliberate one; a
   uniform/theme-neutral outline (every Newsweek box is plain white) never reaches this class at all,
   so a title's own hand-tuned header CSS (e.g. Newsweek-overrides.css's var(--main-color) border) is
   untouched. Higher specificity than the plain/dark rules above (2/3 classes vs 1/2 + the span type)
   so it wins regardless of source order in either theme. */
.toc-section-header span.toc-sh-outline,
.darktheme .toc-section-header span.toc-sh-outline {
    background: none;
    border: 1.5px solid var(--sa, currentColor);
    color: var(--sa-on-bg, var(--sa, currentColor));
}

/* Placeholder thumbnail — logo on grey bg */
.toc-img-placeholder {
    background: var(--toc-placeholder-bg);
    display: flex;
    align-items: center;
    justify-content: center;
}
.toc-img-placeholder img {
    object-fit: contain !important;
    object-position: center !important;
    padding: 14% !important;
    opacity: 0.55;
}

.toc-card {
    display: flex;
    text-decoration: none;
    color: inherit;
    background: transparent;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.15s;
}
.toc-card:active { background: rgba(0,0,0,0.04); }

/* Feature card — full-width with large hero above, text below */
.toc-feature {
    flex-direction: column;
    margin: 0 0 1px;
}
.toc-feature-img {
    width: 100%;
    aspect-ratio: 16/9;
    overflow: hidden;
    flex-shrink: 0;
}
.toc-feature-img img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center top;
    display: block;
}
.toc-feature-body {
    padding: 0.6rem 1.5rem 0.8rem;
    flex: 1;
}
.toc-feature-body .toc-title {
    font-size: var(--font-large);
}
.toc-feature-body .toc-sell {
    font-size: var(--font-small);
    margin: 0.3rem 0 0.5rem;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    display: -webkit-box;
    overflow: hidden;
}

/* Standard card — thumbnail left, text right */
.toc-standard {
    flex-direction: row;
    align-items: center;
    padding: 0.6rem 1.5rem 0.6rem 0;
    margin: 0;
    border-bottom: 1px solid rgba(0,0,0,0.07);
    gap: 0.75rem;
}
.toc-thumb {
    width: 72px;
    height: 72px;
    object-fit: cover;
    object-position: center top;
    flex-shrink: 0;
    display: block;
}
.toc-standard-body {
    flex: 1;
    min-width: 0;
}
.toc-standard-body .toc-title {
    font-size: var(--font-standard);
}
.toc-standard-body .toc-sell {
    font-size: 13px;
    margin: 0.2rem 0 0;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    display: -webkit-box;
    overflow: hidden;
}

/* Compact card — thumbnail + title + page */
.toc-compact {
    flex-direction: row;
    align-items: center;
    padding: 0.45rem 1.5rem 0.45rem 0;
    border-bottom: 1px solid rgba(0,0,0,0.05);
    gap: 0.75rem;
}
.toc-compact .toc-thumb {
    width: 56px;
    height: 56px;
    flex-shrink: 0;
}
.toc-compact .toc-title {
    font-size: var(--font-standard);
    flex: 1;
    min-width: 0;
}
.toc-compact .toc-page {
    flex-shrink: 0;
}

/* Shared title / sell / page */
.toc-title {
    font-family: var(--toc-title-font);
    font-weight: var(--toc-title-weight);
    color: var(--main-color);
    margin: 0;
    line-height: 1.2;
    text-transform: var(--toc-title-transform);
    letter-spacing: var(--toc-title-tracking);
}
.toc-sell {
    font-family: var(--toc-sell-font);
    font-weight: var(--toc-sell-weight);
    font-style: var(--toc-sell-style);
    color: var(--second-color);
    line-height: 1.4;
    padding: 0;
    margin: 0;
}
.toc-feature-variant {
    display: block;
    font-family: var(--font-family-condensed);
    font-size: var(--font-smallest);
    font-weight: var(--font-weight-x-bold);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--accent);
    margin-bottom: 0.2rem;
}
.toc-page {
    font-family: var(--font-family-condensed);
    font-size: var(--font-smallest);
    font-weight: var(--font-weight-bold);
    color: var(--accent);
    letter-spacing: 0.1em;
    flex-shrink: 0;
    margin-left: 0.5rem;
}
.toc-feature-body .toc-page {
    display: block;
    margin-left: 0;
    margin-bottom: 0.5rem;
}

/* TOC card dark-theme tweaks (class-scoped — see the THEMES note below). */
.darktheme .toc-section-header span { background: var(--sa, var(--toc-section-span-bg)); color: var(--sa-on-bg, var(--toc-section-span-fg)); }
.darktheme .toc-standard { border-bottom-color: rgba(255,255,255,0.07); }
.darktheme .toc-compact { border-bottom-color: rgba(255,255,255,0.05); }
.darktheme .toc-card:active { background: rgba(255,255,255,0.06); }
.darktheme .toc-title { color: var(--main-color-dark); }
.darktheme .toc-sell { color: var(--second-color-dark); }
.darktheme .toc-issue-label { color: var(--forth-color-dark); }
.darktheme .toc-page { color: var(--accent); }
.darktheme .toc-img-placeholder { background: #1e1e1e; }
/* Sepia TOC: borders/dividers warm rather than cool. */
.sepiatheme .toc-standard { border-bottom-color: rgba(91,70,54,0.12); }
.sepiatheme .toc-compact { border-bottom-color: rgba(91,70,54,0.09); }
.sepiatheme .toc-card:active { background: rgba(91,70,54,0.06); }
.sepiatheme .toc-img-placeholder { background: #e7dcc2; }

/* Desktop grid layout */
@media (min-width: 640px) and (max-resolution: 2dppx) {
    .toc-card-grid {
        display: grid;
        grid-template-columns: 1fr 1fr;
        column-gap: 12px;
        row-gap: 0;
        margin-bottom: 1.25rem;
    }

    .toc-feature {
        grid-column: span 1;
        flex-direction: column;
    }
    .toc-feature-img {
        width: 100%;
        height: 220px;
        min-height: 220px;
        aspect-ratio: unset !important;
        background: #e8e8e8;
    }
    .toc-feature-img img {
        width: 100%;
        height: 100%;
        min-height: 220px;
        object-fit: cover;
        object-position: center top;
        display: block;
    }
    .toc-feature-body {
        padding: 0.65rem 0.9rem 0.8rem;
    }
    .toc-feature-body .toc-title {
        font-size: var(--font-medium);
    }
    .toc-feature-body .toc-sell {
        font-size: 13px;
        -webkit-line-clamp: 2;
        margin-top: 0.2rem;
    }

    /* Single-card section: full-width row for any card type */
    .toc-card-grid:not(:has(.toc-card ~ .toc-card)) .toc-feature,
    .toc-card-grid:not(:has(.toc-card ~ .toc-card)) .toc-standard,
    .toc-card-grid:not(:has(.toc-card ~ .toc-card)) .toc-compact {
        grid-column: 1 / -1;
        flex-direction: row;
        align-items: center;
    }
    .toc-card-grid:not(:has(.toc-card ~ .toc-card)) .toc-feature .toc-feature-img {
        width: 240px;
        height: 160px;
        min-height: 160px;
        flex-shrink: 0;
    }
    .toc-card-grid:not(:has(.toc-card ~ .toc-card)) .toc-feature .toc-feature-body {
        flex: 1;
        padding: 0.75rem 1.25rem;
        display: flex;
        flex-direction: column;
        justify-content: center;
    }
    .toc-card-grid:not(:has(.toc-card ~ .toc-card)) .toc-feature .toc-feature-body .toc-title {
        font-size: var(--font-large);
    }

    .toc-standard { align-items: flex-start; }
    .toc-standard-body { align-self: center; }
    .toc-thumb { width: 88px; height: 88px; }
    .toc-compact .toc-thumb { width: 88px; height: 88px; }
}


/** PAGE JUMP **/

p.article-page-jump { font-family: var(--font-family-condensed); font-size: var(--font-small);
                       font-weight: var(--font-weight-bold); text-align: right;
                       color: var(--forth-color); letter-spacing: 0.06em;
                       text-transform: uppercase; margin: 12px 0 4px; padding: 0 var(--side-pad); }
p.article-page-jump a { color: var(--accent); text-decoration: none; }
p.article-page-jump a:hover { text-decoration: underline; }


/** CONTRIBUTOR / HEADSHOT COMPONENTS — pipeline additions **/

.byline-with-headshot { display: flex; align-items: center; gap: 12px;
                         margin: 0 1.5rem 1.25rem; }
.byline-with-headshot .article-full-byline { padding: 0; margin: 0; }
/* The headshot is emitted FIRST in the DOM (so the narrow-mode float below wraps the headline);
   flex `order` puts it back on the RIGHT on desktop. */
.headline-headshot-row { display: flex; align-items: flex-start; gap: 14px; }
.headline-headshot-text { flex: 1; min-width: 0; order: 1; }
.header-headshot { width: 100px; height: 100px; object-fit: cover; border-radius: 50%; flex-shrink: 0; order: 2; margin-top: 2px; margin-right: var(--side-pad); }
/* NARROW devices (the phone treatment): the side-by-side headline|headshot flex cramps the text
   column, badly with a long lead (Patrick, RM "Little Ashton"/"A brush with steam" 2026-07-22).
   Stack instead: the headshot becomes a SMALL top-right float the headline + lead wrap around,
   full text width restored. */
@media (max-width: 520px), (min-resolution: 2.5dppx) {
  .headline-headshot-row { display: block; overflow: hidden; }  /* contain the float so the
      lead (now BELOW the row) starts full-width, not wrapping the headshot */
  .headline-headshot-row .header-headshot {
      float: right; width: 64px; height: 64px; margin: 2px var(--side-pad) 8px 12px; }
}
.byline-headshot { width: 48px; height: 48px; border-radius: 50%;
                   object-fit: cover; flex-shrink: 0; }

.sign-off-with-headshot { display: flex; gap: 12px; align-items: center; margin-top: 1.25rem;
                           padding: 0 var(--side-pad); }
.sign-off-with-headshot .byline-headshot { width: 52px; height: 52px; }

/* Centred-header treatment: a SMALL image sits ABOVE the headline (WCM 5-1 editor's letter,
   WCM 23-1 "Re:View", and the Times-style columnist opener). The image is centred at the top,
   the head block (standfirst/headline/lead/byline) is centred beneath it, and the body then flows
   NORMALLY (left-aligned, dropcap at the true first paragraph — no leading float). Phase-4 emits
   this only when a start-page Image_Asset slot is ≤30% of the page and predominantly above the
   headline; see phase4 _find_centered_header. */
.article-header-centered { text-align: center; padding: 0 var(--side-pad); margin: 0 0 0.4rem; }
.article-header-centered-img { margin: 0.2rem auto 0.9rem; max-width: 62%; }
.article-header-centered-img img {
    display: block; margin: 0 auto;
    max-width: 100%; width: auto; height: auto; max-height: 240px;
    border-radius: 6px;
}
/* A headshot / byline portrait renders at the USUAL small headshot size (~110px), not the larger
   lead-image cap above — e.g. the WCM 5-1 editor portrait. */
.article-header-centered-img.headshot { width: 110px; max-width: 110px; }
/* A WIDE panorama / stitched DPS above the headline (CM 24-1) is the showpiece — full width. */
.article-header-centered-img.wide { max-width: 100%; }
.article-header-centered-img.wide img { max-height: none; width: 100%; }
.article-header-centered-img.headshot img { max-height: 150px; border-radius: 4px; }
/* A circular headshot stays circular (fixed square box, cover-cropped); a natural photo keeps its
   own aspect via the rule above. */
.article-header-centered-img.shape-circle { width: 110px; height: 110px; max-width: 110px; }
.article-header-centered-img.shape-circle img {
    width: 100%; height: 100%; object-fit: cover; border-radius: 50%; max-height: none;
}
.article-header-centered .article-full-headline,
.article-header-centered .article-full-lead,
.article-header-centered .article-full-byline {
    text-align: center; padding-left: 0; padding-right: 0;
}
/* The STANDFIRST keeps its own padding — it's an inline-block PILL, centred by the parent's
   text-align; zeroing its sides let the 20px radius clip the text (CM 24-1 "N | Plan of the month").
   But its default MARGIN is asymmetric (left = --side-pad, to align with the body column) — inside
   the centred header that shifted the whole inline-block ~20px right of centre (cycp 18-1 "Opinion",
   Patrick 2026-07-21) — so the side margins are zeroed here (the padding is kept for the pill case). */
.article-header-centered .article-standfirst { text-align: center; margin-left: 0; margin-right: 0; }
.article-header-centered .article-full-headline { text-wrap: balance; }

/* Numbered roundup — a "this month's products" list: each item = number badge + image + name/blurb,
   paired by the printed number (phase3b.run_roundup) instead of a wall of text + scattered images. */
.article-roundup { margin: 1rem var(--side-pad) 1.5rem; }
.roundup-item {
    display: grid;
    grid-template-columns: auto 96px 1fr;
    gap: 12px 14px;
    align-items: start;
    padding: 0.9rem 0;
    border-top: 1px solid color-mix(in srgb, var(--main-color, #222) 12%, transparent);
}
.roundup-item:first-child { border-top: none; }
.roundup-num {
    font-family: var(--font-family-condensed);
    font-weight: var(--font-weight-x-bold);
    font-size: 1.4rem;
    line-height: 1;
    color: var(--accent, #888);
}
.roundup-img { width: 96px; height: auto; border-radius: 4px; }
.roundup-item:not(:has(.roundup-img)) { grid-template-columns: auto 1fr; }  /* no image → 2 cols */
.roundup-text { min-width: 0; }
.roundup-name {
    margin: 0 0 0.25rem;
    font-family: var(--font-family-condensed);
    font-weight: var(--font-weight-x-bold);
    color: var(--main-color);
}
.roundup-text .article-full-body { margin: 0 0 0.4rem; padding: 0; }

/* Author-bio card — a byline-matched bio at the article end (headshot left, bio text right).
   Readable body-style text (NOT the uppercase condensed pull-quote-credit treatment), set off by a
   subtle accent rule + tint so it reads as an "about the author" box. */
.article-author-bio {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    margin: 2rem var(--side-pad) 1rem;
    padding: 1rem 1.1rem;
    border-top: 3px solid var(--accent, #888);
    background: color-mix(in srgb, var(--accent, #888) 6%, transparent);
    border-radius: 4px;
}
.article-author-bio .author-bio-headshot {
    /* Keep the image's natural (as-extracted / as-printed) shape — fixed width, auto height,
       no circular crop. A gentle radius just matches the card corners. */
    width: 84px; height: auto;
    border-radius: 4px; flex-shrink: 0;
}
.article-author-bio-text {
    margin: 0;
    font-family: var(--font-family-main);
    font-size: var(--font-small);
    line-height: 1.5;
    color: var(--main-color);
    text-transform: none;       /* override any inherited credit casing */
    letter-spacing: normal;
    font-weight: normal;
}
.article-author-bio-text .ld_bold { font-weight: var(--font-weight-bold); }

/* Editorial float headshot — editor's letter portrait circled and floated into body copy */
.editorial-float-headshot {
    float: right;
    width: 120px;
    height: 120px;
    border-radius: 50%;
    overflow: hidden;
    shape-outside: circle(50%);
    margin: 0 0 12px 16px;
    flex-shrink: 0;
}
.editorial-float-headshot img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: top center;
}

.subhead-with-headshot { display: flex; align-items: center; gap: 12px;
                          margin: 1.25rem 1.5rem 0.5rem; }
.subhead-headshot { width: 52px; height: 52px; border-radius: 50%;
                    object-fit: cover; flex-shrink: 0; }
.subhead-with-headshot p.article-subhead { margin: 0; padding: 0; }

.contributor-strip { display: flex; flex-wrap: wrap; gap: 10px;
                     margin: 1rem 1.5rem 1.25rem; }
.contributor-strip-img { width: 52px; height: 52px; border-radius: 50%; object-fit: cover; }

/* Float-based so body paragraphs wrap freely around the headshot. */
.contributor-entry { margin: 12px 0; padding: 0 var(--side-pad); }
.contributor-entry::after { content: ""; display: table; clear: both; }
.contributor-entry .contributor-headshot { float: left; width: 56px; height: 56px;
    object-fit: cover; border-radius: 50%; margin: 2px 12px 6px 0; }
.contributor-entry-text { }
.contributor-entry-text p { padding: 0; }

.secondary-image-strip { display: flex; flex-wrap: wrap; gap: 12px; margin: 1.25rem 0;
                          justify-content: center; }
.secondary-image-strip .article-in-image { margin: 0; }


/** PRODUCT ITEMS **/

.product-item { overflow: hidden; margin-bottom: 1.5rem; clear: both; }
.product-item .article-in-image.portrait-float img,
.product-item .article-in-image.contained img,
.product-item .article-in-image.inline-float-right img { max-height: 285px; }


/** BOXOUTS **/

.article-boxout {
    background-color: var(--boxout-color);
    border: var(--boxout-border);
    border-top: var(--boxout-border-top);
    margin: 1.5rem 1.5rem;
    box-sizing: border-box;
    width: calc(100% - 3rem);
    padding: 1.125rem 1.5rem 1.5rem;
    overflow: hidden;
}

.article-boxout .article-full-headline {
    font-family: var(--font-family-condensed);
    font-size: var(--font-standard);
    font-weight: var(--font-weight-xx-bold);
    color: var(--accent);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0;
    margin: 0 0 0.5rem;
}

/* A handwritten SIGNATURE graphic (editor's letter sign-off — TEG 3-1 "Clare"): a modest inline
   block after the letter text, never floated beside copy. Keyed off the image description. */
/* left-aligned with the BODY COLUMN, not the screen edge: the ink sits where the sign-off text
   does. In-app the cap block below adds --read-inset so it tracks the column on tablets too
   (Patrick, cyp 4-1: "the James signature is centred in the screen, not the body column"). */
.article-in-image.signature-img { float: none; clear: both;
    margin: 0.4rem 0 1rem var(--side-pad); }
.connect-mobile-edition .article-in-image.signature-img {
    margin-left: calc(var(--side-pad) + var(--read-inset, 0px));
    max-width: 640px; }   /* the ink centres over the BODY COLUMN, not the screen (cyp 4-1) */
.article-in-image.signature-img img.article-in-image { max-height: 64px; width: auto; margin: 0; }
/* …and it reads as part of the sign-off, so it sits on the same left edge as the name and email
   beneath it rather than centred over the column (Patrick, cyp 4-1 2026-08-11). */
.article-in-image.signature-img { text-align: left; }
/* A contributor headshot inside a boxout: the block carries a left inset AND centres its contents,
   so the NAME — which is the block's own caption — lands out of line with the role and blurb below
   it (Patrick, cyp 4-1 "the headshots and names are slightly indented… feels like it should follow
   the body copy"). The boxout already supplies the inset, so align both with the copy. */
.article-boxout .article-in-image.retro-headshot { margin-left: 0; text-align: left; }
.article-boxout .article-in-image.retro-headshot img { margin-left: 0; margin-right: 0; }
/* crest/badge/logo — small badge with its NAME centred beneath (a full-width crest looks
   silly; keep logo + county name together, centred — Patrick, WCM 31-1). */
/* width: fit-content so the block hugs the crest+name (not a full-width container) — else the
   QA swap-button / caption bar stretch full width and cover the county name (Patrick). */
/* !important so the badge constraint WINS over the shape class it co-occurs with (portrait-float/
   contained/inline-float-right) AND that class's responsive width:100% inside the phone media
   query — else a portrait-float crest balloons full-width on a phone (WCM 31-1 Middlesex/Worcs/Kent). */
.article-in-image.crest-img { float: none !important; clear: both; width: -moz-fit-content !important;
    width: fit-content !important; max-width: 100%; text-align: center; margin: 0.9rem auto !important; }
.article-in-image.crest-img img { width: 76px !important; height: 76px !important; object-fit: contain;
    display: block; margin: 0 auto 5px; max-height: none !important; }
/* the county NAME reads as a mini SUBHEAD (display font, bold), not shrunken caption text
   (Patrick: the subhead-style treatment was right). */
.article-in-image.crest-img .articleImageCaption { display: block; text-align: center;
    margin: 0; font-family: var(--font-family-main-display); font-weight: 700;
    font-size: 1.05rem; line-height: 1.2; color: var(--main-color); text-transform: none;
    letter-spacing: 0; }
/* book cover — constrained portrait with its caption centred beneath, never full-width */
.article-in-image.book-img { float: none !important; clear: both; width: -moz-fit-content !important;
    width: fit-content !important; max-width: 100%; text-align: center; margin: 0.9rem auto !important; }
.article-in-image.book-img img { width: auto !important; max-width: 180px !important; height: auto !important;
    display: block; margin: 0 auto 5px; max-height: none !important; }
.article-in-image.book-img .articleImageCaption { text-align: center; margin: 0; }

/* A SUBHEAD that IS a kicker label in print (phase-3c: sans face + ≤10pt — TEG 9-1 "In flower
   now", 8pt pink caps Gotham): render as the small caps sans label, not the serif subhead face.
   Class only appears with the 3c attrs ⇒ no-op elsewhere. Beats a per-title serif override via
   the two-class selector. */
.article-subhead.subhead-kicker,
.article-boxout .article-subhead.subhead-kicker {
    font-family: var(--font-family-condensed);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
}

/* Kicker label above a boxout heading (phase-3c `eyebrow` attr — TEG 9-1 "NEW GARDEN"): a small
   caps sans line in its TRUE print colour (inline style; defaults to main text colour). Emitted
   only when 3c captured a printed label ⇒ no-op elsewhere. */
.boxout-eyebrow {
    font-family: var(--font-family-condensed);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    margin: 0.9rem 0 0.1rem;
    padding: 0;
}
.boxout-eyebrow + .article-subhead { margin-top: 0; }
.article-boxout .article-full-subhead {
    font-family: var(--font-family-condensed);
    font-size: var(--font-large);
    font-weight: var(--font-weight-x-bold);
    color: var(--main-color);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    padding: 0;
    margin: 0 0 0.5rem;
}
.article-boxout .article-full-lead {
    font-family: var(--font-family-main);
    font-size: var(--font-standard);
    font-weight: var(--font-weight-standard);
    color: var(--second-color);
    padding: 0;
    margin: 0 0 0.5rem;
}
.article-boxout .article-full-byline {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-bold);
    color: var(--main-color);
    letter-spacing: 0.04em;
    padding: 0;
    margin: 0 0 0.5rem;
}
.article-boxout .article-full-body {
    font-family: var(--font-family-main);
    font-size: var(--font-small);
    font-weight: var(--font-weight-standard);
    color: var(--main-color);
    line-height: 1.55;
    padding: 0;
    margin: 0 0 0.5rem;
}
.article-boxout .article-pull-quote {
    font-family: var(--pull-quote-font);
    font-size: var(--font-medium);
    font-weight: var(--pull-quote-weight);
    font-style: var(--pull-quote-style);
    color: var(--pull-quote-color);
    line-height: 1.2;
    padding: 0;
    margin: 0 0 0.5rem;
    border: none;
}
.article-boxout .article-pull-quote-credit {
    font-family: var(--font-family-condensed);
    font-size: var(--font-small);
    font-weight: var(--font-weight-bold);
    color: var(--forth-color);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0;
    margin: 0 0 0.5rem;
}
.article-boxout table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--font-family-main);
    font-size: var(--font-small);
    font-weight: var(--font-weight-standard);
    margin: 0.4rem 0 0.8rem;
}
/* WIDE EDITORIAL TABLE -> scrolls inside itself instead of being CLIPPED.
   Triggered by Patrick on Wisden ("a few which are way too big to fit on screen, should/could we
   horizontally scroll them?"). The bug is worse than overflow: `html, body` carry
   `overflow-x: clip` (the full-bleed hero wobble fix, ~line 221), so the columns past the edge
   were not merely cut off, they were UNREACHABLE — no scrollbar, no pan, no way to read them.
   Measured editorial widths: Angler's Mail 60-1 = 18 cells, Gardens Illustrated 22-1 = 13,
   WCM 64-1 = 12, Racing Post 11, World Soccer 96-1 = 10.
   `display: block` is what makes the table its own scroll container; its rows still lay out as a
   table (anonymous table boxes), so nothing about the table rendering changes. The body clip —
   and therefore the hero fix — is untouched. */
.article-boxout table,
.article-full-body table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;   /* momentum pan on iOS webviews */
    max-width: 100%;
}
/* ⚠️ PUZZLE GRIDS ARE <table>s TOO — codeword, set-square, crossword, sudoku — and are sized to
   fit as a GRID. Turning one into a scroll container would break the puzzle. This reset must stay
   AFTER the rule above (same specificity, so source order decides). NGK 256 40-1's 27-cell "table"
   is a puzzle grid, not editorial copy — the exact case that makes this exclusion necessary. */
.article-puzzle table {
    display: table;
    overflow: visible;
    max-width: none;
}
.article-boxout th {
    font-weight: var(--font-weight-bold);
    text-align: left;
    padding: 0.1rem 0.5rem 0.3rem 0;
    border-bottom: 1px solid var(--main-color);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: var(--font-smallest);
}
.article-boxout td {
    padding: 0.15rem 0.5rem 0.15rem 0;
    vertical-align: top;
}
.article-boxout .article-subhead {
    font-family: var(--font-family-condensed);
    font-size: var(--font-medium);
    font-weight: var(--font-weight-x-bold);
    color: var(--main-color);
    line-height: 1.2;
    text-transform: none;   /* captured (printed) case — see .article-subhead above */
    letter-spacing: 0.04em;
    padding: 0;
    margin: 0 0 0.5rem;
}
.article-boxout .article-body {
    font-family: var(--font-family-main);
    font-size: var(--font-small);
    font-weight: var(--font-weight-standard);
    color: var(--main-color);
    line-height: 1.55;
    padding: 0;
    margin: 0 0 0.5rem;
}
.article-boxout .articleImageCredit {
    font-family: var(--font-family-condensed);
    font-size: var(--font-smallest);
    color: var(--forth-color);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0;
    margin: 0 0 0.5rem;
}
.article-boxout .contributor-entry { margin: 10px 0; padding-left: 0; padding-right: 0; }
/* inside a boxout the boxout already provides the side inset, so drop .contributor-entry's own
   side-pad padding — otherwise the float-left headshot sits ~side-pad RIGHT of the name/body text
   below it ("above the text but a little over to the right", Patrick WCM 23-1, 2026-07-24). Now the
   headshot's left edge aligns with the name/text. */
.article-boxout .contributor-entry .contributor-headshot { margin-left: 0; }


/** READER OVERRIDE **/

.listenholder { text-align: left !important; padding: 0 var(--side-pad); margin-bottom: 20px; }
.article-pull-quote .ld_bold { font-weight: var(--font-weight-xx-bold); }


/** RETINA / HIGH-DPI ADJUSTMENTS **/

@media only screen and (-webkit-device-pixel-ratio: 3) {
    :root { --side-pad: 1.5rem; }
    .flexy { margin-top: 20px; }
    .article-standfirst { padding: 5px 0 6px; }
    .article-full-byline { margin-bottom: 1.5rem; }
}

/* Feature card with secondary image but no real hero: contained within grey panel */
.toc-feature-img.toc-img-contained {
    background: #e8e8e8;
}
.toc-feature-img.toc-img-contained img {
    object-fit: contain !important;
    object-position: center top !important;
}

/* Mobile: hide thumbnails on standard/compact, indent text to align with feature cards */
@media (max-width: 639px), (min-resolution: 2.5dppx) {
    .toc-standard .toc-thumb,
    .toc-compact .toc-thumb { display: none; }
    .toc-standard { padding-left: 1.5rem; padding-top: 0.9rem; padding-bottom: 0.9rem; }
    .toc-compact { padding-left: 1.5rem; padding-top: 0.75rem; padding-bottom: 0.75rem; }
}
/* (The old scrim duplicate-and-hide rules were removed 2026-06-05 — the hero is now a single
   responsive DOM block, .article-hero--scrim, that needs no per-breakpoint hiding.) */

@media (max-width: 520px), (min-resolution: 2.5dppx) {
    .article-hero-portrait-float { flex-direction: column; }
    .portrait-float-img { flex: 0 0 auto; max-width: 68%; margin: 0 auto; }
    .article-in-image.portrait-float { float: none; width: 100%; max-width: 100%; margin: 16px 0; }
    .article-in-image.portrait-float img { display: block; width: 100%; height: auto; max-height: 60vh;
        object-fit: contain; }  /* contain so a landscape-phone short viewport clamps max-height without stretching */
    /* A byline headshot stays a SMALL left float on mobile too (don't blow it up to full width like a
       normal portrait-float); left edge aligns with the body text; natural (square) shape, not circle. */
    .article-in-image.contributor-byline-float { float: left; width: auto; max-width: 72px; margin: 2px 12px 6px var(--side-pad); }
    .article-in-image.contributor-byline-float img { display: block; width: auto; max-width: 72px; max-height: 72px; height: auto; border-radius: 4px; object-fit: contain; }
    .header-headshot { width: 18vw; height: 18vw; min-width: 56px; min-height: 56px; }
    .article-in-image.contained { float: none; width: 100%; margin: 16px 0; }
    .article-in-image.inline-float-right { float: none; width: 100%; max-width: 100%; margin: 16px 0; }
    .article-in-image.page-centered { float: none; width: 100%; max-width: 100%; margin: 16px 0; }
    .article-in-image.solo { max-width: 100%; }
    /* A boxout image on a PHONE is already un-floated (`float: none` right here), so nothing wraps
       beside it — which made the inherited 55%/180px sizing pure loss: a 1400px-wide photo rendered
       at 165px inside a 306px column (NGK 256 8-1, 258 8-1 + 34-1 — Patrick 2026-08-11, "a lot of
       images in boxouts having their width reduced, when there's an argument for them to be wider").
       The 42-44% caps on DESKTOP are deliberate and untouched: there the image really is floated with
       copy running alongside it.
       `width: auto` + `max-width: 100%` fills the column when the source is big enough and NEVER
       upscales a small extraction — a plain `width: 100%` would blow a 165px crop up to full width. */
    .article-boxout .article-in-image.portrait-float,
    .article-boxout .article-in-image.contained,
    .article-boxout .article-in-image.inline-float-right {
        float: none; max-width: 100%; margin: 10px auto 12px; display: block;
    }
    .article-boxout .article-in-image.portrait-float img,
    .article-boxout .article-in-image.contained img,
    .article-boxout .article-in-image.inline-float-right img {
        display: block; margin: 0 auto; width: auto; max-width: 100%; height: auto;
        max-height: 300px;    /* was 180px — still stops a tall portrait running the whole screen */
        object-fit: contain;  /* preserve ratio when the max-height clamps (landscape phone) */
    }
}


/** THEMES — light is the DEFAULT (no class needed).
 *
 *  The reader detects the OS/user colour scheme and sets a class on the body/container in code:
 *  `.darktheme` or `.sepiatheme`. It never sets a `.lighttheme` class — light is just the base CSS
 *  above. So dark and sepia are class-scoped OVERRIDES (this also lets the user override the OS
 *  setting from inside the reader). We do NOT use `@media (prefers-color-scheme)` — the reader owns
 *  the decision. Theme colours are redefined as CSS variables ON the theme class so they cascade to
 *  every descendant; the per-article inline `:root{--accent…}` still feeds `var(--accent-dark)`.
 *  Set the background on the theme class itself (not `body`) so it works whether the reader puts the
 *  class on <body> or on a wrapper element above it. **/

.darktheme {
    --main-color:           var(--main-color-dark);
    --second-color:         var(--second-color-dark);
    --third-color:          var(--third-color-dark);
    --forth-color:          var(--forth-color-dark);
    --boxout-color:         var(--boxout-color-dark);
    --boxout-border-color:  var(--boxout-border-color-dark);
    --accent:               var(--accent-dark);
    background-color: #0f0f0f;
}
.darktheme a { color: #ffffff; }
/* DARK MODE gaps (Patrick, phone 2026-07-23): elements that DON'T pick up the variable remap —
   quiz questions + crossword clues set NO colour (they fell to UA black → invisible on the dark
   background), and the drop cap carries a per-article literal hex from the print read. Scoped to
   .darktheme so light mode is untouched. The crossword/sudoku GRIDS stay white-with-dark-ink
   (like a printed grid) — only the clue LISTS invert, so the rules are scoped to .cw-clues. */
.darktheme .article-quiz .quiz-q { color: var(--main-color); border-bottom-color: rgba(255,255,255,0.12); }
.darktheme .crossword-widget .cw-clues { color: var(--main-color); }
.darktheme .crossword-widget .cw-clues-list { border-color: rgba(255,255,255,0.15); }
.darktheme .crossword-widget .cw-clue { border-bottom-color: rgba(255,255,255,0.08); }
.darktheme .crossword-widget .cw-clue:hover { background: rgba(255,255,255,0.08); }
.darktheme .crossword-widget .cw-clue-active { background: #1e3a5f; }
/* the per-article print colour is a light-mode value; in dark, the cap takes the remapped text
   colour. The externalized per-article rules are `body.art-<aid> p.dropcap::first-letter
   {color:#hex!important}` — appended AFTER this block at compile — so this needs !important AND
   higher specificity: the repeated class (0,3,2) beats their (0,2,2) whether the reader stamps
   .darktheme on body or a wrapper. */
/* --dropcap-color-dark is the printed colour lifted for a dark ground at BUILD time
   (phase4._lift_for_dark): a near-black cap becomes the dark body colour, a deliberate hue is
   lightened, an already-light one is kept. Falls back to the remapped text colour for articles
   built before the vars existed. */
.darktheme.darktheme p.dropcap::first-letter {
    color: var(--dropcap-color-dark, var(--main-color)) !important; }
.darktheme .article-puzzle-title { color: var(--main-color); }

.sepiatheme {
    --main-color:    #5b4636;   /* warm dark brown on paper */
    --second-color:  #6b5645;
    --third-color:   #5b4636;
    --forth-color:   #8a7560;
    --boxout-color:  #efe3c8;   /* slightly deeper paper for boxout fills */
    background-color: #f4ecd8;  /* sepia paper */
}
.sepiatheme a { color: var(--accent); }


/** MAGAZINE CONTENTS / SECTION PAGES (EPUB reader nav — section_NN.xml) **/

.masthead { padding: 14px 0 10px; border-bottom: 2px solid var(--accent); margin-bottom: 12px; }
.masthead .sectionName { font-family: var(--font-family-main-display); font-weight: 800;
                         font-size: 1.6rem; text-transform: uppercase; letter-spacing: 0.02em;
                         color: var(--accent-on-light, var(--main-color)); }
.masthead .masthead-date { font-family: var(--font-family-main); font-size: 0.8rem;
                           color: var(--second-color); margin-top: 2px; }

.sectionArticleBlockOdd { display: flex; align-items: flex-start; gap: 12px; padding: 4px 0; }
.sectionArticleThumbBlock { flex: 0 0 100px; }
.sectionArticleImage { width: 100px; height: 100px; object-fit: cover; display: block;
                       border-radius: 3px; background: #eee; }
.sectionArticleTextBlock { flex: 1 1 auto; min-width: 0; }
.sectionArticleLink { text-decoration: none; }
.sectionArticleHeadline { font-family: var(--font-family-main-display); font-weight: 700;
                          font-size: 1.05rem; line-height: 1.15; color: var(--main-color); }
.sectionArticleByline { font-family: var(--font-family-main); font-size: 0.78rem;
                        color: var(--second-color); margin-top: 2px; }
.sectionArticleBody { font-family: var(--font-family-main); font-size: 0.82rem;
                      color: var(--third-color); margin-top: 4px; line-height: 1.35; }
.article-spacer { height: 1px; background: rgba(0,0,0,0.08); margin: 8px 0; }

/* ── Comfortable reading measure on wide screens (tablet / landscape / a misreporting wide webview) ──
   Cap the TEXT column and centre it (≈ BBC Sport's ~80% measure) so body copy doesn't sprawl
   edge-to-edge on a tablet/desktop. Media (hero / images / carousels / feature cards) is deliberately
   NOT listed here, so full-bleed stays full-bleed.
   DPR-GATED (max-resolution: 2dppx) — the in-app phone webview MISREPORTS a wide layout viewport
   (innerWidth≈996) at DPR=3 then zooms the page to fit the device, so an unconditional cap treated the
   PHONE as a wide screen and narrowed the reading column to ~680/996 → far too thin after the zoom.
   Gating to <2.5dppx applies the cap only on genuine desktop/tablet (DPR≤2.49; iPad is 2dppx, and a
   Windows monitor at ~225% scaling is 2.0–2.49) and leaves high-DPR phones (DPR≥2.5 — the misreporting
   webview AND real phone browsers) full-width. The upper bound is 2.49dppx (was 2dppx) to match the
   hero DPR boundary so a Windows-scaled desktop doesn't fall in the old gap. 730px matches the web
   reader's text-column width (the reader caps its article column at 1000px with ~730px of text). */
@media (max-resolution: 2.49dppx) {
    /* GROUP A — text + left-aligned blocks. Cap the WIDTH and left-align (margin-right:auto), but do
       NOT set margin-left: each element keeps its OWN left inset (body via padding, the standfirst pill
       + boxout via their margin-left), so they all line up at ~--side-pad. (An earlier --read-inset
       margin-left CLOBBERED those insets → standfirst/boxout/headshots jumped to the screen edge and
       misaligned with the body — Patrick, iPad 2026-07-24. Reverted.) The mini-grid + headline-headshot
       row keep their own inset too, so the grid cells / headline align with the body and the headshot
       sits at the column's right edge, not the screen. */
    /* GROUP A1 — PADDING-based blocks (no own margin-left): shift the whole column right by
       --read-inset, which is 0 in portrait (so they stay at --side-pad, unchanged) and a chunky
       left margin in landscape (~1/15, set below). Their left inset is padding, so adding a
       margin-left is safe — nothing to clobber. */
    .connect-mobile-edition .article-full-headline,
    .connect-mobile-edition .article-full-lead,
    .connect-mobile-edition .article-full-byline,
    .connect-mobile-edition .article-full-body,
    .connect-mobile-edition .article-subhead,
    .connect-mobile-edition .gallery-viewer,
    /* retrofit VENDOR text classes (pipeline never emits these — retrofit-only by construction):
       both carry the --side-pad padding like the body but were missing from this list, so on an
       iPad they stayed screen-relative while the capped column moved (Patrick, cyp 4-1 credit
       line + 6-1 "Kigali, Rwanda" at the left edge, 2026-08-06). */
    .connect-mobile-edition .article-byline,
    .connect-mobile-edition .article-subsubhead,
    /* article-full-subhead was the one article-full-* sibling missing from this list — cyp
       100-1 "How to maintain a tubeless tyre setup" (+ 62-1/61-1) sat at the screen's left
       edge on iPad landscape while its capped siblings moved right (Patrick, 2026-08-10). */
    .connect-mobile-edition .article-full-subhead,
    .connect-mobile-edition .headline-headshot-row {
        max-width: 640px;   /* comfortable ~65-char measure (was 730 — read too wide on iPad
                               landscape / large portrait; Patrick, iPad mini 2026-07-24). */
        margin-left: var(--read-inset, 0px);
        margin-right: auto;
    }
    /* GROUP A2 — MARGIN-based blocks (standfirst pill, boxout, mini-grid): their left inset is a
       margin-left, so --read-inset must be ADDED to it (calc), never replace it. An earlier version
       set margin-left:--read-inset flat, which in portrait (read-inset 0) CLOBBERED the pill/boxout
       insets → they jumped to the screen edge (Patrick, iPad 2026-07-24). calc(own + read-inset)
       keeps the portrait inset AND shifts with the column in landscape, aligning with the body. */
    .connect-mobile-edition .article-standfirst,
    .connect-mobile-edition .article-mini-grid {
        max-width: 640px;
        margin-left: calc(var(--side-pad) + var(--read-inset, 0px));
        margin-right: auto;
    }
    .connect-mobile-edition .article-boxout {
        max-width: 640px;
        margin-left: calc(1.5rem + var(--read-inset, 0px));
        margin-right: auto;
    }
    /* GROUP B — CENTRED blocks (centred header, quiz, puzzle, solo image, crest, pull-quote). Box
       left-aligned at the column's left (+ --read-inset), capped to 640; the element's own
       text-align:center / img margin:auto then centres its content OVER the body column (the 640
       box's centre == the body text's centre). Fixes 5-1 head block, 54-1 solo photo, 31-1 crests,
       90-1 crossword and 92-1 pull-quote centring on-screen / off-centre instead of over the article. */
    .connect-mobile-edition .article-header-centered,
    .connect-mobile-edition .article-quiz,
    .connect-mobile-edition .article-puzzle,
    .connect-mobile-edition .article-pull-quote,
    .connect-mobile-edition .article-pull-quote-credit,
    .connect-mobile-edition .article-work-citation,
    .connect-mobile-edition .article-in-image.solo,
    .connect-mobile-edition .article-in-image.page-centered {
        width: auto; max-width: 640px; margin-left: var(--read-inset, 0px); margin-right: auto; }
    /* the crest wrapper carries margin:0.9rem auto !important (auto-centres in the FULL parent →
       screen centre), so it needs !important here to re-home over the reading column. */
    .connect-mobile-edition .article-in-image.crest-img {
        width: auto !important; max-width: 640px;   /* !important overrides the base fit-content
            !important (else the 76px wrapper hugs the column-left instead of filling it, so the
            centred crest sat at the column's left, not its centre — Patrick, iPad 2026-07-24) */
        padding-left: var(--side-pad); padding-right: var(--side-pad);  /* match the body's
            content-box outer width (max-width 640 + side-pad each side) so the crest's centre
            lands on the body-text centre, not 20px left of it */
        margin-left: var(--read-inset, 0px) !important; margin-right: auto !important; }
    /* CENTRED puzzle + solo image: content-box (default) + side padding so their OUTER width
       matches the body (max-width 640 content + --side-pad each side = 680 outer) and their centre
       lands on the body-TEXT centre — not 20px left of it, as it was with no padding (640 outer).
       (90-1 crossword "against the left edge"; 50-1/90-1 solo photos off-centre.) */
    .connect-mobile-edition .article-puzzle,
    .connect-mobile-edition .article-in-image.solo,
    .connect-mobile-edition .article-in-image.page-centered {
        padding-left: var(--side-pad); padding-right: var(--side-pad); }
    /* the gallery is a LEFT-aligned carousel, so border-box keeps its content flush with the
       body-left at --side-pad rather than widening the strip. */
    .connect-mobile-edition .gallery-viewer {
        padding-left: var(--side-pad); padding-right: var(--side-pad); box-sizing: border-box; }
    /* SCROLLER strips bleed to the RIGHT screen edge (Patrick, iPad 2026-08-10: "it makes sense
       on the left, but ends up cutting off images on the right in the middle of the image") —
       the left edge stays on the reading column, the strip's window runs to the screen so a
       part-visible photo peeks naturally instead of being guillotined at the 640px cap. A strip
       narrower than the column just ends early — visually identical to before. (0,3,0) beats
       the GROUP A1 cap above. */
    .connect-mobile-edition .gallery-viewer.gallery-scroller {
        max-width: none; margin-right: 0; }
    /* NESTED cap-elements must NOT re-apply --read-inset: the CONTAINER (boxout / headline-headshot
       row / feature card / debate / roundup / section-toc / product-item / steps) already carries the
       inset, so a body/subhead/standfirst/headline/gallery INSIDE it would otherwise be indented TWICE
       (24-1 boxout text at 201 not 124; 86-1 headline at 153 not 97 — Patrick, iPad 2026-07-24). These
       3-class selectors out-specify GROUP A1/A2 (2 classes) so they win regardless of order. */
    .connect-mobile-edition .article-boxout .article-full-headline,
    .connect-mobile-edition .article-boxout .article-full-lead,
    .connect-mobile-edition .article-boxout .article-full-byline,
    .connect-mobile-edition .article-boxout .article-full-body,
    .connect-mobile-edition .article-boxout .article-subhead,
    .connect-mobile-edition .article-boxout .article-standfirst,
    .connect-mobile-edition .article-boxout .article-mini-grid,
    .connect-mobile-edition .article-boxout .gallery-viewer,
    .connect-mobile-edition .article-feature-card .article-full-body,
    .connect-mobile-edition .article-feature-card .article-subhead,
    .connect-mobile-edition .article-debate .article-full-body,
    .connect-mobile-edition .article-debate .article-subhead,
    .connect-mobile-edition .article-roundup .article-full-body,
    .connect-mobile-edition .article-roundup .article-subhead,
    .connect-mobile-edition .section-toc .article-full-body,
    .connect-mobile-edition .product-item .article-full-body,
    .connect-mobile-edition .product-item .article-subhead,
    .connect-mobile-edition .article-steps .article-full-body,
    .connect-mobile-edition .headline-headshot-row .article-full-headline {
        margin-left: 0; }
    /* the headline-headshot row itself is a flex container with no padding, so its (now un-shifted)
       headline sat at the column's LEFT edge, --side-pad left of the eyebrow/body. Pad it so the
       headline aligns with the body text (86-1). */
    .connect-mobile-edition .headline-headshot-row {
        padding-left: var(--side-pad); box-sizing: border-box; }
    /* INSIDE a scrim panel the panel padding already carries the reading inset (+ --read-inset), so
       the headline-headshot row must NOT add its own margin/padding on top — else the headline sits
       ~read-inset+side-pad RIGHT of the eyebrow (RM 42-1/66-1 scrim heroes with a header headshot).
       3-class selector beats group A1's margin-left + the row padding above. */
    .connect-mobile-edition .article-hero__text .headline-headshot-row {
        margin-left: 0; padding-left: 0; }
    /* head elements INSIDE a centred header must CENTRE within it — group A1 gave them
       margin-left:--read-inset (left-biased), pushing the headline ~read-inset RIGHT of the
       centred container so it read off-centre (Patrick WCM 5-1). Auto margins re-centre them. */
    .connect-mobile-edition .article-header-centered .article-full-headline,
    .connect-mobile-edition .article-header-centered .article-full-lead,
    .connect-mobile-edition .article-header-centered .article-full-byline,
    .connect-mobile-edition .article-header-centered .article-standfirst {
        margin-left: auto !important; margin-right: auto !important; }
    /* the .article-full-headline INSIDE the row carries its OWN --side-pad padding too, so with the
       row's padding the headline text sat DOUBLE-indented (~20px right of the eyebrow/body — the
       "Number 104" kicker; Patrick desktop 2026-07-24). The row provides the inset, so zero the
       headline's own side padding inside it. */
    .connect-mobile-edition .headline-headshot-row .article-full-headline {
        padding-left: 0; padding-right: 0; }
    /* SCRIM/HERO head text: cap to a hair over the reading measure so it doesn't sprawl full-width
       on desktop (Patrick "head block going full width on desktop, should be reading width or a bit
       wider", 2026-07-24). The panel/flex layout provides the left inset; this only limits LENGTH. */
    .connect-mobile-edition .article-hero__text .hero-headline,
    .connect-mobile-edition .article-hero__text .hero-lead,
    .connect-mobile-edition .article-hero__text .hero-byline,
    .connect-mobile-edition .article-hero__text .hero-eyebrow,
    .connect-mobile-edition .article-hero__text .headline-headshot-row {
        max-width: 680px; }
    /* a full-bleed hero image's caption sat at the bleed edge (--read-inset + --side-pad LEFT of the
       body text — Patrick 10-1). Shift it by --read-inset so it lines up with the body column (its
       own --side-pad padding does the rest; mobile read-inset 0 = unchanged). */
    .connect-mobile-edition .article-hero-stitched .articleImageCaption,
    .connect-mobile-edition .article-hero-landscape .articleImageCaption {
        margin-left: var(--read-inset, 0px); }
}
/* Chunky left reading margin (~1/15 of the viewport, BBC-Sport-like) on LANDSCAPE tablet/desktop
   only — portrait keeps the plain --side-pad inset (it already reads balanced at 640 in the narrower
   portrait width). --read-inset defaults to 0px (via the var fallback) everywhere else, so this is
   the ONLY place the column shifts. Set on the body so both cap groups + the Listen button pick it up. */
@media (orientation: landscape) and (max-resolution: 2.49dppx) {
    body.connect-mobile-edition { --read-inset: 6.5vw; }
}


/* ==== PUZZLE WIDGETS (external-CSS pilot): class-scoped copies of the per-widget
   inline <style> rules. Pilot articles ship NO inline widget styles and rely on these;
   non-pilot articles still carry their #uid inline copies, which win ties (same values,
   higher specificity) - so these are additive and safe everywhere. Per-instance grid
   columns (crossword cols, pathfinder cols) are set by script.js at init. ==== */
/* ==== krisskross-widget ==== */
.krisskross-widget{max-width:520px;margin:1em auto;font-family:var(--font-family-main,sans-serif);text-align:center;}.krisskross-widget .kk-grid{border-collapse:collapse;margin:0 auto 10px;}.krisskross-widget .kk-cell{border:1.5px solid #888;width:1.9em;height:1.9em;padding:0;background:#fff;position:relative;}.krisskross-widget .kk-len-n{position:absolute;top:-0.05em;left:1px;font-size:0.45em;color:#888;line-height:1;font-weight:700;}.krisskross-widget .kk-empty{width:1.9em;height:1.9em;}.krisskross-widget .kk-in{width:100%;height:100%;border:none;background:transparent;text-align:center;font-weight:700;text-transform:uppercase;outline:none;color:#1d4ed8;}.krisskross-widget .kk-in.kk-right{color:#16a34a;}.krisskross-widget .kk-in.kk-wrong{color:#dc2626;}
/* printed STARTER letters ("we've added some letters to get you started") — locked, and in the
   body colour so they read as part of the printed puzzle rather than as the reader's own entry
   (which is the blue). Plus the crossword-style current-word highlight. */
.krisskross-widget .kk-cell.kk-given{background:#f3f4f6;}
.krisskross-widget .kk-in.kk-given{color:#1a1a1a;cursor:default;}
.krisskross-widget .kk-cell.kk-hl{background:#dbeafe;}
.krisskross-widget .kk-cell.kk-hl.kk-given{background:#cfe0f5;}
.krisskross-widget .kk-cell.kk-cur{background:#fde68a;}.krisskross-widget .kk-group{font-size:0.85em;margin:3px 0;text-align:left;}.krisskross-widget .kk-len{font-weight:800;color:var(--accent-on-light,#1a1a1a);margin-right:6px;}.krisskross-widget .kk-w{margin-right:9px;letter-spacing:0.4px;}.krisskross-widget .kk-w.kk-done{text-decoration:line-through;color:#9ca3af;}.krisskross-widget .kk-check{margin-top:6px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.krisskross-widget .kk-msg{margin-left:8px;font-size:0.9em;font-weight:700;}.krisskross-widget .kk-note{font-size:0.85em;color:#6b7280;}

/* ==== bricktrick-widget ==== */
.bricktrick-widget{max-width:460px;margin:1em auto;font-family:var(--font-family-main,sans-serif);text-align:center;}.bricktrick-widget .bt-row{display:flex;align-items:center;justify-content:center;gap:6px;margin:3px 0;}.bricktrick-widget .bt-n{font-weight:800;color:var(--accent-on-light,#1a1a1a);min-width:1.2em;}.bricktrick-widget .bt-cells{display:inline-flex;gap:2px;}.bricktrick-widget .bt-c{width:1.7em;height:1.9em;text-transform:uppercase;font-weight:700;text-align:center;border:1.5px solid #b78a5a;border-radius:3px;background:#faf3ea;outline:none;color:#1d4ed8;padding:0;}.bricktrick-widget .bt-c.bt-ok{border-color:#16a34a;color:#16a34a;background:#f0fdf4;}.bricktrick-widget .bt-c.bt-bad{border-color:#dc2626;color:#dc2626;background:#fef2f2;}.bricktrick-widget .bt-clue{font-size:0.85em;text-align:left;margin:2px 0;}.bricktrick-widget .bt-clues{margin-top:8px;column-count:2;column-gap:18px;}.bricktrick-widget .bt-check{margin-top:8px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.bricktrick-widget .bt-msg{margin-left:8px;font-size:0.9em;font-weight:700;}

/* ==== missinglink-widget ==== */
.missinglink-widget{max-width:480px;margin:1em auto;font-family:var(--font-family-main,sans-serif);}.missinglink-widget .mlk-row{display:flex;align-items:center;justify-content:center;gap:9px;margin:5px 0;}.missinglink-widget .mlk-w{font-weight:800;min-width:7.5em;}.missinglink-widget .mlk-row .mlk-w:first-child{text-align:right;}.missinglink-widget .mlk-cells{display:inline-flex;gap:2px;}.missinglink-widget .mlk-c{width:1.7em;height:1.9em;text-transform:uppercase;font-weight:700;text-align:center;border:1.5px solid #bbb;border-radius:3px;outline:none;color:#1d4ed8;padding:0;}.missinglink-widget .mlk-c.mlk-shaded{background:#fbdce4;border-color:#e8a2b8;}.missinglink-widget .mlk-c.mlk-right{border-color:#16a34a;color:#16a34a;background:#f0fdf4;}.missinglink-widget .mlk-c.mlk-shaded.mlk-right{background:#d9f3e1;}.missinglink-widget .mlk-c.mlk-wrong{border-color:#dc2626;color:#dc2626;background:#fef2f2;}.missinglink-widget .mlk-check{margin-top:8px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.missinglink-widget .mlk-msg{margin-left:8px;font-size:0.9em;font-weight:700;}.missinglink-widget .mlk-foot{text-align:center;}

/* ==== crossword-widget ==== */
.crossword-widget{max-width:100%;margin:1em auto;}.crossword-widget .cw-grid{display:grid;gap:0;border:2px solid #333;width:100%;max-width:560px;}.crossword-widget .cw-cell{position:relative;background:#fff;border:1px solid #999;}.crossword-widget .cw-cell::before{content:'';display:block;padding-bottom:100%;}.crossword-widget .cw-black{background:#222;border-color:#222;}.crossword-widget .cw-word{background:#d4eaff;}.crossword-widget .cw-active{background:#6aaeee;}.crossword-widget .cw-num{position:absolute;top:1px;left:2px;font-size:0.55em;line-height:1;color:#333;pointer-events:none;z-index:1;}.crossword-widget .cw-input{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;border:none;background:transparent;text-align:center;text-transform:uppercase;font-size:1.3em;font-weight:700;padding:0;cursor:pointer;caret-color:transparent;outline:none;}.crossword-widget .cw-clues{display:flex;gap:1.5em;margin-top:1em;font-size:0.85em;}.crossword-widget .cw-clues-col{flex:1;min-width:0;}.crossword-widget .cw-clues-heading{font-size:0.95em;font-weight:700;margin:0 0 0.4em;text-transform:uppercase;letter-spacing:0.05em;}.crossword-widget .cw-clues-list{list-style:none;margin:0;padding:0;max-height:320px;overflow-y:auto;border:1px solid #ddd;border-radius:4px;}.crossword-widget .cw-clue{padding:0.3em 0.5em;cursor:pointer;border-bottom:1px solid #eee;line-height:1.3;}.crossword-widget .cw-clue:last-child{border-bottom:none;}.crossword-widget .cw-clue:hover{background:#f5f5f5;}.crossword-widget .cw-clue-active{background:#d4eaff;}.crossword-widget .cw-cn{font-weight:700;min-width:1.8em;display:inline-block;}

/* ==== sudoku-widget ==== */
.sudoku-widget{max-width:380px;margin:1em auto;}.sudoku-widget .su-grid{display:grid;grid-template-columns:repeat(9,2.2em);width:fit-content;margin:0 auto;border:3px solid #333;}.sudoku-widget .su-cell{position:relative;aspect-ratio:1/1;background:#fff;border:1px solid #bbb;display:flex;align-items:center;justify-content:center;font-size:1.1em;font-weight:700;}.sudoku-widget .su-box-tl{border-top:3px solid #333;border-left:3px solid #333;}.sudoku-widget .su-box-t{border-top:3px solid #333;}.sudoku-widget .su-box-l{border-left:3px solid #333;}.sudoku-widget .su-given{color:#222;background:#f0f0f0;}.sudoku-widget .su-cell.su-active{background:#d4eaff;}.sudoku-widget .su-cell.su-related{background:#eef5ff;}.sudoku-widget .su-input{width:100%;height:100%;border:none;background:transparent;text-align:center;font-size:1em;font-weight:700;padding:0;caret-color:transparent;outline:none;}.sudoku-widget .su-input.su-right{color:#16a34a;}.sudoku-widget .su-input.su-wrong{color:#dc2626;}.sudoku-widget .su-check{margin-top:6px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.sudoku-widget .su-msg{margin-left:8px;font-size:0.9em;font-weight:700;}

/* ==== codeword-widget ==== */
.codeword-widget{max-width:560px;margin:1em auto;font-family:var(--font-family-main,sans-serif);text-align:center;}.codeword-widget table{border-collapse:collapse;margin:0 auto;}.codeword-widget .cwd-cell{border:1px solid #777;width:1.85em;height:1.85em;position:relative;padding:0;background:#fff;}.codeword-widget .cwd-n{position:absolute;top:0;left:1px;font-size:0.5em;color:#555;line-height:1;}.codeword-widget .cwd-black-given{background:#1a1a1a;}.codeword-widget .cwd-cell.cwd-black{background:#1a1a1a;}.codeword-widget .cwd-cell.cwd-black .cwd-in{color:transparent;}.codeword-widget .cwd-cell.cwd-black .cwd-n{color:#888;}.codeword-widget .cwd-gl{font-weight:800;font-size:0.95em;display:block;margin-top:0.35em;}.codeword-widget .cwd-in{width:100%;height:100%;border:none;background:transparent;text-align:center;font-weight:700;text-transform:uppercase;outline:none;color:#1d4ed8;padding-top:0.4em;}.codeword-widget .cwd-in.cwd-right{color:#16a34a;}.codeword-widget .cwd-in.cwd-wrong{color:#dc2626;}.codeword-widget .cwd-hint{font-size:0.75em;color:#6b7280;margin:6px 0;}.codeword-widget .cwd-track{display:flex;flex-wrap:wrap;gap:3px;justify-content:center;margin:8px 0;}.codeword-widget .cwd-tr{display:inline-flex;flex-direction:column;align-items:center;border:1px solid #ccc;border-radius:3px;min-width:1.5em;padding:1px 2px;}.codeword-widget .cwd-tr b{font-size:0.55em;color:#666;font-weight:700;}.codeword-widget .cwd-tr-in{width:1.3em;height:1.1em;border:none;background:transparent;text-align:center;font-weight:800;font-size:0.8em;text-transform:uppercase;outline:none;color:#1d4ed8;padding:0;}.codeword-widget .cwd-tr.cwd-got{border-color:#16a34a;background:#f0fdf4;}.codeword-widget .cwd-check{margin-top:6px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.codeword-widget .cwd-msg{margin-left:8px;font-size:0.9em;font-weight:700;}

/* ==== setsquare-widget ==== */
.setsquare-widget table{border-collapse:collapse;margin:0 auto;}.setsquare-widget td{text-align:center;font-weight:700;padding:2px;}.setsquare-widget .ss-cell{border:1.5px solid #999;width:2.3em;height:2.3em;font-size:1.05em;background:#fff;}.setsquare-widget .ss-given{background:#f2f2f2;}.setsquare-widget .ss-op{width:1.6em;color:#444;}.setsquare-widget .ss-t{color:var(--accent-on-light,#1a1a1a);}.setsquare-widget .ss-in{width:100%;height:100%;border:none;background:transparent;text-align:center;font-size:1em;font-weight:700;outline:none;color:#1d4ed8;}.setsquare-widget .ss-in.ss-right{color:#16a34a;}.setsquare-widget .ss-in.ss-wrong{color:#dc2626;}.setsquare-widget .ss-check{margin-top:8px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.setsquare-widget .ss-msg{margin-left:8px;font-size:0.9em;font-weight:700;}

/* ==== pathfinder-widget ==== */
.pathfinder-widget{max-width:420px;margin:1em auto;font-family:var(--font-family-main,sans-serif);text-align:center;}.pathfinder-widget .pf-wrap{position:relative;display:inline-block;}.pathfinder-widget .pf-grid{display:grid;width:fit-content;margin:0 auto;border:2px solid #333;background:#fff;position:relative;font-size:14px;}.pathfinder-widget .pf-c{width:2em;height:2em;line-height:2em;font-weight:700;border:1px solid #ddd;cursor:pointer;user-select:none;position:relative;z-index:2;color:#1a1a1a;}.pathfinder-widget .pf-c.pf-on{background:rgba(37,99,235,0.18);color:#1d4ed8;}.pathfinder-widget .pf-c.pf-start{background:#fbcfe8;}.pathfinder-widget .pf-c.pf-start.pf-on{background:#f9a8d4;}.pathfinder-widget .pf-c.pf-no{background:#fecaca !important;transition:background 0.1s;}.pathfinder-widget svg.pf-trail{position:absolute;inset:0;z-index:1;pointer-events:none;}.pathfinder-widget .pf-words{list-style:none;padding:0;margin:10px 0 0;column-count:2;column-gap:18px;}.pathfinder-widget .pf-w{font-size:0.85em;text-align:left;margin:2px 0;}.pathfinder-widget .pf-w.pf-done{text-decoration:line-through;color:#16a34a;}.pathfinder-widget .pf-w.pf-done::after{content:' \2713';}.pathfinder-widget .pf-reset{margin-top:8px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.pathfinder-widget .pf-msg{margin-left:8px;font-size:0.9em;font-weight:700;}
/* ==== wordsearch-widget ==== (class-scoped copy of phase4's #ws-uid rules — the per-instance
   grid-template-columns stays on the .s-<hash> class. Was MISSING, so once the inline <style> is
   stripped the grid had no display:grid and collapsed to a single column — "a line of letters down
   the page", NGK 258 40-1.) */
.wordsearch-widget{max-width:480px;margin:1em auto;}.wordsearch-widget .ws-grid{display:grid;border:2px solid #333;background:#fff;user-select:none;}.wordsearch-widget .ws-cell{aspect-ratio:1/1;display:flex;align-items:center;justify-content:center;min-width:0;overflow:hidden;border:1px solid #ddd;font-weight:700;text-transform:uppercase;font-size:0.95em;font-family:var(--font-family-main,sans-serif);color:#222;cursor:pointer;}.wordsearch-widget .ws-cell.ws-start{background:#fde68a;}.wordsearch-widget .ws-gridwrap{position:relative;}.wordsearch-widget .ws-ov{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;}.wordsearch-widget .ws-words{columns:3;-webkit-columns:3;list-style:none;padding:0;margin:0.8em 0 0;font-weight:600;text-transform:uppercase;font-size:0.9em;}.wordsearch-widget .ws-words li{padding:2px 0;break-inside:avoid;}.wordsearch-widget .ws-words li.ws-done .ws-w{text-decoration:line-through;color:#16a34a;}.wordsearch-widget .ws-words li.ws-done::after{content:' \2713';color:#16a34a;font-weight:700;}.wordsearch-widget .ws-prog{font-size:0.8em;color:#9ca3af;margin-left:4px;font-weight:400;}.wordsearch-widget .ws-msg{margin-top:6px;font-size:0.9em;font-weight:700;color:#16a34a;}
/* ==== missingletters-widget ==== (class-scoped copy of phase4's #ml-uid rules — was MISSING, so the
   "first letters of pictured objects" layout lost its flex rows / cell boxes, NGK 258 40-1.) */
.missingletters-widget{max-width:480px;margin:1em auto;font-family:var(--font-family-main,sans-serif);}.missingletters-widget .ml-theme{font-size:0.85em;color:#6b7280;margin-bottom:6px;}.missingletters-widget .ml-item{display:flex;align-items:center;gap:4px;margin:5px 0;flex-wrap:wrap;}.missingletters-widget .ml-n{min-width:1.4em;font-weight:700;color:#9ca3af;font-size:0.9em;}.missingletters-widget .ml-fixed,.missingletters-widget .ml-in{width:1.5em;height:1.8em;text-align:center;font-weight:700;text-transform:uppercase;font-size:1em;border:1px solid #ccc;border-radius:3px;}.missingletters-widget .ml-fixed{display:inline-flex;align-items:center;justify-content:center;background:#f0f0f0;color:#222;}.missingletters-widget .ml-in{background:#fff;color:#1d4ed8;padding:0;outline:none;}.missingletters-widget .ml-in.ml-right{border-color:#16a34a;color:#16a34a;background:#f0fdf4;}.missingletters-widget .ml-in.ml-wrong{border-color:#dc2626;color:#dc2626;background:#fef2f2;}.missingletters-widget .ml-check{margin-top:8px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.missingletters-widget .ml-msg{margin-left:8px;font-size:0.9em;font-weight:700;}
/* ==== jumble-widget ==== (class-scoped copy of phase4's #jb-uid rules) */
.jumble-widget{max-width:480px;margin:1em auto;font-family:var(--font-family-main,sans-serif);}.jumble-widget .jb-theme{font-size:0.85em;color:#6b7280;margin-bottom:6px;}.jumble-widget .jb-item{display:flex;align-items:center;gap:5px;margin:6px 0;flex-wrap:wrap;}.jumble-widget .jb-n{min-width:1.4em;font-weight:700;color:#9ca3af;font-size:0.9em;}.jumble-widget .jb-tiles{display:inline-flex;gap:2px;}.jumble-widget .jb-tile{width:1.5em;height:1.8em;display:inline-flex;align-items:center;justify-content:center;background:#f0f0f0;border:1px solid #ccc;border-radius:3px;font-weight:700;text-transform:uppercase;}.jumble-widget .jb-in{min-width:7em;height:1.9em;text-transform:uppercase;font-weight:700;text-align:center;border:1px solid #ccc;border-radius:3px;outline:none;color:#1d4ed8;}.jumble-widget .jb-in.jb-right{border-color:#16a34a;color:#16a34a;background:#f0fdf4;}.jumble-widget .jb-in.jb-right::after{content:' \2713';}.jumble-widget .jb-in.jb-wrong{border-color:#dc2626;color:#dc2626;background:#fef2f2;}.jumble-widget .jb-check{margin-top:8px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.jumble-widget .jb-msg{margin-left:8px;font-size:0.9em;font-weight:700;}
/* ==== what-in-the-world / image-anagram (.wiw-carousel) ==== (class-scoped copy of #wiw-uid) */
.wiw-carousel{max-width:440px;margin:1em auto;font-family:var(--font-family-main,sans-serif);text-align:center;}.wiw-carousel .wiw-theme{font-size:0.9em;color:#6b7280;margin-bottom:6px;}.wiw-carousel .wiw-stage{position:relative;}.wiw-carousel .wiw-slide{display:none;}.wiw-carousel .wiw-slide.on{display:block;}.wiw-carousel .wiw-slide img{max-width:100%;max-height:300px;width:auto;border-radius:8px;}.wiw-carousel .wiw-scramble{display:flex;justify-content:center;gap:3px;flex-wrap:wrap;margin:10px 0 8px;}.wiw-carousel .wiw-tile{width:1.5em;height:1.8em;display:inline-flex;align-items:center;justify-content:center;background:#f0f0f0;border:1px solid #ccc;border-radius:3px;font-weight:700;text-transform:uppercase;}.wiw-carousel .wiw-in{min-width:9em;height:2em;text-transform:uppercase;font-weight:700;text-align:center;border:1px solid #ccc;border-radius:4px;outline:none;color:#1a1a1a;caret-color:#1a1a1a;}.wiw-carousel .wiw-in.ok{border-color:#16a34a;color:#16a34a;background:#f0fdf4;}.wiw-carousel .wiw-in.no{border-color:#dc2626;color:#dc2626;background:#fef2f2;}.wiw-carousel .wiw-nav{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:6px;}.wiw-carousel .wiw-arrow{background:var(--accent,#1e3a5f);color:var(--standfirst-fg,#fff);border:none;border-radius:50%;width:2em;height:2em;font-size:1.1em;cursor:pointer;line-height:1;}.wiw-carousel .wiw-dots{display:flex;gap:5px;}.wiw-carousel .wiw-dot{width:9px;height:9px;border-radius:50%;border:none;background:#ccc;cursor:pointer;padding:0;}.wiw-carousel .wiw-dot.on{background:var(--accent,#1e3a5f);}.wiw-carousel .wiw-check{margin-top:10px;font-size:0.9em;padding:4px 14px;background:var(--accent,#1e3a5f);color:var(--standfirst-fg,#fff);border:none;border-radius:4px;cursor:pointer;}.wiw-carousel .wiw-msg{margin-left:8px;font-size:0.9em;font-weight:700;}
/* ==== spellitout-widget ==== (class-scoped copy of #so-uid) */
.spellitout-widget{max-width:480px;margin:1em auto;font-family:var(--font-family-main,sans-serif);}.spellitout-widget .so-q{font-size:0.9em;margin-bottom:6px;}.spellitout-widget .so-q b{letter-spacing:1px;}.spellitout-widget .so-opt{display:block;width:100%;text-align:left;margin:4px 0;padding:6px 10px;background:#fff;border:1px solid #ccc;border-radius:5px;cursor:pointer;font-weight:700;text-transform:uppercase;letter-spacing:1px;}.spellitout-widget .so-opt:hover{border-color:#60a5fa;}.spellitout-widget .so-n{color:#9ca3af;margin-right:6px;}.spellitout-widget .so-opt.so-right{border-color:#16a34a;background:#f0fdf4;color:#16a34a;}.spellitout-widget .so-opt.so-right::after{content:' \2713';float:right;}.spellitout-widget .so-opt.so-wrong{border-color:#dc2626;background:#fef2f2;color:#dc2626;}
/* ==== missingmaths-widget ==== (class-scoped copy of #mm-uid) */
.missingmaths-widget{max-width:420px;margin:1em auto;font-family:var(--font-family-main,sans-serif);}.missingmaths-widget .mm-item{display:flex;align-items:center;gap:6px;margin:6px 0;font-size:1.05em;}.missingmaths-widget .mm-n{min-width:1.4em;font-weight:700;color:#9ca3af;}.missingmaths-widget .mm-expr{display:inline-flex;align-items:center;gap:5px;font-weight:700;}.missingmaths-widget .mm-in{width:2.4em;height:1.8em;text-align:center;font-weight:700;border:1px solid #ccc;border-radius:3px;outline:none;color:#1d4ed8;}.missingmaths-widget .mm-in.mm-right{border-color:#16a34a;color:#16a34a;background:#f0fdf4;}.missingmaths-widget .mm-in.mm-wrong{border-color:#dc2626;color:#dc2626;background:#fef2f2;}.missingmaths-widget .mm-check{margin-top:8px;font-size:0.85em;padding:3px 10px;background:#1e3a5f;color:#93c5fd;border:1px solid #2a4a7f;border-radius:4px;cursor:pointer;}.missingmaths-widget .mm-msg{margin-left:8px;font-size:0.9em;font-weight:700;}

/* "Solve the sums" (NGK 258): the OPERATOR is the blank, so the reader taps one of + - x div.
   The correct symbol is COMPUTED (only accepted when exactly one satisfies the sum). */
.missingmaths-widget .mm-ops { display: inline-flex; gap: 3px; margin: 0 4px; }
.missingmaths-widget .mm-op {
  width: 1.7em; height: 1.7em; padding: 0; cursor: pointer; font: inherit; font-weight: 700;
  border: 1px solid #ccc; border-radius: 4px; background: #fff; color: #1d4ed8;
}
.missingmaths-widget .mm-ops.locked .mm-op { cursor: default; }
.missingmaths-widget .mm-op.right { border-color: #16a34a; background: #f0fdf4; color: #16a34a; }
.missingmaths-widget .mm-op.shown { border-color: #16a34a; border-style: dashed; color: #16a34a; }
.missingmaths-widget .mm-op.wrong { border-color: #dc2626; background: #fef2f2; color: #dc2626; }

/* ==== crossword: HIGHLIGHTED cells spell a hidden word ==== (Aquila 24-1's 9 blue squares =
   "TROY FALLS"). Tinted with the title accent so the reader can see which letters to collect; the
   reveal only appears when the answer's letter count matches the highlighted count. */
.crossword-widget .cw-cell.cw-hl {
  background: color-mix(in srgb, var(--accent, #9dc789) 26%, #fff);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #9dc789) 55%, #fff);
}
.crossword-widget .cw-hidden { margin: 10px 0 0; font-size: 0.9em; }
/* The same WHITE pill as the wordmorph's "Show a solution" and the CYOA card's Close — this was the
   last reveal still wearing the old grey box (Patrick, on Cassandra's "Reveal the hidden word"). */
.crossword-widget .cw-hidden summary {
  cursor: pointer; display: inline-block; list-style: none;
  font-family: inherit; font-weight: 800; font-size: .95em; padding: 8px 18px;
  border: 1.5px solid #cfcfc8; border-radius: 24px; background: #fff; color: #444;
  box-shadow: 0 3px 0 rgba(0,0,0,.12);
  transition: transform .06s, box-shadow .06s;
}
.crossword-widget .cw-hidden summary::-webkit-details-marker { display: none; }
.crossword-widget .cw-hidden summary:active {
  transform: translateY(2px); box-shadow: 0 1px 0 rgba(0,0,0,.12);
}
.crossword-widget .cw-hidden-ans {
  display: inline-block; margin-top: 6px; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--accent-on-light, inherit);
}
.crossword-widget .cw-hidden-note { margin: 10px 0 0; font-size: 0.85em; color: #6b7280; }

/* ==== branching-widget ==== (choose-your-own-adventure: one box at a time, tap a choice. The graph
   is validated before it ships, so no choice can lead nowhere. Aquila 36-1.) */
.branching-widget { max-width: 34em; margin: 1em auto; font-family: var(--font-family-main, sans-serif); text-align: left; }
.branching-widget .br-node { display: none; }
.branching-widget .br-node.on { display: block; }
.branching-widget .br-label {
  margin: 0 0 0.4em; font-family: var(--font-family-main-display, inherit); font-weight: 700;
  font-size: 1.25em; color: var(--accent-on-light, inherit);
}
.branching-widget .br-text { margin: 0 0 0.9em; line-height: 1.5; }
.branching-widget .br-choices { display: flex; flex-direction: column; gap: 6px; }
.branching-widget .br-choice {
  width: 100%; text-align: left; cursor: pointer; padding: 9px 12px; font: inherit; font-weight: 700;
  border: 1px solid #ccc; border-radius: 6px; background: #fff; color: inherit;
}
.branching-widget .br-choice:hover { border-color: var(--accent, #888); }
.branching-widget .br-end { margin: 0 0 0.6em; font-weight: 700; font-size: 1.1em; }
.branching-widget .br-node[data-outcome="win"] .br-end { color: #16a34a; }
.branching-widget .br-node[data-outcome="lose"] .br-end { color: #dc2626; }
.branching-widget .br-restart {
  margin-top: 12px; cursor: pointer; font: inherit; font-size: 0.85em; padding: 4px 12px;
  border: 1px solid #ccc; border-radius: 4px; background: #f0f0f0;
}

/* ==== wordmorph-widget ==== (word ladder — Aquila 24-1 HERA→FIRE. Print sets the chains as the
   COLUMNS of a table, so they lay out as columns here too; they wrap on a narrow phone. Validation
   is structural (one letter per rung) and lives in script.js — there is no answer key. NOTE: new
   puzzle widgets ship their CSS here and their JS in carousel.js ONLY — phase4 emits markup, since
   _EXTERNAL_ASSETS_ALL strips any inline <style>/<script> from every article. ==== */
.wordmorph-widget { margin: 1em auto; font-family: var(--font-family-main, sans-serif); }
.wordmorph-widget .wm-ladders { display: flex; flex-wrap: wrap; gap: 1.4em; justify-content: center; }
.wordmorph-widget .wm-ladder { display: flex; flex-direction: column; gap: 4px; align-items: center; }
/* one cell PER LETTER, mirroring the printed dashes */
.wordmorph-widget .wm-rung { display: flex; gap: 3px; justify-content: center; }
.wordmorph-widget .wm-ch,
.wordmorph-widget .wm-in {
  width: 1.5em; height: 1.9em; box-sizing: border-box; text-align: center;
  font-weight: 700; text-transform: uppercase;
  border: 1px solid #ccc; border-radius: 3px;
}
.wordmorph-widget .wm-ch { display: flex; align-items: center; justify-content: center; background: #f0f0f0; }
/* font-size: inherit is the fix, not a bigger width. Both cells are sized in `em`, and an <input>
   does NOT inherit font-size — so the blank cells were an em of the browser default (~13px)
   against the filled cells' 16px, i.e. visibly smaller (Patrick). */
.wordmorph-widget .wm-in { outline: none; color: #1d4ed8; background: transparent; padding: 0;
                           font-size: inherit; line-height: 1; }
.wordmorph-widget .wm-in.wm-right { border-color: #16a34a; color: #16a34a; background: #f0fdf4; }
.wordmorph-widget .wm-in.wm-wrong { border-color: #dc2626; color: #dc2626; background: #fef2f2; }
.wordmorph-widget .wm-check,
.riddle-widget .rd-check {
  margin-top: 10px; font-size: 0.85em; padding: 3px 10px; background: #1e3a5f;
  color: #93c5fd; border: 1px solid #2a4a7f; border-radius: 4px; cursor: pointer;
}
.wordmorph-widget .wm-msg,
.riddle-widget .rd-msg { margin-left: 8px; font-size: 0.9em; font-weight: 700; }
/* worked chain from the printed solutions page — only ever emitted when every step validates */
.wordmorph-widget .wm-soln { margin-top: 10px; font-size: 0.9em; }
/* A WHITE pill with the same rounding as our other buttons, rather than the old grey box (Patrick).
   Matches the CYOA ending card's Close and the puzzle Check pill: same 24px radius, same hard bottom
   edge that presses in. */
.wordmorph-widget .wm-soln summary {
  cursor: pointer; display: inline-block; list-style: none;
  font-family: inherit; font-weight: 800; font-size: .95em; padding: 8px 18px;
  border: 1.5px solid #cfcfc8; border-radius: 24px; background: #fff; color: #444;
  box-shadow: 0 3px 0 rgba(0,0,0,.12);
  transition: transform .06s, box-shadow .06s;
}
.wordmorph-widget .wm-soln summary::-webkit-details-marker { display: none; }
.wordmorph-widget .wm-soln summary:active {
  transform: translateY(2px); box-shadow: 0 1px 0 rgba(0,0,0,.12);
}
.wordmorph-widget .wm-soln-chain {
  margin-top: 6px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
}

/* ==== quiz: tappable multiple-choice options ==== (Aquila's quizzes print their options inside the
   question and p45 says which is right, so tapping gives instant feedback plus a live score.) */
.article-quiz .quiz-opts { display: flex; flex-direction: column; gap: 5px; margin: 8px 0 4px; }
.article-quiz .quiz-opt {
  display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left; cursor: pointer;
  padding: 7px 10px; border: 1px solid #ccc; border-radius: 5px; background: #fff;
  font: inherit; color: inherit;
}
.article-quiz .quiz-opt:hover:not(.picked) { border-color: var(--accent, #888); }
.article-quiz .quiz-opt-tag { font-weight: 700; color: var(--accent-on-light, #888); }
.article-quiz .quiz-opt.right,
.article-quiz .quiz-opt.shown { border-color: #16a34a; background: #f0fdf4; color: #14532d; }
.article-quiz .quiz-opt.shown { border-style: dashed; }   /* revealed, not the reader's pick */
.article-quiz .quiz-opt.wrong { border-color: #dc2626; background: #fef2f2; color: #7f1d1d; }
.article-quiz .quiz-score { margin: 10px 0 0; font-weight: 700; }
/* An option set we cannot judge (the printed answer names no lettered option — "All", "True", or a
   mis-captured one). It still lays out as options for a consistent quiz, and a tap records the
   reader's pick neutrally; the Reveal beneath gives the answer. */
.article-quiz .quiz-opts-unjudged .quiz-opt.picked {
  border-color: var(--accent, #888); background: #f6f6f6; font-weight: 700;
}

/* imagechoice: the options are NUMBERS printed in the picture above, so they lay out as a compact
   row of tappable tiles rather than a stacked list. Everything else (tap → green/red, the reveal,
   the score) is inherited from .article-quiz — it is the same multiple choice. */
.imagechoice-widget { margin: 0.5em auto 1em; }
.imagechoice-widget .ic-opts { flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 8px; }
.imagechoice-widget .ic-opt {
  /* the tag is baseline-aligned in a normal option cell, which left the number sitting high in a
     circle (Patrick). Centre it, and leave room under it for the tick/cross. */
  width: 2.8em; height: 2.8em; padding: 0; border-radius: 50%;
  flex-direction: column; align-items: center; justify-content: center; gap: 0;
  font-weight: 700; font-size: 1.05em; line-height: 1.05;
}
/* the verdict goes INSIDE the circle, under the number */
.imagechoice-widget .ic-opt.right::after,
.imagechoice-widget .ic-opt.shown::after { content: "\2713"; font-size: 0.75em; line-height: 1; }
.imagechoice-widget .ic-opt.wrong::after { content: "\2717"; font-size: 0.75em; line-height: 1; }
.imagechoice-widget .ic-opt .quiz-opt-tag { color: inherit; }

/* ==== wordmatch-widget ==== (two tappable columns — Aquila 24-1 "That's foe business", hero → foe;
   NGK 258's synonym Word match is the same shape) */
.wordmatch-widget { margin: 1em auto; font-family: var(--font-family-main, sans-serif); }
.wordmatch-widget .wmt-cols { display: flex; gap: 12px; align-items: flex-start; }
.wordmatch-widget .wmt-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wordmatch-widget .wmt-cell {
  display: flex; align-items: baseline; gap: 7px; width: 100%; text-align: left; cursor: pointer;
  padding: 8px 10px; border: 1px solid #ccc; border-radius: 5px; background: #fff;
  font: inherit; color: inherit;
}
.wordmatch-widget .wmt-tag { font-weight: 700; color: var(--accent-on-light, #888); }
.wordmatch-widget .wmt-cell.sel { border-color: var(--accent, #888); box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06); }
.wordmatch-widget .wmt-cell.done { border-color: #16a34a; background: #f0fdf4; color: #14532d; cursor: default; }
.wordmatch-widget .wmt-cell.bad { border-color: #dc2626; background: #fef2f2; }
.wordmatch-widget .wmt-msg { display: inline-block; margin-top: 10px; font-weight: 700; }

/* ==== riddle-widget ==== ("What am I?" verse + a guess box — Aquila 32-1 CACAO/JADE, 40-1's
   Taskmaster riddle). The tick exists only when the PAGE stated an answer; otherwise the input is
   fillable-no-tick and no Check button is emitted, so we never call a reader's guess wrong. ==== */
.riddle-widget { max-width: 34em; margin: 1em auto; font-family: var(--font-family-main, sans-serif); text-align: left; }
.riddle-widget .rd-item { display: flex; gap: 8px; margin: 0 0 1em; }
.riddle-widget .rd-n { font-weight: 700; color: var(--accent-on-light, #9ca3af); min-width: 1.4em; }
.riddle-widget .rd-body { flex: 1; }
.riddle-widget .rd-verse { font-style: italic; line-height: 1.5; margin-bottom: 6px; }
.riddle-widget .rd-in {
  width: 100%; max-width: 16em; height: 2em; box-sizing: border-box; padding: 0 0.5em;
  font-weight: 700; border: 1px solid #ccc; border-radius: 3px; outline: none; color: #1d4ed8;
}
.riddle-widget .rd-in.rd-right { border-color: #16a34a; color: #16a34a; background: #f0fdf4; }
.riddle-widget .rd-in.rd-wrong { border-color: #dc2626; color: #dc2626; background: #fef2f2; }

/* ==== CORRECTNESS IS NEVER COLOUR-ALONE ==== (Patrick, 2026-07-28, reviewing the Greek to Me
   widget: "we should also add a tick or cross as well so not colour dependant for colour blind
   people"). Red-green colour blindness affects roughly 1 boy in 12 — the core audience of the kids'
   titles these widgets are built for — so a green tint and a red tint were, for those readers, the
   same tint. A SURVEY of every widget found only `imagechoice` and `wordsearch` carried a glyph; the
   other ~20 families signalled right/wrong by colour alone. This block is that gap closed in one
   place, following the glyph the imagechoice widget already established (U+2713 / U+2717).

   It is deliberately CSS-ONLY, so no article XML changes and every title picks it up on a rebuild.

   TWO mechanisms, because half of these elements are <input>: a REPLACED element has no content box,
   so `::after` silently does nothing on one — the trap that makes "just add ::after everywhere" fail
   on exactly the crossword/sudoku/ladder cells. Buttons, spans and list items take the pseudo-element;
   inputs take an inline-SVG background glyph in the corner instead, which does work on them.
   ⚠️ The input rules MUST stay after each widget's own `background:` SHORTHAND (which resets
   background-image to none) — that is why this whole block sits below every widget above. */

/* -- tappable options / cells / list items: a pseudo-element glyph -- */
.article-quiz .quiz-opt.right::after,
.article-quiz .quiz-opt.shown::after,
.wordmatch-widget .wmt-cell.done::after,
.missingmaths-widget .mm-op.right::after,
.missingmaths-widget .mm-op.shown::after,
.spellitout-widget .so-opt.so-right::after,
.pathfinder-widget .pf-w.pf-done::after { content: "\2713"; font-weight: 700; margin-left: auto; }
.article-quiz .quiz-opt.wrong::after,
.wordmatch-widget .wmt-cell.bad::after,
.missingmaths-widget .mm-op.wrong::after,
.spellitout-widget .so-opt.so-wrong::after { content: "\2717"; font-weight: 700; margin-left: auto; }
/* `margin-left:auto` pushes the verdict to the trailing edge of the flex row it lands in. The
   imagechoice circle stacks its glyph UNDER the number instead and sets its own layout, so it is not
   listed here — its existing rules already win. */
.pathfinder-widget .pf-w.pf-done::after,
.spellitout-widget .so-opt.so-right::after,
.spellitout-widget .so-opt.so-wrong::after { margin-left: 6px; }
/* The imagechoice CIRCLE stacks its glyph UNDER the number (column flex), where the shared
   `margin-left: auto` above stops meaning "trailing edge of the row" and starts meaning "right edge
   of the circle" — Patrick's off-centre tick on Stone me!. Recentre it. */
.imagechoice-widget .ic-opt.right::after,
.imagechoice-widget .ic-opt.shown::after,
.imagechoice-widget .ic-opt.wrong::after { margin-left: 0; }

/* -- WIDE answer <input>s: an inline-SVG glyph, because ::after cannot render on a replaced element.
   Only the roomy boxes get a glyph. MEASURED: a wordmorph/crossword letter cell is 24x30px with a
   16px letter in it, so any glyph big enough to read would sit ON the letter — those take the
   space-free treatment below instead. -- */
.jumble-widget .jb-in.jb-right,
.riddle-widget .rd-in.rd-right,
.wiw-carousel .wiw-in.ok {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.5 9l4 4 7-9' fill='none' stroke='%2316a34a' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 6px center; background-size: 13px 13px;
}
.jumble-widget .jb-in.jb-wrong,
.riddle-widget .rd-in.rd-wrong,
.wiw-carousel .wiw-in.no {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 3.5l9 9M12.5 3.5l-9 9' fill='none' stroke='%23dc2626' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 6px center; background-size: 13px 13px;
}

/* -- SMALL single-letter grid cells: a SPACE-FREE cue, since a glyph does not fit (above). A wrong
   letter is struck through and a right one is bolded, so the two are told apart by the letter's own
   rendering rather than by hue — the proof-reader's convention, and it costs no room at 24px. -- */
.krisskross-widget .kk-in.kk-wrong,
.sudoku-widget .su-input.su-wrong,
.codeword-widget .cwd-in.cwd-wrong,
.setsquare-widget .ss-in.ss-wrong,
.missingletters-widget .ml-in.ml-wrong,
.missingmaths-widget .mm-in.mm-wrong,
.wordmorph-widget .wm-in.wm-wrong,
.missinglink-widget .mlk-c.mlk-wrong,
.bricktrick-widget .bt-c.bt-bad { text-decoration: line-through; text-decoration-thickness: 2px; }
.krisskross-widget .kk-in.kk-right,
.sudoku-widget .su-input.su-right,
.codeword-widget .cwd-in.cwd-right,
.setsquare-widget .ss-in.ss-right,
.missingletters-widget .ml-in.ml-right,
.missingmaths-widget .mm-in.mm-right,
.wordmorph-widget .wm-in.wm-right,
.missinglink-widget .mlk-c.mlk-right,
.bricktrick-widget .bt-c.bt-ok { font-weight: 800; text-decoration: none; }

/* ============================================================================================
   DARK MODE — WIDGET/PUZZLE/QUIZ/SCRIM SWEEP (Mark, in-app QA, 2026-07-30)
   ============================================================================================
   Two gaps he found, both real: (1) fixed light-surface colours in the puzzle/quiz widgets don't
   respond to `.darktheme` at all (crossword/quiz-list clues were already patched — see the small
   .darktheme block above — but everything built AFTER that patch, i.e. every widget family below
   it in the file, was never swept); (2) the `--accent-on-light` variable used across many of these
   same widgets is deliberately computed ONCE, per article, to be legible on a WHITE page — it is
   baked in with `!important` at `div[article=…]`/`body.connect-mobile-edition` scope (phase4.py
   `_accent_style`), the SAME element `.darktheme` lands on. Redefining `--accent-on-light` itself
   inside `.darktheme` would therefore be racing that inline `!important` rule for the SAME element,
   which is not a race worth depending on. So instead: (a) a NEW `--accent-on-dark` token is derived
   from `--accent` (which the existing block above already remaps to `--accent-dark`) by lightening
   it toward white — `--accent` itself is left completely alone anywhere it's already used as a fill
   (quiz-n circle, standfirst pill bg, quiz-score pill, step-n circle …) per the standing rule that
   accent-driven FILLS are self-contained (their own text colour is computed for contrast against
   THEM, not the page); and (b) every `color` this sweep touches is set directly, with `!important`,
   so it always wins regardless of how the underlying custom property resolves — a `color` inside
   `.article-quiz .quiz-opt-tag { color: var(--accent-on-light,#888) }` is NOT itself `!important`,
   so a `!important` override here beats it outright.

   Design split, kept from the file's own documented precedent (the small .darktheme block above
   says "the crossword/sudoku GRIDS stay white-with-dark-ink (like a printed grid) — only the clue
   LISTS invert"): CROSSWORD + SUDOKU keep that exact behaviour (their letter grids are untouched
   here — the paper-page look is a deliberate, already-shipped decision, not a gap). EVERY OTHER
   grid/cell widget (wordsearch, kriss-kross, codeword, setsquare, bricktrick, missinglink,
   pathfinder) is genuine "app chrome" with no such precedent, so it gets the same dark-surface
   treatment as the quiz/riddle/jumble/etc widgets. Semantic green/red (right/wrong) is KEPT green/
   red everywhere (Patrick's colour-blind glyph work already carries the actual tick/cross — this
   sweep only makes the tint legible on a dark surface, never removes it).

   One correctness bug surfaced by this audit, fixed here rather than left: `.cw-input` (crossword)
   and `.su-input` (sudoku) set NO explicit `color` at all for the reader's own in-progress typed
   letter — it inherits the ambient text colour, which is `var(--main-color)` and therefore FLIPS
   to a light colour in dark mode. Since their grid cells deliberately stay WHITE paper, a flipped
   light ink would be invisible on that white cell. Pinned to a fixed dark ink below (independent
   of theme is the correct fix — the cell is paper in both themes, so the ink must stay dark ink
   in both themes too). ==== */

.darktheme {
  /* Lightened accent for text that sits DIRECTLY on the page/panel background (never on a solid
     accent FILL — those are untouched, see above). 58% keeps the brand hue recognisable while
     guaranteeing it reads meaningfully lighter than the ~#0f0f0f page, whatever the raw accent is. */
  --accent-on-dark:   color-mix(in srgb, var(--accent) 58%, #fff);
  /* The plain-text contents section header (new base default) sits directly on the dark page,
     so it takes the lightened accent — the boxed variant (a title override restoring the accent
     FILL + #fff) is untouched because those overrides set the fg explicitly. */
  --toc-section-span-fg: var(--accent-on-dark);
  /* Generic dark-widget palette — reused across every non-paper widget below. */
  --dm-surface:       #1e1e1e;   /* default cell / input / pill background (was #fff) */
  --dm-surface-2:     #2a2a2a;   /* "given"/printed/fixed letter background (was #f0f0f0/#f2f2f2/#f3f4f6) */
  --dm-border:        rgba(255,255,255,0.22);   /* generic light-grey borders (#ccc/#ddd/#bbb/#999/#888/#777) */
  --dm-ink:           #93c5fd;   /* the reader's own in-progress answer (was #1d4ed8 — too dark to read on a dark cell) */
  --dm-given-ink:     #e8e8e8;   /* printed/given letter ink (was #1a1a1a/#222/#333/#444) */
  --dm-muted-ink:     rgba(255,255,255,0.55);   /* small in-cell numbers/labels (was #555/#888) */
  --dm-right-bg:      #10281c;   /* correct-answer surface (was #f0fdf4) */
  --dm-right-fg:      #4ade80;   /* correct-answer text (was #16a34a — kept GREEN, just lightened for a dark bg) */
  --dm-right-border:  #22c55e;
  --dm-wrong-bg:      #3a1414;   /* wrong-answer surface (was #fef2f2) */
  --dm-wrong-fg:      #f87171;   /* wrong-answer text (was #dc2626 — kept RED, just lightened) */
  --dm-wrong-border:  #ef4444;
  --dm-hl-blue:       #1e3a5f;   /* "in-progress word" highlight (was #dbeafe/#d4eaff/#6aaeee) */
  --dm-hl-blue-2:     #16304d;
  --dm-hl-amber:      #4a3b12;   /* "current cell" highlight (was #fde68a) */
  --dm-hl-pink:       #4a1d33;   /* start-cell / shaded-cell highlight (was #fbcfe8/#fbdce4/#f9a8d4) */
  --dm-hl-pink-2:     #5c2440;
}

/* -- accent-on-light gaps: every place text sits on the page/panel background using the
   white-legible accent, incl. several "scrim head bits" Mark called out directly. -- */
.darktheme .article-standfirst { color: var(--standfirst-text, var(--accent-on-dark)) !important; }
.darktheme .article-pull-quote { color: var(--accent-on-dark) !important; }
.darktheme .article-hero--scrim:not(.article-hero--desktop) .hero-eyebrow,
.darktheme .article-hero--scrim:not(.article-hero--wide) .hero-eyebrow { color: var(--accent-on-dark) !important; }
.darktheme .article-image-feature .feature-heading { color: var(--accent-on-dark) !important; }
.darktheme .section-toc-num { color: var(--accent-on-dark) !important; }
.darktheme .article-debate .debate-pick { color: var(--accent-on-dark) !important; }
.darktheme .article-boxout.article-tip .ld_bold:first-child,
.darktheme .article-boxout.article-tip b:first-child { color: var(--accent-on-dark) !important; }
.darktheme .masthead .sectionName { color: var(--accent-on-dark) !important; }
.darktheme a.page-ref { color: var(--accent-on-dark) !important; }
.darktheme .krisskross-widget .kk-len,
.darktheme .bricktrick-widget .bt-n,
.darktheme .setsquare-widget .ss-t,
.darktheme .crossword-widget .cw-hidden-ans,
.darktheme .branching-widget .br-label,
.darktheme .article-quiz .quiz-opt-tag,
.darktheme .wordmatch-widget .wmt-tag,
.darktheme .riddle-widget .rd-n { color: var(--accent-on-dark) !important; }
/* these two use currentColor for their circular border, so fixing `color` fixes the ring too */
.darktheme .branching-widget .br-restart,
.darktheme .cyoa-restart { color: var(--accent-on-dark) !important; }
.darktheme .cyoa-tab:not(.on) { color: var(--accent-on-dark) !important; }

/* -- scrim desktop-flip media panel: a light-grey letterbox box reads as a stray white panel on a
   dark page. Also the plain (non-scrim) landscape hero container, which shares the same #f2f2f2. -- */
.darktheme .article-hero-landscape,
.darktheme .article-hero--scrim:not(.article-hero--desktop) .article-hero__media {
  background: var(--dm-surface);
}
@media (min-width: 640px) {
  .darktheme .article-hero--scrim:not(.article-hero--wide) .article-hero__media { background: var(--dm-surface); }
}
/* contents/section-page thumb placeholder + a plain divider rule, both light in the base CSS */
.darktheme .sectionArticleImage { background: var(--dm-surface); }
.darktheme .article-spacer { background: rgba(255,255,255,0.10); }
.darktheme .article-image-feature { background: var(--dm-surface-2); }
.darktheme .article-boxout.article-tip {
  background: color-mix(in srgb, var(--accent) 18%, #000);
}

/* -- crossword / sudoku: the GRID stays white paper by design (see the note above) — only pin the
   reader's OWN typed ink to a fixed dark colour, since it was inheriting the (now light) ambient
   text colour and vanishing on the white cell. Everything else in these two widgets is untouched. -- */
.darktheme .crossword-widget .cw-input,
.darktheme .sudoku-widget .su-input { color: #1a1a1a; }

/* -- quiz / imagechoice / wordmatch: white option cells -- */
.darktheme .article-quiz .quiz-opt { background: var(--dm-surface); border-color: var(--dm-border); }
.darktheme .article-quiz .quiz-opt.right,
.darktheme .article-quiz .quiz-opt.shown {
  border-color: var(--dm-right-border); background: var(--dm-right-bg); color: var(--dm-right-fg);
}
.darktheme .article-quiz .quiz-opt.wrong {
  border-color: var(--dm-wrong-border); background: var(--dm-wrong-bg); color: var(--dm-wrong-fg);
}
.darktheme .article-quiz .quiz-opts-unjudged .quiz-opt.picked { background: var(--dm-surface-2); }
.darktheme .article-quiz .quiz-a summary { background: var(--dm-surface-2); color: var(--dm-given-ink); }
.darktheme .article-quiz .quiz-a summary:hover { background: #383838; }
.darktheme .wordmatch-widget .wmt-cell { background: var(--dm-surface); border-color: var(--dm-border); }
.darktheme .wordmatch-widget .wmt-cell.sel { box-shadow: 0 0 0 2px rgba(255,255,255,0.10); }
.darktheme .wordmatch-widget .wmt-cell.done {
  border-color: var(--dm-right-border); background: var(--dm-right-bg); color: var(--dm-right-fg);
}
.darktheme .wordmatch-widget .wmt-cell.bad { border-color: var(--dm-wrong-border); background: var(--dm-wrong-bg); }
.darktheme .branching-widget .br-choice { background: var(--dm-surface); border-color: var(--dm-border); }

/* -- wordmorph -- */
.darktheme .wordmorph-widget .wm-ch { background: var(--dm-surface-2); }
.darktheme .wordmorph-widget .wm-in { color: var(--dm-ink); border-color: var(--dm-border); }
.darktheme .wordmorph-widget .wm-in.wm-right {
  border-color: var(--dm-right-border); color: var(--dm-right-fg); background: var(--dm-right-bg);
}
.darktheme .wordmorph-widget .wm-in.wm-wrong {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}
.darktheme .wordmorph-widget .wm-soln summary {
  background: var(--dm-surface); color: var(--dm-given-ink); border-color: #3a3a3a;
  box-shadow: 0 3px 0 rgba(0,0,0,.4);
}

/* -- riddle -- */
.darktheme .riddle-widget .rd-in { color: var(--dm-ink); border-color: var(--dm-border); }
.darktheme .riddle-widget .rd-in.rd-right {
  border-color: var(--dm-right-border); color: var(--dm-right-fg); background: var(--dm-right-bg);
}
.darktheme .riddle-widget .rd-in.rd-wrong {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}

/* -- jumble -- */
.darktheme .jumble-widget .jb-tile { background: var(--dm-surface-2); border-color: var(--dm-border); }
.darktheme .jumble-widget .jb-in { color: var(--dm-ink); border-color: var(--dm-border); }
.darktheme .jumble-widget .jb-in.jb-right {
  border-color: var(--dm-right-border); color: var(--dm-right-fg); background: var(--dm-right-bg);
}
.darktheme .jumble-widget .jb-in.jb-wrong {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}

/* -- Aquila dark pass (Patrick, TestFlight 2026-08-10): text still black-on-black. Two causes.
   (1) THE :root DERIVATION TRAP (this file's own documented trap, again): `--lead-color:
   var(--second-color)` RESOLVES at :root (html), but `.darktheme` lands on BODY — so the lead
   inherited the resolved LIGHT-theme literal and every article-full-lead read #2e2e2e on #0f0f0f
   (8-1 "Time to get into all the deets", 14-1, 44-1 "Big ideas from inquisitive kids"). Any
   var-derived colour custom property must be RE-DERIVED inside .darktheme. */
.darktheme {
  --lead-color: var(--second-color);
  --boxout-border-top: 3px solid var(--boxout-border-color);
  --toc-section-border-bottom: 1px solid var(--main-color);
}
/* (2) widget rules above set SURFACES but not the TEXT: audited every reported article with a
   computed-luminance sweep — these are all the classes whose text stayed near-black on a dark
   ground (wordmorph fixed letters, wordmatch cell words, wordsearch clue list, riddle verses,
   the solutions chains, the feature-card body that carries "Stone me!"-style rules). */
.darktheme .wordmorph-widget .wm-ch { color: var(--dm-given-ink); }
.darktheme .wordmatch-widget .wmt-cell { color: var(--dm-given-ink); }
.darktheme .wordsearch-widget .ws-w { color: var(--dm-given-ink); }
.darktheme .riddle-widget .rd-verse { color: var(--dm-given-ink); }
.darktheme .wm-soln-chain { color: var(--dm-given-ink); }
.darktheme .article-image-feature .feature-body { color: var(--dm-given-ink); }
.darktheme .article-image-feature .feature-credit { color: var(--dm-muted-ink); }

/* -- spell-it-out -- */
.darktheme .spellitout-widget .so-opt { background: var(--dm-surface); border-color: var(--dm-border); }
.darktheme .spellitout-widget .so-opt.so-right {
  border-color: var(--dm-right-border); background: var(--dm-right-bg); color: var(--dm-right-fg);
}
.darktheme .spellitout-widget .so-opt.so-wrong {
  border-color: var(--dm-wrong-border); background: var(--dm-wrong-bg); color: var(--dm-wrong-fg);
}

/* -- missing-letters -- */
.darktheme .missingletters-widget .ml-fixed,
.darktheme .missingletters-widget .ml-in { border-color: var(--dm-border); }
.darktheme .missingletters-widget .ml-fixed { background: var(--dm-surface-2); color: var(--dm-given-ink); }
.darktheme .missingletters-widget .ml-in { background: var(--dm-surface); color: var(--dm-ink); }
.darktheme .missingletters-widget .ml-in.ml-right {
  border-color: var(--dm-right-border); color: var(--dm-right-fg); background: var(--dm-right-bg);
}
.darktheme .missingletters-widget .ml-in.ml-wrong {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}

/* -- missing-maths (both the number-blank and the "solve the sums" operator-blank variant) -- */
.darktheme .missingmaths-widget .mm-in { color: var(--dm-ink); border-color: var(--dm-border); }
.darktheme .missingmaths-widget .mm-in.mm-right {
  border-color: var(--dm-right-border); color: var(--dm-right-fg); background: var(--dm-right-bg);
}
.darktheme .missingmaths-widget .mm-in.mm-wrong {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}
.darktheme .missingmaths-widget .mm-op { background: var(--dm-surface); color: var(--dm-ink); border-color: var(--dm-border); }
.darktheme .missingmaths-widget .mm-op.right,
.darktheme .missingmaths-widget .mm-op.shown {
  border-color: var(--dm-right-border); color: var(--dm-right-fg);
}
.darktheme .missingmaths-widget .mm-op.right { background: var(--dm-right-bg); }
.darktheme .missingmaths-widget .mm-op.wrong {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}

/* -- image-anagram ("what in the world?") -- */
.darktheme .wiw-carousel .wiw-tile { background: var(--dm-surface-2); border-color: var(--dm-border); }
.darktheme .wiw-carousel .wiw-in { color: var(--dm-given-ink); caret-color: var(--dm-given-ink); border-color: var(--dm-border); }
.darktheme .wiw-carousel .wiw-in.ok {
  border-color: var(--dm-right-border); color: var(--dm-right-fg); background: var(--dm-right-bg);
}
.darktheme .wiw-carousel .wiw-in.no {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}
.darktheme .wiw-carousel .wiw-dot { background: rgba(255,255,255,0.28); }

/* -- kriss-kross (NOT crossword/sudoku — app chrome, no "paper grid" precedent, so it inverts) -- */
.darktheme .krisskross-widget .kk-cell { background: var(--dm-surface); border-color: #555; }
.darktheme .krisskross-widget .kk-len-n { color: var(--dm-muted-ink); }
.darktheme .krisskross-widget .kk-in { color: var(--dm-ink); }
.darktheme .krisskross-widget .kk-in.kk-right { color: var(--dm-right-fg); }
.darktheme .krisskross-widget .kk-in.kk-wrong { color: var(--dm-wrong-fg); }
.darktheme .krisskross-widget .kk-cell.kk-given { background: var(--dm-surface-2); }
.darktheme .krisskross-widget .kk-in.kk-given { color: var(--dm-given-ink); }
.darktheme .krisskross-widget .kk-cell.kk-hl { background: var(--dm-hl-blue); }
.darktheme .krisskross-widget .kk-cell.kk-hl.kk-given { background: var(--dm-hl-blue-2); }
.darktheme .krisskross-widget .kk-cell.kk-cur { background: var(--dm-hl-amber); }

/* -- bricktrick -- */
.darktheme .bricktrick-widget .bt-c { background: var(--dm-surface); border-color: var(--dm-border); color: var(--dm-ink); }
.darktheme .bricktrick-widget .bt-c.bt-ok {
  border-color: var(--dm-right-border); color: var(--dm-right-fg); background: var(--dm-right-bg);
}
.darktheme .bricktrick-widget .bt-c.bt-bad {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}

/* -- missinglink -- */
.darktheme .missinglink-widget .mlk-c { border-color: var(--dm-border); color: var(--dm-ink); }
.darktheme .missinglink-widget .mlk-c.mlk-shaded { background: var(--dm-hl-pink); border-color: var(--dm-hl-pink-2); }
.darktheme .missinglink-widget .mlk-c.mlk-right {
  border-color: var(--dm-right-border); color: var(--dm-right-fg); background: var(--dm-right-bg);
}
.darktheme .missinglink-widget .mlk-c.mlk-shaded.mlk-right { background: #1c3327; }
.darktheme .missinglink-widget .mlk-c.mlk-wrong {
  border-color: var(--dm-wrong-border); color: var(--dm-wrong-fg); background: var(--dm-wrong-bg);
}

/* -- codeword -- */
.darktheme .codeword-widget .cwd-cell { background: var(--dm-surface); border-color: #555; }
.darktheme .codeword-widget .cwd-n { color: var(--dm-muted-ink); }
.darktheme .codeword-widget .cwd-in { color: var(--dm-ink); }
.darktheme .codeword-widget .cwd-in.cwd-right { color: var(--dm-right-fg); }
.darktheme .codeword-widget .cwd-in.cwd-wrong { color: var(--dm-wrong-fg); }
.darktheme .codeword-widget .cwd-track .cwd-tr { border-color: var(--dm-border); }
.darktheme .codeword-widget .cwd-tr-in { color: var(--dm-ink); }
.darktheme .codeword-widget .cwd-tr.cwd-got { border-color: var(--dm-right-border); background: var(--dm-right-bg); }

/* -- setsquare -- */
.darktheme .setsquare-widget .ss-cell { background: var(--dm-surface); border-color: var(--dm-border); }
.darktheme .setsquare-widget .ss-given { background: var(--dm-surface-2); }
.darktheme .setsquare-widget .ss-op { color: var(--dm-muted-ink); }
.darktheme .setsquare-widget .ss-in { color: var(--dm-ink); }
.darktheme .setsquare-widget .ss-in.ss-right { color: var(--dm-right-fg); }
.darktheme .setsquare-widget .ss-in.ss-wrong { color: var(--dm-wrong-fg); }

/* -- pathfinder -- */
.darktheme .pathfinder-widget .pf-grid { background: var(--dm-surface); border-color: #555; }
.darktheme .pathfinder-widget .pf-c { border-color: var(--dm-border); color: var(--dm-given-ink); }
.darktheme .pathfinder-widget .pf-c.pf-on { background: rgba(59,130,246,0.30); color: var(--dm-ink); }
.darktheme .pathfinder-widget .pf-c.pf-start { background: var(--dm-hl-pink); }
.darktheme .pathfinder-widget .pf-c.pf-start.pf-on { background: var(--dm-hl-pink-2); }
.darktheme .pathfinder-widget .pf-c.pf-no { background: #5c1414 !important; }
.darktheme .pathfinder-widget .pf-w.pf-done { color: var(--dm-right-fg); }

/* -- wordsearch -- */
.darktheme .wordsearch-widget .ws-grid { background: var(--dm-surface); border-color: #555; }
.darktheme .wordsearch-widget .ws-cell { border-color: var(--dm-border); color: var(--dm-given-ink); }
.darktheme .wordsearch-widget .ws-cell.ws-start { background: var(--dm-hl-amber); }
.darktheme .wordsearch-widget .ws-words li.ws-done .ws-w,
.darktheme .wordsearch-widget .ws-words li.ws-done::after,
.darktheme .wordsearch-widget .ws-msg { color: var(--dm-right-fg); }

/* ==== end dark-mode widget sweep ==== */

/* ==== photo-story (caption-prose roundup — cycp 24-1 "Shock & Awe"): numbered photos stack
   FULL-WIDTH in printed-number order, each with its explanation beneath — impact over grids
   (Patrick, 2026-07-21). The stack is an OWNED layout: galleries/anchoring leave it alone. ==== */
.article-photo-story { margin: 1rem 0 1.75rem; }
.article-photo-story .article-in-image {
    float: none; width: 100%; max-width: 100%; margin: 1.6rem 0; clear: both;
}
.article-photo-story img.article-in-image {
    width: 100%; height: auto; max-height: none; border-radius: 4px; display: block;
}
.article-photo-story .articleImageCaption { padding: 0 var(--side-pad); margin-top: 0.5rem; }

/* Stacked author-bio variant — a LEADING contributor-bio boxout moved to the article end with
   its inner elements kept verbatim (cycp 18-1 Oscar Huckle; QA correction keys survive). The
   base card is a flex ROW (headshot|text); this variant stacks the boxout's own paragraphs. */
.article-author-bio--stack { display: block; }
.article-author-bio--stack .article-subhead {
    background: none; padding: 0.1rem 0; margin: 0 0 0.2rem; text-align: left; font-size: 1.05em;
}
.article-author-bio--stack .article-full-body { padding: 0; margin: 0.4rem 0; font-size: 0.95em; }
.article-author-bio--stack .article-in-image,
.article-author-bio--stack .article-in-image img {
    float: none; width: 110px; max-width: 110px; height: auto; margin: 0.5rem 0 0.2rem;
    border-radius: 4px;
}

/* ==== inline page-reference links (cycp 5-1 "Your Experts" p33/p51; continuation refs) ====
   Same navigation as the contents cards, styled as a quiet accent link so it reads as
   editorial furniture, not a shouty hyperlink. */
a.page-ref { color: var(--accent-on-light, var(--accent)); text-decoration: underline;
    text-decoration-thickness: 1px; text-underline-offset: 2px; }

/* ==== Listen button (app-injected into .listenlocation — dev contract, Stephen 2026-07-22).
   .listenlocation itself MUST stay unstyled (empty div, invisible in old apps). The app owns
   .listenholder/.listen; everything below is CONTEXT styling, scoped to `.listenlocation
   .listenholder` so it can only affect a button injected into OUR placement div — old apps'
   fallback-positioned button keeps its stock look. ==== */
.listenlocation .listenholder { display: inline-block; margin: 0 0 2px; }
/* Consistent gap ABOVE the Listen button. The gap used to be the preceding head element's
   bottom-margin PLUS the holder's 14px top-margin, which STACKED to 12–34px across articles
   (a lead's 20px margin + 14px = ~34; a byline's 0 + 14 = ~14) — Patrick 86-1/89-1/14-1. Now the
   holder adds nothing on top (0 above) and the .listenlocation carries a single fixed top margin
   that COLLAPSES with the preceding margin (so the gap is max(prev, 1.25rem), ~20px everywhere).
   Scrim keeps its own holder spacing (below), so its panel gap is unchanged. */
.listenlocation { margin-top: 1.25rem; }
.article-hero__text .listenlocation { margin-top: 0; }
/* empty head furniture (a byline/eyebrow/lead/standfirst with no text) still took a line's height
   before the Listen, inflating the gap (14-1's empty byline). Collapse truly-empty ones. */
.hero-eyebrow:empty, .hero-byline:empty, .article-full-byline:empty,
.article-standfirst:empty, .article-full-lead:empty { display: none; }
/* PLAIN header: the head block's elements self-pad with --side-pad but the empty
   listenlocation div doesn't, so the injected pill sat flush with the screen edge (RM
   Welcome, Patrick 2026-07-22). Indent it to the text column; containers that already
   pad (scrim panel) or centre (centred header) their content are excluded. */
.listenlocation { margin-left: calc(var(--side-pad) + var(--read-inset, 0px)); }
.article-hero__text .listenlocation,
.article-header-centered .listenlocation { margin-left: 0; }
/* Centred header (editor's letter etc.): the head block is centred, so the Listen pill centres
   with it (Patrick, WCM 5-1 — it sat left-aligned under a centred headline). text-align centres
   the inline-block .listenholder the app injects. !important because the APP injects the holder
   with its own styles (it centred in viewerv2 but not in-app — the app's display/width/float on
   the holder was defeating the text-align; author !important beats even inline styles). */
.article-header-centered .listenlocation { text-align: center; }
.article-header-centered .listenlocation .listenholder {
    display: inline-block !important; float: none !important; width: auto !important;
    margin-left: auto !important; margin-right: auto !important; text-align: center; }
/* Bottom-anchored scrim (--tall): balance the space above/below the pill (the gap read
   asymmetric in-app — Patrick, RM "Going outside"). */
.article-hero--scrim .listenlocation .listenholder { margin: 12px 0 0; }
/* The app's injected .listenholder ships height:94px + a border-bottom separator + 5% side
   padding — a huge gap + rule between lead and body, and the 5% pad on top of DIFFERING
   container pads is why the button indented on some head treatments and not others (Patrick,
   RM 2026-07-22). Neutralize all three: size to the button, no separator, no extra indent —
   it then left-aligns with the surrounding text in EVERY treatment. */
.listenlocation .listenholder { height: auto; border-bottom: none; padding: 0; width: auto; }
/* Times-style pill (Patrick 2026-07-22): replace the app's sprite artwork (117×46 png on
   .listen::before; frame 2 = playing) with a neutral monochrome pill — black on white,
   inverted to white on dark scrims by the existing holder filter. ::before becomes the
   headphones icon (masked, currentColor); the label renders via ::after, keyed off the
   app's .play state class. Scoped to .listenlocation, so old apps keep the stock button. */
.listenlocation .listen, .listenlocation .play {
    background: none; display: inline-flex; align-items: center; gap: 8px;
    margin: 8px 0; padding: 10px 18px; border: 1.5px solid currentColor;
    border-radius: 999px; color: var(--main-color, #1a1a1a);
    font-family: var(--font-family-main); font-size: 15px; font-weight: 600;
    line-height: 1; text-decoration: none; }
.listenlocation .listen::before, .listenlocation .play::before {
    content: ''; position: static; display: block; width: 16px; height: 16px;
    background: none; background-color: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a9 9 0 0 0-9 9v6a3 3 0 0 0 3 3h1a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H5v-1a7 7 0 0 1 14 0v1h-2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h1a3 3 0 0 0 3-3v-6a9 9 0 0 0-9-9z'/%3E%3C/svg%3E") center/contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a9 9 0 0 0-9 9v6a3 3 0 0 0 3 3h1a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H5v-1a7 7 0 0 1 14 0v1h-2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h1a3 3 0 0 0 3-3v-6a9 9 0 0 0-9-9z'/%3E%3C/svg%3E") center/contain no-repeat; }
.listenlocation .listen::after { content: 'Listen'; }
.listenlocation .listen.play::after, .listenlocation .play::after { content: 'Pause'; }
/* Centred header: the button centres with the block (inline-block + parent text-align). */
/* On a DARK scrim / coloured panel the stock blue control vanishes — render it WHITE. Scoped to
   the PHONE treatment (the desktop flip turns the same block dark-on-light, where white would
   vanish); the --desktop variant keeps its scrim at all widths so it inverts unconditionally.
   A LIGHT panel (--panel-light) keeps the stock look. */
@media (min-resolution: 2.5dppx) {
  .article-hero--scrim:not(.article-hero--panel-light) .listenlocation .listenholder {
      filter: brightness(0) invert(1); }
}
.article-hero--desktop:not(.article-hero--panel-light) .listenlocation .listenholder {
    filter: brightness(0) invert(1); }

/* WEB READER (#onlineReader, Patrick 2026-07-24): the online reader inserts its OWN
   .listenholder/.listen (with a real <span>Listen</span>) BEFORE the first content element —
   NOT into our .listenlocation — and paints the legacy blue sprite via `#onlineReader .listen:before`
   (id specificity, so it beats our .listenlocation rules AND wouldn't match them anyway). Restyle
   THAT button to the same monochrome pill here. Scoped to #onlineReader, so in-app (.listenlocation)
   and old-app fallbacks are untouched; !important on ::before beats the id-specificity sprite
   regardless of stylesheet load order. The label is the reader's own <span> (no ::after here, or it
   would double). */
#onlineReader .listen {
    background: none; display: inline-flex; align-items: center; gap: 8px; position: static;
    margin: 8px 0; padding: 10px 18px; border: 1.5px solid currentColor; width: auto; height: auto;
    border-radius: 999px; color: var(--main-color, #1a1a1a);
    font-family: var(--font-family-main); font-size: 15px; font-weight: 600;
    line-height: 1; text-decoration: none; }
#onlineReader .listen:before {
    content: '' !important; position: static !important; display: inline-block;
    width: 16px !important; height: 16px !important; margin: 0 !important; line-height: 1 !important;
    background: none !important; background-color: currentColor !important;
    border: none !important; border-radius: 0 !important;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a9 9 0 0 0-9 9v6a3 3 0 0 0 3 3h1a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H5v-1a7 7 0 0 1 14 0v1h-2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h1a3 3 0 0 0 3-3v-6a9 9 0 0 0-9-9z'/%3E%3C/svg%3E") center/contain no-repeat !important;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a9 9 0 0 0-9 9v6a3 3 0 0 0 3 3h1a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H5v-1a7 7 0 0 1 14 0v1h-2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h1a3 3 0 0 0 3-3v-6a9 9 0 0 0-9-9z'/%3E%3C/svg%3E") center/contain no-repeat !important; }

/* ── Puzzle CHECK button + the solved celebration ──────────────────────────────
   Triggered by Patrick reviewing tools/prototypes/celebration_options.html: "the buttons are a bit
   dull at the moment - a very functional 'check' and the celebration is just an instant 'success'
   type thing". He chose option B (tactile pill) plus celebrations 2+1 together — confetti AND the
   button itself becoming the reward — fired ONCE PER PUZZLE ("they might only do 1 type").

   All TWELVE check buttons shared one hard-coded navy (#1e3a5f) and never picked up the title
   accent, unlike the quiz option cells — so this is one rule, not twelve. Listing the classes rather
   than adding a shared one in phase-4 keeps every article XML byte-identical.

   NOTE: no shorthand `transition`. A shorthand covers ALL properties, so a var(--accent)-driven
   colour animates, and a transition that never completes leaves the computed colour on the OLD
   accent (caught switching titles in the prototype). Only transform/box-shadow transition here. */
.article-puzzle .bt-check, .article-puzzle .cwd-check, .article-puzzle .jb-check,
.article-puzzle .kk-check, .article-puzzle .ml-check,  .article-puzzle .mlk-check,
.article-puzzle .mm-check, .article-puzzle .rd-check,  .article-puzzle .ss-check,
.article-puzzle .su-check, .article-puzzle .wiw-check, .article-puzzle .wm-check {
  position: relative;               /* the confetti is absolutely positioned inside the button */
  margin-top: 10px;
  /* NOT the `font` shorthand: `font: 800 .95em/1.2 inherit` is INVALID (inherit is not a legal
     family inside the shorthand) so the whole declaration is dropped and the weight silently
     stays 400 — caught in the browser. Longhand it is. */
  font-family: inherit;
  font-weight: 800;
  font-size: 0.95em;
  line-height: 1.2;
  /* Wide enough for the reward label so nothing shifts when "Check" becomes "✓ Correct"
     (Patrick: "perhaps we start with a wider button so there's no shift"). */
  min-width: 8.4em;
  padding: 10px 22px;
  background: var(--accent, #1e3a5f);
  color: var(--standfirst-fg, #fff);
  border: 0;
  border-radius: 26px;
  cursor: pointer;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .28);
  transition: transform .06s, box-shadow .06s;
  overflow: visible;
}
.article-puzzle .bt-check:active, .article-puzzle .cwd-check:active,
.article-puzzle .jb-check:active, .article-puzzle .kk-check:active,
.article-puzzle .ml-check:active, .article-puzzle .mlk-check:active,
.article-puzzle .mm-check:active, .article-puzzle .rd-check:active,
.article-puzzle .ss-check:active, .article-puzzle .su-check:active,
.article-puzzle .wiw-check:active, .article-puzzle .wm-check:active {
  transform: translateY(3px);
  box-shadow: 0 1px 0 rgba(0, 0, 0, .28);
}
/* SOLVED: the button becomes the reward, so the good news is exactly where you just tapped. Green
   rather than the accent, because a light accent (FFT/NGK yellow) does not read as "correct". */
.article-puzzle .pz-won {
  background: #16a34a !important;
  color: #fff !important;
  cursor: default;
  animation: pzPop .42s ease-out;
}
@keyframes pzPop { 0% { transform: scale(.88) } 45% { transform: scale(1.08) } 100% { transform: none } }
/* Several puzzles have NO check button — a wordsearch, a pathfinder and a word match are solved by
   tapping the grid itself — so they burst from their own message line instead. */
.article-puzzle .pz-burst { position: relative; display: inline-block; }
.article-puzzle .pz-conf {
  position: absolute; left: 50%; top: 50%;
  width: 9px; height: 9px; border-radius: 2px;
  opacity: 0; pointer-events: none;
  animation: pzFly .95s cubic-bezier(.2, .7, .4, 1) forwards;
}
@keyframes pzFly {
  0%   { opacity: 1; transform: translate(-50%, -50%) rotate(0) scale(.6) }
  100% { opacity: 0;
         transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy)))
                    rotate(var(--rot)) scale(1) }
}
@media (prefers-reduced-motion: reduce) {
  .article-puzzle .pz-won { animation: none }
  .article-puzzle .pz-conf { display: none }
}

/* "↺ Reset" — the way back out of a one-and-done lock now that puzzle state PERSISTS (Patrick:
   "we'll probably want a clear as well because a few of them are one and done, ie get it wrong
   and it's locked"). Injected by carousel.js as a SIBLING after each puzzle widget; hidden until
   the widget actually holds state. White-pill convention (the Show-a-solution / Reveal family).
   UNSCOPED deliberately: a promoted quiz has no .article-puzzle wrapper. */
.pz-reset {
  display: none;
  margin: 8px auto 2px;
  font-family: inherit; font-size: 12px;
  color: #666; background: transparent;
  border: 1px solid #c9c9c9; border-radius: 999px;
  padding: 4px 14px; cursor: pointer;
}
.pz-reset.on { display: block; }
.darktheme .pz-reset { color: #b9b9b9; border-color: #555; background: transparent; }
.sepiatheme .pz-reset { color: #6b5744; border-color: #b7a68e; }

/* Tap-to-enlarge affordance in CSS rather than JS: the cursor used to be set by the lightbox's
   bind() pass, which only runs if the host calls Lightbox.init() after injecting the article — the
   same dependency that made the tap itself unreliable (now delegated; see carousel.js). A linked
   image keeps the pointer, since its link wins. */
.article-mini-grid .mini-grid-cell img,
div.article-in-image > img.article-in-image,
.article-hero__media > img { cursor: zoom-in; }
.section-toc img, .toc-wrapper img, a img { cursor: pointer; }

/* ── Advert link pill ─────────────────────────────────────────────────────────
   An advert page is the creative plus ONE obvious tap target (Patrick, 2026-08-11 — matching the
   web reader: the advertiser name is hidden, the creative itself links, and the address repeats
   as a pill below it). A plain link paragraph reads as body copy on a page that has none, and on
   a full-bleed creative it is easy to miss entirely. The globe is a CSS mask (currentColor, so it
   follows the theme) rather than an inline <svg> — an un-namespaced svg is dead in an XHTML
   webview, the trap this codebase has hit three times. */
/* STACKED, one per line (Patrick, 2026-08-11: "on web we stack pills vertically in case of long
   ones") — a long URL pill beside another would wrap mid-address or overflow the column. */
.ad-links { display: flex; flex-direction: column; align-items: center;
            margin: 0.9rem 0 1.2rem; padding: 0 var(--side-pad); }
.ad-link-pill {
  display: inline-flex; align-items: center; gap: 7px;
  max-width: 100%; overflow-wrap: anywhere; text-align: center;
  margin: 6px 0 0;
  padding: 8px 18px 8px 15px;
  border: 1px solid #e0e0e0; border-radius: 999px; background: #f4f4f5;
  font-family: var(--font-family-main); font-size: 0.95rem; line-height: 1.2;
  color: var(--main-color); text-decoration: none;
}
.ad-link-pill::before {
  content: ""; width: 15px; height: 15px; flex: 0 0 15px; background: currentColor; opacity: .75;
  -webkit-mask: var(--globe-icon) center/contain no-repeat;
          mask: var(--globe-icon) center/contain no-repeat;
}
:root {
  --globe-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M3 12h18M12 3a15 15 0 0 1 0 18a15 15 0 0 1 0-18'/%3E%3C/svg%3E");
}
.darktheme .ad-link-pill { background: #1e1e1e; border-color: #3a3a3a; color: #ededed; }
.sepiatheme .ad-link-pill { background: #ece1cc; border-color: #cbbb9f; }

/* ── Reviewed-work citation ───────────────────────────────────────────────────
   The literary-review convention (TNR 58-1/62-1: book title / "by Author" / "Publisher, N pp.,
   $X") rendered AS PRINT SHOWS IT (Patrick: "treat it like it is on those pages" — p58 rendered
   at 5x): centred, no box, bold ACCENT title, muted grey credit lines, and a thin rule between
   consecutive citations in a multi-book review. phase-4's _style_work_citations reclasses the
   captured boxout; signature-gated, so titles without the convention never see these rules. */
.article-work-citation { text-align: center; margin: 1.4rem auto; max-width: 34em;
    padding: 0 var(--side-pad); }
.article-work-citation .wc-title { color: var(--accent-on-light, var(--accent));
    font-weight: 700; font-family: var(--font-family-main); font-size: 1.05rem;
    line-height: 1.3; margin: 0 0 3px; padding: 0; text-transform: none; }
.article-work-citation .wc-by, .article-work-citation .wc-pub {
    color: #6d6e71; font-family: var(--font-family-main); font-size: 0.95rem;
    line-height: 1.4; margin: 0; padding: 0; }
.article-work-citation + .article-work-citation {
    border-top: 1px solid #d0d0d0; padding-top: 1.15rem; margin-top: 1.15rem; }
.darktheme .article-work-citation .wc-title { color: var(--accent-on-dark, var(--accent)); }
.darktheme .article-work-citation .wc-by,
.darktheme .article-work-citation .wc-pub { color: #9a9a9a; }
.darktheme .article-work-citation + .article-work-citation { border-top-color: #3a3a3a; }

/* ── CYOA ending card ─────────────────────────────────────────────────────────
   Generic by design (Patrick: "just have the you made it and the not this time (without detail)"),
   because the ending's own printed words already render above it as .br-text — a card repeating them
   would say everything twice, and nothing has to be authored per issue. Green flag for a win, RED
   for a game over: a skull read as an error rather than a finish line. */
.branching-widget .br-endcard, .gv-ovl .br-endcard{
  margin:12px auto 4px;max-width:330px;padding:16px;border-radius:12px;text-align:center;
}
.branching-widget .br-win , .gv-ovl .br-win {background:linear-gradient(#f0fdf4,#dcfce7);border:2px solid #16a34a;}
.branching-widget .br-lose, .gv-ovl .br-lose{background:linear-gradient(#fff7ed,#ffedd5);border:2px solid #f59e0b;}
.branching-widget .br-fin , .gv-ovl .br-fin {background:#f6f6f4;border:2px solid #cfcfc8;}
.branching-widget .br-flag, .gv-ovl .br-flag{display:block;font-size:30px;line-height:1;margin-bottom:4px;}
.branching-widget .br-win  .br-flag, .gv-ovl .br-win  .br-flag{color:#16a34a;animation:brWave 1.1s ease-in-out infinite;}
.branching-widget .br-lose .br-flag, .gv-ovl .br-lose .br-flag{color:#dc2626;}
.branching-widget .br-fin  .br-flag, .gv-ovl .br-fin  .br-flag{color:#8b8b8b;}
@keyframes brWave{0%,100%{transform:rotate(-7deg)}50%{transform:rotate(7deg)}}
.branching-widget .br-endh, .gv-ovl .br-endh{
  margin:0 0 10px;font-family:var(--font-family-main-display,inherit);font-weight:800;font-size:1.2em;
}
.branching-widget .br-win  .br-endh, .gv-ovl .br-win  .br-endh{color:#15803d;}
.branching-widget .br-lose .br-endh, .gv-ovl .br-lose .br-endh{color:#b45309;}
.branching-widget .br-again, .gv-ovl .br-again{
  font-family:inherit;font-weight:800;font-size:0.95em;padding:9px 20px;border:0;border-radius:24px;
  cursor:pointer;background:var(--accent,#1e3a5f);color:var(--standfirst-fg,#fff);
  box-shadow:0 3px 0 rgba(0,0,0,.25);transition:transform .06s,box-shadow .06s;
}
.branching-widget .br-again:active, .gv-ovl .br-again:active{transform:translateY(2px);box-shadow:0 1px 0 rgba(0,0,0,.25);}
@media (prefers-reduced-motion:reduce){
  .branching-widget .br-win .br-flag, .gv-ovl .br-win .br-flag{animation:none}
}

/* Score + result lines in the BRAND colour rather than black (Patrick, on Aquila 24-1: "the 1/1
   correct and all matched on that's foe business, that could be in brand colour"). --accent-on-light
   is the luminance-safe accent, so a light brand (FFT/NGK yellow) stays legible on white. */
.article-quiz .quiz-score:not(:empty),
.article-puzzle .wm-msg:not(:empty),
.article-puzzle .ws-msg:not(:empty),
.article-puzzle .wmt-msg:not(:empty) {
  display: inline-block; margin-top: 8px; padding: 5px 14px;
  background: var(--accent, #1e3a5f); color: var(--standfirst-fg, #fff);
  border-radius: 20px; font-weight: 800; font-size: 0.9em;
}
/* ⚠️ .wmt-msg (word MATCH) is in that list because it was simply missed — Patrick spotted "1/4"
   shipping as bare text under That's foe business while every sibling widget wore the pill. The
   quiz score keys off .article-quiz rather than the puzzle wrapper: a PROMOTED body quiz (Aquila
   32-1 Olmec) has no wrapper, and a score line only ever exists where tappable options parsed, so
   this can't restyle anything else. And since .article-quiz is now left-aligned under a wrapper
   (the question-column fix), the inline-block pill stopped inheriting a centring text-align —
   shrink-wrap it and centre it with margins instead, so it stays centred whatever the quiz's own
   alignment does. The widget-level msgs (wm/ws/wmt) still inherit the WRAPPER's centre. */
.article-quiz .quiz-score:not(:empty) {
  display: table; margin-left: auto; margin-right: auto;
}

/* Restart is a small icon TOP-RIGHT, not a wide button under the story (Patrick: "the 'start again'
   button could get clicked accidentally... just have the two arrow restart symbol"). A deliberate
   restart lives on the ending card's own full-width Play again. */
.branching-widget{position:relative;}
.branching-widget .br-restart{
  position:absolute;top:0;right:0;z-index:2;
  width:30px;height:30px;padding:0;line-height:28px;text-align:center;
  font-family:inherit;font-size:16px;font-weight:700;
  background:transparent;color:var(--accent-on-light,#6b7280);
  border:1.5px solid currentColor;border-radius:50%;cursor:pointer;opacity:.65;
}
.branching-widget .br-restart:hover,.branching-widget .br-restart:focus{opacity:1;}
/* Only the LABEL row clears the icon — padding the whole node indented every line of the story
   (Patrick: "it doesn't need to push everything else over the left, maybe just align it with
   the 'offer your cookie to sobek' row"). */
.branching-widget .br-label{padding-right:38px;}

/* ==== cyoa-modes + guided-widget ==== (a choose-your-own-adventure rendered TWICE: the guided
   "comic" view pans and zooms the REAL printed spread from box to box, and the extracted text
   version sits behind a toggle. Patrick greenlit the guided view on the condition the text
   version stays first-class rather than being replaced, so the toggle is a genuine pair.
   Geometry — image scale, spotlight, vignette — is set inline by initGuided in carousel.js, which
   is the only place that knows the real viewport. */
.cyoa-modes { max-width: 34em; margin: 1em auto; text-align: left; }
.cyoa-tabs { display: flex; gap: 6px; justify-content: center; margin: 0 0 10px; }
.cyoa-tab {
  font: inherit; font-size: 0.85em; font-weight: 700; padding: 6px 14px; cursor: pointer;
  border: 1.5px solid var(--accent, #888); border-radius: 20px;
  background: transparent; color: var(--accent-on-light, #333);
}
.cyoa-tab.on { background: var(--accent, #1e3a5f); color: var(--standfirst-fg, #fff); }
.cyoa-pane { display: none; }
.cyoa-modes[data-mode="guided"] .cyoa-pane[data-mode="guided"],
.cyoa-modes[data-mode="text"] .cyoa-pane[data-mode="text"] { display: block; }
/* The text pane owns the widget's own centring margin, so don't double it up. */
.cyoa-pane .branching-widget, .cyoa-pane .guided-widget { margin-top: 0; }

.guided-widget { max-width: 34em; margin: 1em auto; font-family: var(--font-family-main, sans-serif); }
.gv-stage {
  position: relative; overflow: hidden; width: 100%; aspect-ratio: 3 / 4;
  background: #0b0b0d; border-radius: 12px;
}
/* Zoom is `transform: translate() scale()`, NOT `width`. Both work, but animating `width` on a
   3308px-wide image forces layout + repaint every frame of the 0.55s move, where a transform is
   composited and touches no layout — worth having on a phone panning a 1.2 MB canvas. The maths is
   identical: with origin 0 0, image point p lands at tx + p*s either way.

   ⚠️ VERIFICATION TRAP that cost a wrong diagnosis here: in a HIDDEN browser pane
   (`document.hidden`), transitions never advance, so `getComputedStyle` reports the *start* value
   forever — an inline `width`/`transform` reads back as the un-animated base even with `!important`,
   because transition declarations outrank importance. That looks exactly like a broken zoom. Set
   `transition: none` before measuring, or read the inline value, before concluding anything. */
.gv-img {
  position: absolute; top: 0; left: 0; width: 100%; transform-origin: 0 0; max-width: none;
  transition: transform .55s cubic-bezier(.22,.9,.28,1);
}
/* ⚠️ There is deliberately NO `[data-pzi] .gv-img { width: auto }` rule any more, and it must not come
   back. `scale()` is only meaningful against a known layout width, and that rule was the thing
   supplying it — so when it did not reach the reader (it hangs off an attribute the JS stamps, and the
   in-app webview is documented as re-creating article DOM) the base `width: 100%` gave the STAGE width
   and the spread rendered at stage*scale: 3308px in a 544px stage => 89px, hard left. That was the
   "tiny and off to the left" poster on phone AND web. `place()` in carousel.js now sets width,
   max-width and transform-origin inline alongside the transform, so the geometry cannot be changed by
   any stylesheet. `width: 100%` here is now purely the no-JS / pre-init fallback, where the image is a
   plain fitted picture — which is also the right thing to show if `place()` bails on a zero-sized
   stage, the case `width: auto` used to turn into a 3308px overflow. */
/* NO JS (or not yet initialised — the INITS loop stamps data-pzi before it runs): show the printed
   spread as a plain picture rather than a clipped, un-navigable window. The reader still gets the
   artwork, and the text pane below carries the whole story. */
.guided-widget:not([data-pzi]) .gv-stage { aspect-ratio: auto; background: none; }
.guided-widget:not([data-pzi]) .gv-img { position: static; }
.guided-widget:not([data-pzi]) .gv-start,
.guided-widget:not([data-pzi]) .gv-ovl { display: none; }

/* A FEATHERED vignette, not a hard-edged shadow (Patrick): the focused panel and its immediate
   surroundings are fully clear, then it fades, and the far field only reaches ~48% so you can still
   make out what is around you if you want to look. */
.gv-vig {
  position: absolute; inset: 0; pointer-events: none; opacity: 0;
  transition: opacity .55s cubic-bezier(.22,.9,.28,1), background .55s cubic-bezier(.22,.9,.28,1);
}
.gv-spot {
  position: absolute; border-radius: 12px; pointer-events: none; opacity: 0;
  box-shadow: 0 0 0 1px rgba(255,255,255,.22);
  transition: all .55s cubic-bezier(.22,.9,.28,1);
}
.gv-stage.playing .gv-vig, .gv-stage.playing .gv-spot { opacity: 1; }
.gv-stage.playing /* The choices FLOAT just below the focused panel rather than sitting in a bar at the bottom of the
   screen (Patrick: "float the buttons a little below the focus… so the choice buttons are by the
   question, just below where the spotlight ends"). `top` is set by the JS, which is the only thing
   that knows where the spotlight ended up; the band the JS reserves is still CONSTANT for the whole
   chart, so the panel never hops up and down between steps. No gradient any more — it belonged to a
   bottom-anchored bar, and the darker vignette now does that work. */
.gv-ovl {
  position: absolute; left: 0; right: 0; padding: 0 10px; display: none;
  flex-direction: column; gap: 8px;
}
.gv-stage.playing .gv-ovl { display: flex; }
/* Capped so the buttons don't stretch the full width of a tablet or desktop stage (Patrick), and
   centred within whatever width they get. */
.gv-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
  width: 100%; max-width: 460px; margin: 0 auto;
}
/* TWO per row and taller, so a choice reads as a BUTTON rather than a flat panel (Patrick): rounder
   corners, a two-line minimum, a green stroke and a hard drop with an :active press. Deliberately
   GREEN and not var(--accent): these sit on near-black, where a dark brand accent would disappear —
   the same reasoning as the puzzle celebration's green tick. The faint white outer stroke is what
   lifts them off the black. An odd last choice spans both columns rather than leaving a gap. */
.gv-ovl .gv-grid button {
  font: inherit; font-weight: 800; font-size: .95em; text-align: center; line-height: 1.25;
  padding: 12px; min-height: 4.3em; display: flex; align-items: center; justify-content: center;
  border: 2px solid #17694f; border-radius: 16px; background: #fffdf7; color: #17694f;
  cursor: pointer; letter-spacing: .01em;
  box-shadow: 0 0 0 2px rgba(255,255,255,.10), 0 4px 0 #0d4433, 0 7px 16px rgba(0,0,0,.34);
  transition: transform .08s, box-shadow .08s;
}
.gv-ovl .gv-grid button:active {
  transform: translateY(3px);
  box-shadow: 0 0 0 2px rgba(255,255,255,.10), 0 1px 0 #0d4433, 0 3px 8px rgba(0,0,0,.3);
}
/* An odd last choice keeps a SINGLE COLUMN'S width and centres in the bottom row rather than
   stretching across it (Patrick: "is it possible for the third button to be that same max width
   and centred in the bottom row"). calc(50% - 4px) is exactly one column, given the 8px gap. */
.gv-ovl .gv-grid button.wide {
  grid-column: 1 / -1; width: calc(50% - 4px); margin: 0 auto;
}
.gv-start {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%);
  font: inherit; font-weight: 700; font-size: 1.05em; padding: 12px 26px; border: 0;
  border-radius: 30px; background: var(--accent, #ef5632); color: var(--standfirst-fg, #fff);
  cursor: pointer; box-shadow: 0 8px 40px rgba(0,0,0,.6);
}
.gv-stage.playing .gv-start { display: none; }
@media (prefers-reduced-motion: reduce) {
  .gv-img, .gv-vig, .gv-spot { transition: none; }
}

/* FULL SCREEN (Patrick: "once you press Start Now would it be possible to go full screen, like our
   lightbox"). Done IN PLACE with position:fixed rather than reparenting to document.body the way the
   mg-lightbox has to: the widget stays inside the article, so the article-scoped stylesheet still
   reaches it (the lightbox self-styles inline precisely because it doesn't), and it also stays clear
   of the in-app webview's habit of stripping injected DOM. */
.guided-widget.gv-full {
  position: fixed; inset: 0; z-index: 99997; margin: 0; max-width: none;
  background: #0b0b0d; display: flex; align-items: center; justify-content: center;
}
/* The stage IS capped on a big screen, answering Patrick's own question — the zoom is computed from
   the REAL viewport, so an uncapped stage upscales the source (past ~760px the fixed zoom exceeds
   1.0 and the artwork softens) and renders one huge panel. Capped, the panel stays a comfortable
   size and MORE of the surrounding art stays visible, which is the nicer trade. A phone is under the
   cap, so it simply gets the whole screen. */
.guided-widget.gv-full .gv-stage {
  width: 100%; height: 100%; max-width: 760px; aspect-ratio: auto; border-radius: 0;
}
.gv-close {
  position: absolute; top: 8px; right: 10px; z-index: 3; display: none;
  width: 36px; height: 36px; padding: 0; line-height: 1;
  font-family: inherit; font-size: 26px; font-weight: 400;
  color: #fff; background: rgba(255,255,255,.12); border: 0; border-radius: 50%; cursor: pointer;
}
/* Pinned to the VIEWPORT, not the capped stage — on a wide screen the stage is centred with black
   either side, and a cross floating 260px in from the edge reads as part of the artwork. Same corner
   as the mg-lightbox's, so the two full-screen surfaces close the same way. */
.guided-widget.gv-full .gv-close { display: block; position: fixed; top: 14px; right: 18px; }
/* The ending's "Close" sits BESIDE Play again in a matching white pill (Patrick: "it makes sense to
   put it next to play again (or try again) in a white button style") — it was inheriting nothing and
   read as unstyled. Same pill geometry as .br-again so the pair reads as one control group. */
.gv-ovl .br-endcard { margin: 0 auto; max-width: 22em; }
.gv-endbtns { display: flex; gap: 8px; justify-content: center; align-items: center; }
.gv-ovl .br-again, .gv-ovl .gv-endclose { min-height: 0; }
.gv-ovl .gv-endclose {
  font-family: inherit; font-weight: 800; font-size: .95em; padding: 9px 20px;
  border: 1.5px solid #cfcfc8; border-radius: 24px; background: #fff; color: #444;
  cursor: pointer; box-shadow: 0 3px 0 rgba(0,0,0,.12);
  transition: transform .06s, box-shadow .06s;
}
.gv-ovl .gv-endclose:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(0,0,0,.12); }
/* Only meaningful full screen — with the panel inline there is nothing to close. */
.guided-widget:not(.gv-full) .gv-endclose { display: none; }
/* ONE restart for both renderings, in the shared header (Patrick: "keep it there in text view,
   rather than it being within text view"). Same icon treatment as the text widget's own. */
.cyoa-tabs { position: relative; }
.cyoa-restart {
  margin-left: auto; width: 30px; height: 30px; padding: 0; line-height: 28px; text-align: center;
  font-family: inherit; font-size: 16px; font-weight: 700;
  background: transparent; color: var(--accent-on-light, #6b7280);
  border: 1.5px solid currentColor; border-radius: 50%; cursor: pointer; opacity: .65;
}
.cyoa-restart:hover, .cyoa-restart:focus { opacity: 1; }

/* ==== comic-widget ==== (guided PANEL view over the printed pages, from operator-drawn boxes in
   panels.json + pages rendered by `--phases comicpages`). Sibling of the CYOA guided view and shares
   its look, but a different camera: a comic advances in READING ORDER with one scale per ROW, where
   the CYOA jumps to an arbitrary node on a stitched canvas.

   ONE set of <img>s serves both states — the page images must be in the MARKUP (viewer.html rewrites
   relative sources with a regex over the HTML, so a runtime-assigned src renders broken), and reusing
   them means no duplicate asset. This CSS is what switches their layout:
     · RESTING  — the pages side by side, i.e. the spread as printed, with Start over it.
     · .cm-full — the same images become the camera's canvas, absolutely positioned and transformed. */
.comic-widget { max-width: 34em; margin: 1em auto;
                font-family: var(--font-family-main, sans-serif); }
.cm-stage { position: relative; }
.cm-pane { display: flex; gap: 4px; }
/* min-width:0 is what makes the resting SPREAD fit. A flex item defaults to min-width:auto and
   these are replaced elements with a 1654px intrinsic width, so both pages refused to shrink and
   the second sat off-screen -- which read as "it only shows a single page" (Patrick). */
.cm-pane img { width: 100%; height: auto; flex: 1 1 0; min-width: 0; border-radius: 4px; }
.cm-start {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); z-index: 3;
  font: inherit; font-weight: 700; font-size: 1.05em; padding: 12px 26px; border: 0;
  border-radius: 30px; background: var(--accent, #ef5632); color: var(--standfirst-fg, #fff);
  cursor: pointer; box-shadow: 0 8px 40px rgba(0,0,0,.55);
}
/* Resting: no camera, no controls — just the spread and Start. */
.cm-vig, .cm-bar, .cm-close, .cm-pos { display: none; }
/* No JS (or not yet initialised): the spread stays a plain readable picture and Start is hidden. */
.comic-widget:not([data-pzi]) .cm-start { display: none; }

/* ── FULL SCREEN: the camera ── */
.comic-widget.cm-full { position: fixed; inset: 0; z-index: 99997; margin: 0; max-width: none;
                        background: #0b0b0d; }
.comic-widget.cm-full .cm-stage { position: absolute; inset: 0; overflow: hidden; }
.comic-widget.cm-full .cm-pane { display: block; position: absolute; inset: 0; }
.comic-widget.cm-full .cm-start { display: none; }
.comic-widget.cm-full .cm-pane img {
  position: absolute; top: 0; left: 0; width: auto; flex: none; border-radius: 0;
  transform-origin: 0 0; opacity: 0; transition: opacity .28s linear;
}
.comic-widget.cm-full .cm-pane img.on { opacity: 1; }
/* Along a row the camera only translates, so that move is animated; a row change or page turn is a
   fade, which is where a scale change is allowed to happen (Patrick's motion finding). */
.comic-widget.cm-full .cm-pane img.slide {
  transition: transform .42s cubic-bezier(.22,.9,.28,1), opacity .28s linear;
}
.comic-widget.cm-full .cm-vig {
  display: block; position: absolute; inset: 0; pointer-events: none;
  transition: background .42s cubic-bezier(.22,.9,.28,1);
}
.comic-widget.cm-full .cm-close {
  display: block; position: absolute; z-index: 3; border: 0; cursor: pointer; color: #fff;
  background: rgba(255,255,255,.12); font-family: inherit;
}
/* The control BAR floats below the focused panel (the JS sets `top`) instead of arrows pinned to the
   screen edges, which were barely visible on the artwork. Green with a faint white outer stroke, the
   same treatment as the CYOA's choice buttons and for the same reason: they sit on near-black, where a
   dark brand accent would disappear. */
/* FIXED near the bottom, in thumb reach, and it NEVER MOVES (Patrick: "have the buttons in a fixed
   position at the bottom of the screen... and don't move them based on the size of the focus (at the
   moment they are jumping)"). Tracking the panel meant the bar hopped on every step, since the panel
   height changes. The JS reserves exactly the space above the bar by MEASURING it, so the two can
   never disagree about where the panel may sit — the same constant-band idea as the CYOA. */
.comic-widget.cm-full .cm-bar {
  display: flex; position: absolute; left: 0; right: 0; top: auto; z-index: 3;
  bottom: var(--cm-bar-bottom, 104px);
  justify-content: center; gap: 10px; padding: 0 12px;
}
/* A short screen (or landscape) cannot spare 104px below the bar. */
@media (max-height: 620px) {
  .comic-widget.cm-full .cm-bar { bottom: 42px; }
}
.comic-widget.cm-full .cm-bar button {
  font-family: inherit; font-weight: 800; font-size: .95em; letter-spacing: .01em;
  min-width: 8.5em; padding: 13px 18px; cursor: pointer;
  border: 2px solid #17694f; border-radius: 16px; background: #fffdf7; color: #17694f;
  box-shadow: 0 0 0 2px rgba(255,255,255,.10), 0 4px 0 #0d4433, 0 7px 16px rgba(0,0,0,.34);
  transition: transform .08s, box-shadow .08s;
}
.comic-widget.cm-full .cm-bar button:active {
  transform: translateY(3px);
  box-shadow: 0 0 0 2px rgba(255,255,255,.10), 0 1px 0 #0d4433, 0 3px 8px rgba(0,0,0,.3);
}
.comic-widget.cm-full .cm-close { top: 14px; right: 18px; width: 36px; height: 36px;
                                  border-radius: 50%; font-size: 26px; line-height: 1; }
.comic-widget.cm-full .cm-pos {
  display: block; position: absolute; left: 0; right: 0; bottom: 10px; margin: 0; text-align: center;
  color: #cbd5cf; font-size: 12px; font-family: var(--font-family-main, sans-serif);
}
@media (prefers-reduced-motion: reduce) {
  .comic-widget.cm-full .cm-pane img,
  .comic-widget.cm-full .cm-pane img.slide,
  .comic-widget.cm-full .cm-vig { transition: opacity .01s linear; }
}

/* IN-APP: the reader injects a ~90px header bar that appears on tap/scroll-up, and our full-screen
   close crosses sat underneath it (Patrick). `body.connect-mobile-edition` is the in-app class the
   landscape reading-width overrides already key off, so the nudge applies only where that bar exists —
   in a browser, the viewer or QA the crosses stay at the top corner. */
body.connect-mobile-edition .guided-widget.gv-full .gv-close,
body.connect-mobile-edition .comic-widget.cm-full .cm-close { top: 104px; }
/* The step readout is gone from the comic view; keep the rule harmless if an older article ships it. */
.comic-widget.cm-full .cm-pos:empty { display: none; }

/* ── Immersive full screen INSIDE the web reader (`pz-inhost`) ──────────────────────────────────
   Both immersive views also carry `pz-full` (one hook for a host, whatever the widget), and when the
   web reader's `.articleOverlayContainer` is present carousel.js adds `pz-inhost` — see pzFullEnter.
   THE APP IS UNTOUCHED: that container only exists in the reader, so nothing here can match in-app
   or in QA, and `gv-full`/`cm-full` keep their existing `position: fixed` there.

   Why it is needed (Ryan, 2026-08-11 — "on web, tapping Start just loads a black popup, the page is
   not there"): `position: fixed` resolves against the nearest positioned/transformed/filtered
   ancestor, not always the viewport. In the reader our article sits inside a transformed page
   container, so the fixed stage was sized against THAT box — the black backdrop painted, the stage
   measured 0-ish and no artwork appeared. `absolute` + an explicit viewport height puts it back in a
   box the camera arithmetic can measure. Both widgets compute their zoom from the stage's live
   clientWidth/Height, so they simply fit the new box; no JS maths changes. */
.pz-full.pz-inhost { position: absolute; inset: 0 0 auto 0; height: 100vh; min-height: 100vh; }
/* The CYOA cross is the one child pinned to the VIEWPORT with `fixed` (the comic's is already
   absolute), so it needs the same treatment or it floats over the reader's own chrome. */
.pz-full.pz-inhost .gv-close { position: absolute; }
/* The reader's own container: its side peeks can go fully black while a view is immersive (Ryan's
   request). Only ever set by the reader path, and removed on exit. */
.articleOverlayContainer.ol-full { background: #000; }

/* ── COVER PAGE: full-picture cover with tap-to-jump hotspots (Patrick 2026-07-28) ─────────────────
   The cover ships as a generated full-page picture article (article_1-1) — the vendor's crop is
   unreliable (Empire's misses its right edge) and the pipeline's coverline-text page "looks a bit
   weird" — with each cover.json feature overlaid as a tappable area. The highlight is the
   "marching ants" treatment Patrick asked for: a light segment zipping around the bounding box
   (an inline SVG rect animated by stroke-dashoffset — static markup, no JS, so the in-app
   webview's DOM-stripping can't remove it). A faint constant outline keeps the bounds readable
   between passes of the light. */
.cover-page { position: relative; margin: 0 calc(-1 * var(--side-pad)); }
.cover-page > img { display: block; width: 100%; height: auto; }
.cover-hotspot { position: absolute; display: block; z-index: 2; } /* MUST beat .cover-full's z-index:1 — without this a non-anim cover's img paints OVER the hotspots (invisible + untappable; the WS/AMB bug) */
.cover-hotspot svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* rounded corners (rx as a CSS property so the markup stays plain); non-scaling stroke keeps the
   line weight even though the 100x100 viewBox is stretched to the box's real aspect */
.cover-hotspot rect {
  fill: none; vector-effect: non-scaling-stroke; rx: 3;
  stroke: rgba(255,255,255,0.35); stroke-width: 1.5;
}
/* ONE lap of the light, then EVERYTHING fades to 0 (Patrick round 3: "one round of marching ants
   would be enough — all we're trying to do is show people things are tappable, they can tap from
   there. Then they fade back to 0"). The hotspots stay tappable, just invisible.
   FLASH pacing (Patrick 2026-07-30): half-time lap (1.6s), fade straight after — "literally like
   a flash to show you this page has interactivity". */
.cover-hotspot rect {
  animation: cover-ants-out 0.5s ease 1.9s forwards;          /* the faint outline goes too */
}
.cover-hotspot rect.ants {
  stroke: rgba(255,255,255,0.95); stroke-width: 2;
  stroke-dasharray: 14 86; stroke-dashoffset: 0;
  animation: cover-ants 1.6s linear 1, cover-ants-out 0.5s ease 1.6s forwards;
  filter: drop-shadow(0 0 3px rgba(255,255,255,0.8));
}
@keyframes cover-ants { to { stroke-dashoffset: -100; } }
@keyframes cover-ants-out { to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  /* no motion: a steady outline that stays — fading it without the show would leave nothing */
  .cover-hotspot rect { animation: none; }
  .cover-hotspot rect.ants { animation: none; stroke-dasharray: none; stroke: rgba(255,255,255,0.7); }
}
/* ── the COVER REVEAL (opt-in, --cover-anim): the clean base ARTWORK alone, the full cover
   (coverlines) crossfading in over it, then the ants run. The base is positioned by its true
   placement bbox so it aligns under the page render; everything is static markup + CSS delays —
   webview-safe, no JS. */
.cover-page .cover-base { position: absolute; z-index: 0; }
.cover-page .cover-full { position: relative; z-index: 1; }
/* timing (Patrick 2026-07-29): let the artwork breathe a second longer (2.1s), and fade the
   coverlines up more slowly (3s). Everything after shifts with it. */
.cover-anim .cover-full { animation: cover-reveal 3s ease 2.1s both; }
@keyframes cover-reveal { from { opacity: 0; } to { opacity: 1; } }
.cover-anim .cover-hotspot { z-index: 2; opacity: 0; animation: cover-reveal 0.4s ease 4.3s both; }
/* the flash rides the settle: outlines appear as the crossfade lands (4.3s), the half-time lap
   runs 4.6-6.2s — "in a second quicker, once the cover settles" — and everything fades after */
.cover-anim .cover-hotspot rect { animation-delay: 6.4s; }
.cover-anim .cover-hotspot rect.ants { animation-delay: 4.6s, 6.2s; }
@media (prefers-reduced-motion: reduce) {
  .cover-anim .cover-full, .cover-anim .cover-hotspot { animation: none; opacity: 1; }
}

/* Contents card SECTION LABEL — the printed page banner ("Women's Six Nations", "News"), read
   from the Eyebrow phase-3 already captures. Patrick, on The Rugby Paper: "you could see at a
   glance the section the round up is from... really useful if you're looking in rugby paper and
   are wanting to see 'Championship' 'Men's Six Nations'". A LABEL, deliberately not a contents
   SECTION — measured, grouping by it gives ~10 groups of one article each. */
.toc-seclabel {
    display: block;
    font-family: var(--font-family-main);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .11em;
    text-transform: uppercase;
    color: var(--accent-on-light, var(--accent, #666));
    margin-bottom: 3px;
}


/* ==== FONT MAP (generated from font_map.json — per-role print fonts via the
   curated proxy table; a title override later in this file still wins) ==== */
:root{--font-family-main-display:"Roboto", sans-serif;}
.article-standfirst,.article-full-lead,.hero-lead{font-family:"Playfair Display", Georgia, serif;}
.article-hero .hero-lead.hero-lead{font-family:"Playfair Display", Georgia, serif;}
.article-subhead{font-family:"Inter", 'Helvetica Neue', sans-serif;}
:root{--font-family-main:"Source Serif 4", Georgia, serif;}
p.article-pull-quote,.article-pull-quote{font-family:"Inter", 'Helvetica Neue', sans-serif;}
.article-full-byline,.hero-byline{font-family:"Playfair Display", Georgia, serif;}
.article-hero .hero-byline.hero-byline{font-family:"Playfair Display", Georgia, serif;}
.articleImageCaption{font-family:"Inter", 'Helvetica Neue', sans-serif;}
:root{--font-family-condensed:"Playfair Display", Georgia, serif;--font-family-secondary:"Playfair Display", Georgia, serif;}
.ff-acta.ff-acta{font-family:"Source Serif 4", Georgia, serif;}
.ff-actadisplay.ff-actadisplay{font-family:"Playfair Display", Georgia, serif;}
.ff-actaheadline.ff-actaheadline{font-family:"Playfair Display", Georgia, serif;}
.ff-actaposter.ff-actaposter{font-family:"Playfair Display", Georgia, serif;}
.ff-aeonik.ff-aeonik{font-family:"Inter", 'Helvetica Neue', sans-serif;}
.ff-gotham.ff-gotham{font-family:"Montserrat", 'Helvetica Neue', sans-serif;}
.ff-poppins.ff-poppins{font-family:"Poppins", sans-serif;}
.ff-roboto.ff-roboto{font-family:"Roboto", sans-serif;}
.ff-tuskergrotesk.ff-tuskergrotesk{font-family:"Archivo", 'Helvetica Neue', sans-serif;}
.fw-300.fw-300.fw-300{font-weight:300;}
.fw-400.fw-400.fw-400{font-weight:400;}
.fw-500.fw-500.fw-500{font-weight:500;}
.fw-700.fw-700.fw-700{font-weight:700;}
.fw-800.fw-800.fw-800{font-weight:800;}
.fw-900.fw-900.fw-900{font-weight:900;}
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 300;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/roboto/v51/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFkD-vYSZviVYUb_rj3ij__anPXDTnogkk7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: italic;
  font-weight: 500;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFkD-vYSZviVYUb_rj3ij__anPXDTnogkk7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: italic;
  font-weight: 600;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFkD-vYSZviVYUb_rj3ij__anPXDTnogkk7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFkD-vYSZviVYUb_rj3ij__anPXDTnogkk7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: italic;
  font-weight: 800;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFkD-vYSZviVYUb_rj3ij__anPXDTnogkk7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: italic;
  font-weight: 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/playfairdisplay/v40/nuFkD-vYSZviVYUb_rj3ij__anPXDTnogkk7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUM.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUM.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: italic;
  font-weight: 500;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUM.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUM.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: italic;
  font-weight: 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/inter/v20/UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk6L5UUM.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/sourceserif4/v14/vEFH2_tTDB4M7-auWDN0ahZJW1ge6NmXpVAHV83Bfb_US0r6aX0.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: italic;
  font-weight: 600;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/sourceserif4/v14/vEFH2_tTDB4M7-auWDN0ahZJW1ge6NmXpVAHV83Bfb_US0r6aX0.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/sourceserif4/v14/vEFH2_tTDB4M7-auWDN0ahZJW1ge6NmXpVAHV83Bfb_US0r6aX0.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/sourceserif4/v14/vEFF2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6kDXr4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/sourceserif4/v14/vEFF2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6kDXr4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/sourceserif4/v14/vEFF2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6kDXr4.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm459Wlhyw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm459Wlhyw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm459Wlhyw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm459Wlhyw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm459Wlhyw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Poppins';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiGyp8kv8JHgFVrJJLucHtA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: italic;
  font-weight: 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiDyp8kv8JHgFVrJJLm81xVF9eO.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 100;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiGyp8kv8JHgFVrLPTucHtA.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 200;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLDD4Z1xlFQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v24/pxiByp8kv8JHgFVrLBT5Z1xlFQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/archivo/v25/k3kPo8UDI-1M0wlSV9XAw6lQkqWY8Q82sLydOxI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/archivo/v25/k3kPo8UDI-1M0wlSV9XAw6lQkqWY8Q82sLydOxI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/archivo/v25/k3kPo8UDI-1M0wlSV9XAw6lQkqWY8Q82sLydOxI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/archivo/v25/k3kPo8UDI-1M0wlSV9XAw6lQkqWY8Q82sLydOxI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 800;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/archivo/v25/k3kPo8UDI-1M0wlSV9XAw6lQkqWY8Q82sLydOxI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 900;
  font-stretch: 100%;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/archivo/v25/k3kPo8UDI-1M0wlSV9XAw6lQkqWY8Q82sLydOxI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


/** per-ROLE weight baseline from the print fonts (the per-element .fw-* classes
    from the font map override this wherever an element's own weight was read) **/
.article-full-headline,h2.article-full-headline,.article-hero .hero-headline{font-weight:300;}
.article-full-lead,.article-hero .hero-lead{font-weight:700;}
.article-standfirst,.article-hero .article-standfirst{font-weight:700;}
.article-subhead,.article-boxout .article-subhead{font-weight:700;}
.article-full-body{font-weight:400;}
p.article-pull-quote,.article-pull-quote{font-weight:500;}
.article-full-byline,.article-hero .hero-byline{font-weight:300;}
.articleImageCaption,.article-in-image .articleImageCaption{font-weight:400;}


/* ==== CyclingPlus-overrides.css (title overrides) ==== */
/** Cycling Plus — title overrides for base.css
 *
 * Print system (font_map.json, extracted from the PDF): Neue Haas Grotesk (DS/TX) for the
 * display tier — headlines (62pt), standfirsts, bylines, captions — and Kepler (an italic
 * serif display) for subheads, pull quotes and body. Field Gothic (condensed) is a feature
 * accent voice on some openers. The fontmap already maps the FAMILIES (Haas→Inter,
 * Kepler→Lora, FieldGothic→Oswald); this override adds the WEIGHT/tracking STRUCTURE that
 * makes them read like the magazine instead of default-weight "boring" text (Patrick, cycp
 * QA 2026-07-21 — same lesson as NGT: the special-ness is mostly weight, not face).
 *
 * Only applies when base.css is compiled with this overrides file (the cycp title).
 */

:root {
    --font-family-main:         "Lora", Georgia, serif;
    --font-family-secondary:    "Inter", "Helvetica Neue", sans-serif;
    --font-family-condensed:    "Inter", "Helvetica Neue", sans-serif;
    --font-family-main-display: "Inter", "Helvetica Neue", sans-serif;

    --headline-transform:       none;
    --headline-weight:          800;
    --headline-line-height:     1.05;

    --body-line-height:         1.55;
}

:root { --headline-font-serif: "Lora", Georgia, serif;
        --headline-font-sans: "Inter", "Helvetica Neue", sans-serif; }

/* Headlines print in Haas Grotesk DS Medium at big display sizes — the digital equivalent
   of that dense grotesk block is Inter heavy with tightened tracking, not Inter 400. */
.article-full-headline, .hero-headline {
    font-weight: 800; letter-spacing: -0.015em;
}

/* Standfirst = Haas DS Medium in print (a size up from body, medium-weight). */
.article-standfirst, .article-full-lead, .hero-lead {
    font-weight: 500; letter-spacing: 0;
}

/* Bylines print BOLD Haas TX, small — bold + a touch of tracking keeps them crisp. */
.article-full-byline, .hero-byline {
    font-weight: 700; letter-spacing: 0.02em;
}

/* Subheads are Kepler ITALIC display in print (the 3c per-heading italic read stamps the
   genuinely-italic ones); give the tier its display weight + size step so it reads as a
   voice change from the body, not just another paragraph. */
p.article-subhead {
    font-weight: 600; font-size: 1.3em; line-height: 1.25;
}

/* Pull quotes = Kepler Medium Italic at display size, clean (no rules) in print. */
p.article-pull-quote, .article-pull-quote {
    font-style: italic; font-weight: 500;
    border-top: none; border-bottom: none;
}
.article-pull-quote-credit { font-family: var(--font-family-secondary); }

/* Dropcap in the grotesk display voice. */
p.dropcap::first-letter { font-family: var(--font-family-main-display); font-weight: 800; }

/* Captions: Haas TX Medium, small + quiet. */
.articleImageCaption { font-weight: 500; }

/* retrofit: constrain small images that look silly full-width (Patrick) — block, indented to the body column, no float (legacy sibling order makes a float wrap raggedly). */
.article-in-image.retro-headshot { float: none; width: 88px; margin: 12px 0 12px calc(var(--side-pad) + var(--read-inset, 0px)); }
.article-in-image.retro-headshot img { width: 88px; height: 88px; border-radius: 50%; object-fit: cover; }
.article-in-image.retro-crest { float: none; width: 64px; margin: 10px 0 10px calc(var(--side-pad) + var(--read-inset, 0px)); }
.article-in-image.retro-crest img { width: 64px; height: 64px; object-fit: contain; }
.article-in-image.retro-book { float: none; width: auto; max-width: 180px; margin: 12px 0 12px calc(var(--side-pad) + var(--read-inset, 0px)); }
.article-in-image.retro-book img { width: auto; max-width: 180px; height: auto; }
.article-in-image.retro-headshot .articleImageCaption,.article-in-image.retro-crest .articleImageCaption { display: none; }

/* retrofit: vendor head-block colours kept for the dark scrim (hl-keep) revert on a light ground */
@media (min-width: 640px) and (max-resolution: 2.49dppx) {
  .article-hero--scrim:not(.article-hero--desktop) .hl-keep { color: var(--second-color) !important; }
}
.article-hero--panel-light .hl-keep { color: var(--panel-fg, #1a1a1a) !important; }

/* PDF-lite: per-article print colours (retrofit) */
body.retro-article_10-1{--accent:#dd3634 !important;--accent-dark:#dd3634 !important;--standfirst-fg:#fff !important;--standfirst-bg:#dd3634 !important;--accent-on-light:#dd3634 !important;--pull-quote-color:#dd3634 !important;--pull-quote-border-color:#dd3634 !important;--boxout-border-color:#dd3634 !important;}
body.retro-article_100-1{--panel-bg:#ffffff !important;--panel-fg:#111111 !important;}
body.retro-article_102-1 p.article-standfirst{background:transparent !important;color:#1a1a1a !important;padding-left:0 !important;padding-right:0 !important;}
body.retro-article_102-1 .article-pull-quote,body.retro-article_102-1 .article-callout{font-style:normal !important;}
body.retro-article_106-1{--accent:#dd3634 !important;--accent-dark:#dd3634 !important;--standfirst-fg:#fff !important;--standfirst-bg:#dd3634 !important;--accent-on-light:#dd3634 !important;--pull-quote-color:#dd3634 !important;--pull-quote-border-color:#dd3634 !important;--boxout-border-color:#dd3634 !important;}
body.retro-article_14-1{--accent:#dd3634 !important;--accent-dark:#dd3634 !important;--standfirst-fg:#fff !important;--standfirst-bg:#dd3634 !important;--accent-on-light:#dd3634 !important;--pull-quote-color:#dd3634 !important;--pull-quote-border-color:#dd3634 !important;--boxout-border-color:#dd3634 !important;}
body.retro-article_15-1{--accent:#dd3634 !important;--accent-dark:#dd3634 !important;--standfirst-fg:#fff !important;--standfirst-bg:#dd3634 !important;--accent-on-light:#dd3634 !important;--pull-quote-color:#dd3634 !important;--pull-quote-border-color:#dd3634 !important;--boxout-border-color:#dd3634 !important;}
body.retro-article_18-1{--accent:#dd3634 !important;--accent-dark:#dd3634 !important;--standfirst-fg:#fff !important;--standfirst-bg:#dd3634 !important;--accent-on-light:#dd3634 !important;--pull-quote-color:#dd3634 !important;--pull-quote-border-color:#dd3634 !important;--boxout-border-color:#dd3634 !important;}
body.retro-article_19-1{--accent:#284878 !important;--accent-dark:#284878 !important;--standfirst-fg:#fff !important;--standfirst-bg:#284878 !important;--accent-on-light:#284878 !important;--pull-quote-color:#284878 !important;--pull-quote-border-color:#284878 !important;--boxout-border-color:#284878 !important;}
body.retro-article_22-1{--accent:#dd3634 !important;--accent-dark:#dd3634 !important;--standfirst-fg:#fff !important;--standfirst-bg:#dd3634 !important;--accent-on-light:#dd3634 !important;--pull-quote-color:#dd3634 !important;--pull-quote-border-color:#dd3634 !important;--boxout-border-color:#dd3634 !important;}
body.retro-article_22-1 .article-pull-quote,body.retro-article_22-1 .article-callout{font-style:normal !important;}
body.retro-article_28-1{--accent:#c00007 !important;--accent-dark:#c00007 !important;--standfirst-fg:#fff !important;--standfirst-bg:#c00007 !important;--accent-on-light:#c00007 !important;--pull-quote-color:#c00007 !important;--pull-quote-border-color:#c00007 !important;--boxout-border-color:#c00007 !important;}
body.retro-article_28-1 .article-pull-quote,body.retro-article_28-1 .article-callout{font-style:normal !important;}
body.retro-article_36-1 p.article-standfirst{background:transparent !important;color:#1a1a1a !important;padding-left:0 !important;padding-right:0 !important;}
body.retro-article_36-1 .article-pull-quote,body.retro-article_36-1 .article-callout{font-style:normal !important;}
body.retro-article_36-1 p.dropcap::first-letter{color:#c0322a !important;}
body.retro-article_36-1{--panel-bg:#ffffff !important;--panel-fg:#111111 !important;}
body.retro-article_45-1 p.article-standfirst{background:transparent !important;color:#1a1a1a !important;padding-left:0 !important;padding-right:0 !important;}
body.retro-article_45-1 .article-pull-quote,body.retro-article_45-1 .article-callout{font-style:normal !important;}
body.retro-article_52-1 .article-pull-quote,body.retro-article_52-1 .article-callout{font-style:normal !important;}
body.retro-article_6-1 p.article-standfirst{background:transparent !important;color:#1a1a1a !important;padding-left:0 !important;padding-right:0 !important;}
body.retro-article_62-1 .article-pull-quote,body.retro-article_62-1 .article-callout{font-style:normal !important;}
body.retro-article_74-1{--panel-bg:#ffffff !important;--panel-fg:#111111 !important;}
body.retro-article_90-1{--accent:#e35900 !important;--accent-dark:#e35900 !important;--standfirst-fg:#fff !important;--standfirst-bg:#e35900 !important;--accent-on-light:#e35900 !important;--pull-quote-color:#e35900 !important;--pull-quote-border-color:#e35900 !important;--boxout-border-color:#e35900 !important;}
body.retro-article_90-1 .article-pull-quote,body.retro-article_90-1 .article-callout{font-style:normal !important;}
body.retro-article_90-1{--panel-bg:#ffffff !important;--panel-fg:#111111 !important;}

/* -- inline-style utility classes (externalized: the reader strips style attrs) -- */
.s-02ac0379{top:88.7% !important;left:41.2% !important;width:21.1% !important;height:8.0% !important;}
.s-05595633{width:calc(var(--sc-h) * 0.9444) !important;}
.s-060c48e1{object-position:center 63.1% !important;}
.s-0614f811{color:#0589d2 !important;}
.s-06278c4d{top:88.7% !important;left:7.1% !important;width:21.9% !important;height:8.0% !important;}
.s-099a0b01{color: rgb(68, 68, 67) !important;}
.s-0d98efec{object-position:center 41.7% !important;}
.s-0e86e588{object-position:center 51.3% !important;}
.s-165c1f14{width:calc(var(--sc-h) * 1.4569) !important;}
.s-17e466ce{color:#c00007 !important;}
.s-17e84a4e{object-position:center 42.1% !important;}
.s-1c65c7a4{color: rgb(227, 89, 0) !important;}
.s-20223968{width:calc(var(--sc-h) * 1.3386) !important;}
.s-20304103{object-position:center 54.1% !important;}
.s-20dcde07{color: rgb(138, 138, 138) !important;}
.s-22802ca0{color: rgb(143, 150, 155) !important;}
.s-246c0872{object-position:center 48.4% !important;}
.s-26d9e1c8{object-position:center 53.4% !important;}
.s-2be52eeb{object-position:center 56.5% !important;}
.s-30c854df{color:#9b0c67 !important;}
.s-34bbeb17{object-position:center 43.7% !important;}
.s-35cdcf0d{color:#855041 !important;}
.s-35ff3380{object-position:center 57.1% !important;}
.s-39475b2a{width:calc(var(--sc-h) * 0.9390) !important;}
.s-3a35e5ee{object-position:center 48.2% !important;}
.s-3f5ab1a2{width:calc(var(--sc-h) * 1.4791) !important;}
.s-4025f601{color:#e35900 !important;}
.s-44034fa0{width:calc(var(--sc-h) * 0.8740) !important;}
.s-480f04be{color: rgb(116, 116, 115) !important;}
.s-4c7b26bc{width:calc(var(--sc-h) * 0.7919) !important;}
.s-50c09ce8{width:calc(var(--sc-h) * 0.7110) !important;}
.s-541fdf3b{top:53.1% !important;left:6.2% !important;width:20.3% !important;height:7.8% !important;}
.s-54ad2a2f{top:39.0% !important;left:2.0% !important;width:96.0% !important;height:29.0% !important;}
.s-54f9e14c{color: rgb(52, 56, 57) !important;}
.s-566913b8{color: rgb(102, 102, 102) !important;}
.s-5a083128{color: rgb(192, 50, 42) !important;}
.s-6108e953{object-position:center 57.8% !important;}
.s-62a91a33{color: rgb(192, 0, 7) !important;}
.s-635c29e2{object-position:center 58.0% !important;}
.s-677629ba{width:calc(var(--sc-h) * 0.8246) !important;}
.s-690c4dc0{color:#e15000 !important;}
.s-6ddeb1d5{width:calc(var(--sc-h) * 0.6237) !important;}
.s-6f30ba34{color:#111111 !important;}
.s-7056fcf2{width:calc(var(--sc-h) * 0.7877) !important;}
.s-719ec092{object-position:center 49.9% !important;}
.s-71ef4b7c{color: rgb(172, 80, 0) !important;}
.s-7b0eee10{color: rgb(210, 0, 8) !important;}
.s-7fb14728{width:calc(var(--sc-h) * 1.5377) !important;}
.s-868a2b03{width:calc(var(--sc-h) * 1.0335) !important;}
.s-86c6de93{object-position:center 57.0% !important;}
.s-8be2dfa2{color: rgb(221, 54, 52) !important;}
.s-8cd89c22{color: rgb(5, 137, 210) !important;}
.s-90896a03{color: rgb(228, 97, 81) !important;}
.s-925b9768{color: rgb(31, 31, 31) !important;}
.s-94b599d2{width:calc(var(--sc-h) * 1.8000) !important;}
.s-985f4240{width:calc(var(--sc-h) * 1.7259) !important;}
.s-98acf2ab{object-position:center 49.7% !important;}
.s-9ae8ec5c{object-position:center 50.1% !important;}
.s-9be287c3{color: rgb(214, 2, 8) !important;}
.s-9f709cee{width:calc(var(--sc-h) * 1.5977) !important;}
.s-a8012291{top:3.0% !important;left:5.8% !important;width:74.2% !important;height:6.5% !important;}
.s-abe17ae5{color: rgb(0, 0, 0) !important;}
.s-abe71872{color:#004958 !important;}
.s-acd096b3{color: rgb(155, 12, 103) !important;}
.s-ace7ae3b{width:calc(var(--sc-h) * 0.7447) !important;}
.s-ba58d2e0{color: rgb(44, 43, 43) !important;}
.s-bae7f905{width:calc(var(--sc-h) * 1.5748) !important;}
.s-bb11fcf2{object-position:center 56.0% !important;}
.s-bc24a6f0{color: rgb(225, 80, 0) !important;}
.s-c136c765{color: rgb(164, 177, 152) !important;}
.s-c1c05271{object-position:center 55.0% !important;}
.s-c426dea9{color:#c0322a !important;}
.s-c62cde92{color:#d60208 !important;}
.s-ca395304{object-position:center 48.7% !important;}
.s-ce11f2f6{top:70.0% !important;left:9.4% !important;width:15.4% !important;height:10.8% !important;}
.s-ce40464b{color: rgb(133, 80, 65) !important;}
.s-d235fe72{--sa:#c00007 !important;--sa-on-bg:#ffffff !important;--sa-text:#c00007 !important;}
.s-d34a8f5c{font-size:0.43em !important;}
.s-d568f128{top:88.7% !important;left:70.7% !important;width:25.6% !important;height:8.0% !important;}
.s-d8d1e0d0{color: rgb(7, 13, 14) !important;}
.s-d9c89e0c{color: rgb(91, 91, 90) !important;}
.s-dd29aad3{width:calc(var(--sc-h) * 1.2755) !important;}
.s-dec1bcdd{width:calc(var(--sc-h) * 1.5947) !important;}
.s-df5925df{color: rgb(0, 73, 88) !important;}
.s-e3bdf2bf{left:0 !important;top:0 !important;width:100% !important;height:100% !important;}
.s-e79a7ebe{color: rgb(17, 98, 114) !important;}
.s-e9f628d1{width:calc(var(--sc-h) * 0.7034) !important;}
.s-ed12dbe0{width:calc(var(--sc-h) * 0.8116) !important;}
.s-f669d39d{color: rgb(18, 18, 17) !important;}
.s-fcb12374{width:calc(var(--sc-h) * 1.0844) !important;}
.s-fd262100{width:calc(var(--sc-h) * 1.2041) !important;}
.s-fe5b55f6{width:calc(var(--sc-h) * 1.3393) !important;}
/* -- dark-mode companions for the above -- */
.darktheme .s-0614f811{color:#f0f0f0 !important;}
.darktheme .s-099a0b01{color:#f0f0f0 !important;}
.darktheme .s-17e466ce{color:#f0f0f0 !important;}
.darktheme .s-1c65c7a4{color:#f0f0f0 !important;}
.darktheme .s-20dcde07{color:rgb(138, 138, 138) !important;}
.darktheme .s-22802ca0{color:rgb(143, 150, 155) !important;}
.darktheme .s-30c854df{color:#f0f0f0 !important;}
.darktheme .s-35cdcf0d{color:#f0f0f0 !important;}
.darktheme .s-4025f601{color:#f0f0f0 !important;}
.darktheme .s-480f04be{color:#f0f0f0 !important;}
.darktheme .s-54f9e14c{color:#f0f0f0 !important;}
.darktheme .s-566913b8{color:#f0f0f0 !important;}
.darktheme .s-5a083128{color:#f0f0f0 !important;}
.darktheme .s-62a91a33{color:#f0f0f0 !important;}
.darktheme .s-690c4dc0{color:#f0f0f0 !important;}
.darktheme .s-6f30ba34{color:#f0f0f0 !important;}
.darktheme .s-71ef4b7c{color:#f0f0f0 !important;}
.darktheme .s-7b0eee10{color:#f0f0f0 !important;}
.darktheme .s-8be2dfa2{color:#f0f0f0 !important;}
.darktheme .s-8cd89c22{color:#f0f0f0 !important;}
.darktheme .s-90896a03{color:rgb(228, 97, 81) !important;}
.darktheme .s-925b9768{color:#f0f0f0 !important;}
.darktheme .s-9be287c3{color:#f0f0f0 !important;}
.darktheme .s-abe17ae5{color:#f0f0f0 !important;}
.darktheme .s-abe71872{color:#f0f0f0 !important;}
.darktheme .s-acd096b3{color:#f0f0f0 !important;}
.darktheme .s-ba58d2e0{color:#f0f0f0 !important;}
.darktheme .s-bc24a6f0{color:#f0f0f0 !important;}
.darktheme .s-c136c765{color:rgb(164, 177, 152) !important;}
.darktheme .s-c426dea9{color:#f0f0f0 !important;}
.darktheme .s-c62cde92{color:#f0f0f0 !important;}
.darktheme .s-ce40464b{color:#f0f0f0 !important;}
.darktheme .s-d8d1e0d0{color:#f0f0f0 !important;}
.darktheme .s-d9c89e0c{color:#f0f0f0 !important;}
.darktheme .s-df5925df{color:#f0f0f0 !important;}
.darktheme .s-e79a7ebe{color:#f0f0f0 !important;}
.darktheme .s-f669d39d{color:#f0f0f0 !important;}
