/**
 * cw-article.css — ContentWorks Article Template v1
 *
 * One stylesheet for every WordPress single-post page. Served at
 * /assets/cw-article.css and injected into WP single-post <head> by
 * worker.js (HTMLRewriter). Every rule is scoped under `body.single-post`
 * so it can only ever affect the blog template, never the static
 * framework pages (those load cw-design-system.css instead).
 *
 * Governs: decisions/2026-09-29-cw-article-template-spec.md §A.
 * Takes over the per-post `CW-READABILITY-CSS-TEST` inline block that
 * ~100+ live posts already carry in their body (that block stays in
 * place — harmless, and largely superseded by this file's higher
 * specificity — see the spec's Build notes for why source order alone
 * would not be enough).
 *
 * Brand tokens (sites/contentworks/build/DESIGN-SYSTEM.md,
 * brand-assets/design.md):
 *   --cw-teal-text  #00717f   text/links on light (~5.7:1 on white)
 *   --cw-h2         #08777d   H2 colour (~5.3:1 on white)
 *   --cw-orange     #f1770a   the ONE primary button
 *   --cw-dark       #082729   brand dark — also the orange button's TEXT
 *                              colour (see "Orange button text" below)
 *   --cw-peach      #ffc693   soft accent on dark
 *   --cw-ink        #0c1416   body text on light
 *   --cw-muted      #5a6b6d   secondary text
 *
 * Fonts: Inter Tight (headings), Inter (body) — per DESIGN-SYSTEM.md /
 * design.md, same pairing the static framework pages use.
 *
 * Font loading (fix 3, independent review): this file used to pull the Google Fonts
 * stylesheet via a CSS `@import` right here. A CSS @import is render-blocking — every
 * WordPress page carrying the injected <link> to THIS file (which is every WP page,
 * not only single-post ones — see worker.js's own comment) had to wait on that fetch
 * before the browser could apply anything in this stylesheet at all. The @import is
 * gone; worker.js's ArticleCssInjector now loads the same Google Fonts stylesheet as a
 * non-blocking <link rel="stylesheet" media="print" onload="…"> instead (the standard
 * loadCSS pattern), with a <link rel="preconnect"> pair ahead of it. Nothing in this
 * file changes: the font-family values below are unchanged, they just resolve once the
 * separately-loaded stylesheet lands instead of blocking this one.
 */

/* JetMenu's own slide-out mobile menu (what real phones get: the plugin swaps in
   .jet-mobile-menu on a mobile user-agent, found on the live read-back 30 Sep).
   Its links shipped as #0caab3 on white (~2.8:1, fails AA) at 22px tall. Darken to
   the brand text teal (5.73:1) and give each row a 44px tap height. */
.jet-mobile-menu .jet-mobile-menu__items a.mobile-link {
  color: #00717f !important;
  min-height: 44px;
  display: flex;
  align-items: center;
  font-weight: 600;
}

/* =========================================================================
 * WP HEADER — mobile nav collapse (Round 3, Dan's mobile screenshots)
 *
 * NOT scoped to body.single-post. The broken header is Elementor's own
 * site-wide header (data-elementor-id 10537), not part of the article
 * template — it renders on every WordPress page, so the fix has to reach
 * every WordPress page too. Selectors below target the header markup
 * directly. This CANNOT reach the static framework pages: those are served
 * straight from env.ASSETS.fetch and never load this file at all (see
 * worker.js's isStatic()/STATIC_EXACT — this stylesheet's <link> is only
 * ever injected on WordPress-proxied responses). Desktop is untouched: every
 * rule here sits inside `@media (max-width: 768px)`.
 *
 * ROOT CAUSE (verified 29 Sep 2026 by reading the live plugin files, not
 * guessed): the visible header nav ("Who We Help / How It Works / What You
 * Get / Pricing / Results / Resources") is NOT GeneratePress's own menu —
 * it's Crocoblock's JetMenu plugin (a "jet-mega-menu" widget) placed inside
 * an Elementor header template. JetMenu ships NO @media query anywhere in
 * its own stylesheet (jet-menu/assets/public/css/public.css, fetched and
 * read in full) to hide the horizontal link list or collapse it into a
 * toggle at narrow widths. That switch is done entirely in
 * jet-menu-public-scripts.js: on load and on resize it runs
 * matchMedia('(max-width: 767px)') and ADDS one of three classes to the
 * widget — .jet-mega-menu--layout-horizontal, --layout-vertical or
 * --layout-dropdown. Only THEN does the plugin's own CSS do anything: the
 * toggle is display:none unless one of the first two classes is present,
 * and the link list only collapses under the third. Until that class lands,
 * the base rules apply with no responsive gate at all — .jet-mega-menu-list
 * is display:flex unconditionally, and .jet-mega-menu-toggle has no
 * display:none to override. That's exactly what both screenshots show and
 * what the captured page source confirms: this specific widget instance
 * (nested inside a sticky + motion-effects Elementor container) carries
 * NEITHER layout class, so the plugin never hides anything and the raw
 * six-link list wraps over the hero. No @media fallback exists anywhere in
 * the plugin for this case — it is 100% JS-timing-dependent with no
 * progressive-enhancement floor under it.
 *
 * FIX CHOICE: don't wait on or depend on JetMenu ever adding a --layout-*
 * class — force the collapsed state directly off a real, deterministic
 * @media query instead, using the plugin's own existing markup/classes so
 * nothing new has to be built or injected as HTML.
 *
 * Open/close state — REVISED in Round 4, see that note below for why: Round
 * 3 keyed the open styling off .jet-mega-menu--dropdown-open, the class
 * JetMenu's own click handler is SUPPOSED to toggle (read directly in
 * jet-menu-public-scripts.js, bound unconditionally in initEvents()). Built
 * that version, then actually clicked the button through Playwright to
 * screenshot the open state — the class never appeared, click or no click.
 * Whatever the exact reason inside the plugin (this widget uses a SEPARATE,
 * Elementor-specific init path — jet-menu-elementor-widgets-scripts.js —
 * never fully read, since the fix no longer depends on it), the observed
 * behaviour is what governs: JetMenu's own open/close mechanism does not
 * fire for this widget instance. So this file's rules now key off
 * .cw-mobile-nav-open — a class OUR OWN script adds and removes, not
 * JetMenu's. Nothing here depends on any JetMenu behaviour anymore beyond
 * the header markup itself being present.
 *
 * The companion script in worker.js's body injector (same file, see
 * MOBILE_NAV_A11Y_SCRIPT's own comment) now owns the whole open/close
 * interaction — click and keyboard both — plus aria-expanded, plus the
 * icon-swap/panel classes this file styles. JetMenu's toggle is also a
 * <div role="button" tabindex="0">, not a real <button>, so it was never
 * keyboard-activatable on top of not reliably opening; the same script
 * closes both.
 *
 * ROUND 4 (Dan): Round 3 hid the six-link list and sized the toggle, but
 * never gave the header itself a real BAR — the logo and toggle were left
 * to whatever height/direction Elementor's own header container naturally
 * computed on mobile, which turned out to be a narrow, shrink-wrapped,
 * COLUMN-direction stack (logo, then toggle, directly above the hero with
 * no solid ground under either) rather than a row. Two more selectors fix
 * that, both stable per-template Elementor IDs from the SAME header
 * template (data-elementor-id 10537, confirmed against live.html), same
 * trust level as .gb-container-8b6d1c4b elsewhere in this file:
 *   .elementor-element-7d597dd4 — the header's own Elementor container.
 *     It's rendered TWICE by Elementor's sticky-header feature: once
 *     position:fixed (the one actually visible, pinned to the viewport)
 *     and once as an invisible "spacer" twin that reserves the equivalent
 *     space in normal document flow so the fixed header doesn't cover the
 *     page underneath it. Both copies carry this SAME class, so one rule
 *     sizes both together — the fixed header and the flow-space it reserves
 *     can never drift out of sync, with no dependency on Elementor's own
 *     JS re-measuring anything (the same "don't wait on a script" choice as
 *     the JetMenu fix above).
 *   .elementor-element-6695880a — the flex row inside it holding the logo
 *     widget and the JetMenu widget (a third widget, the desktop-only
 *     "Sign In" button, is a SEPARATE sibling container already carrying
 *     Elementor's own elementor-hidden-mobile class — confirmed in
 *     live.html — so it's already gone on mobile and needs no rule here).
 * ========================================================================= */
@media (max-width: 768px) {
  /* The header bar itself: a fixed height so it reads as a bar, not a
     content-hugging strip, and a solid white fill so the hero (or anything
     else) can never show through it. Applies to both the fixed header and
     its flow-spacer twin (see note above) — they share this class, so they
     size identically without any JS keeping them in sync.
     min-height, not just height: Elementor's own Container widget sets its
     OWN min-height (its "sizing" control), and min-height always wins over
     a smaller height regardless of !important — they aren't competing on
     the same property, min-height is simply a floor height can't go below.
     Verified directly (not assumed) by rendering with height alone: the bar
     measured 165px, not 64px, until min-height was added here too. */
  .elementor-element-7d597dd4 {
    height: 64px !important;
    min-height: 64px !important;
    background: #ffffff !important;
  }

  /* Logo left, toggle right, both vertically centred in the 64px bar.
     flex-wrap:nowrap + width:auto on both children (below) — verified
     directly (not assumed): without these, JetMenu's own base rule
     .jet-mega-menu{width:100%} (unconditional, checked in its CSS) made the
     menu widget want the row's FULL width, which didn't fit next to the
     logo and wrapped to a second line under it — the exact
     logo-then-toggle-stacked layout this section exists to fix. */
  .elementor-element-6695880a {
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: nowrap !important;
    align-items: center !important;
    justify-content: space-between !important;
    height: 100% !important;
    padding: 0 16px !important;
    gap: 12px;
  }
  .elementor-element-4916e6dd {
    flex: 0 0 auto !important;
  }
  .elementor-element-4916e6dd img {
    max-height: 32px;
    width: auto;
  }
  /* Belt-and-braces with justify-content:space-between above: guarantees the
     toggle sits at the right edge. width:auto/flex:0 0 auto override
     JetMenu's own width:100% at both the widget-wrapper and .jet-mega-menu
     levels — either one left at 100% reproduces the wrap bug above. */
  .elementor-element-b7f345f {
    flex: 0 0 auto !important;
    width: auto !important;
    margin-left: auto;
  }
  .jet-mega-menu {
    width: auto !important;
  }

  /* Deterministic default: list hidden, regardless of any --layout-* class. */
  .jet-mega-menu-list {
    display: none;
  }

  /* A real ≥44px tap target (DESIGN-SYSTEM.md rule 4) — JetMenu's own
     default is a bare 36x36px box, and depending on a stale/absent
     --layout-horizontal class it could be display:none outright. Dark icon
     on the now-solid-white bar (#082729 on #ffffff ≈ 15.8:1) — Round 4:
     dropped the faint rgba(8,39,41,.06) tint background, which read as
     "barely visible" against the header once the bar was actually white
     (it was tuned against a hero background it no longer sits on). */
  .jet-mega-menu-toggle {
    display: flex !important;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    min-width: 44px;
    min-height: 44px;
    border-radius: 8px;
    color: #082729;
  }
  .jet-mega-menu-toggle svg {
    width: 20px;
    height: 20px;
  }

  /* Icon swap (bars <-> close) and the open panel below are BOTH keyed off
     .cw-mobile-nav-open — OUR OWN class, added/removed by the companion
     script in worker.js, never JetMenu's own .jet-mega-menu--dropdown-open.

     Round 4 correction: Round 3 assumed JetMenu's click handler (bound in
     its own initEvents(), unconditionally, per the plugin's source) was the
     live mechanism and just needed CSS to react to the class it sets. Built
     and RENDERED that version, then drove an actual click through it
     (Playwright, same render harness as every other screenshot in this
     round) to produce v4-mobile-menu-open.png before calling it done —
     .jet-mega-menu--dropdown-open never appeared on the widget, click or
     no click. Rather than keep trusting a mechanism that visibly doesn't
     fire for this specific Elementor-placed widget instance, the script now
     owns open/close itself end to end: it listens for the click, toggles
     .cw-mobile-nav-open, and sets aria-expanded in the SAME handler — one
     source of truth, nothing left waiting on a plugin behaviour that was
     checked and did not hold up. See MOBILE_NAV_A11Y_SCRIPT in worker.js. */
  .jet-mega-menu-toggle-icon--opened-state {
    display: none;
  }
  .jet-mega-menu.cw-mobile-nav-open .jet-mega-menu-toggle-icon--default-state {
    display: none;
  }
  .jet-mega-menu.cw-mobile-nav-open .jet-mega-menu-toggle-icon--opened-state {
    display: flex;
  }

  /* Open state: the list becomes a full-width, fully OPAQUE dropdown panel
     sitting over the page content, dark links on white. position:fixed,
     anchored to the header's own forced height (top:64px) rather than
     top:100% of .jet-mega-menu's own (content-sized, vertically-centred-
     within-the-bar) box, which would land somewhere mid-bar rather than at
     the bar's bottom edge. Stays correct under the fixed/sticky header
     regardless of scroll position, since the header never moves. */
  .jet-mega-menu.cw-mobile-nav-open .jet-mega-menu-list {
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 64px;
    left: 0;
    right: 0;
    background: #ffffff;
    opacity: 1 !important;
    box-shadow: 0 12px 24px -8px rgba(8, 39, 41, .25);
    max-height: calc(100dvh - 64px);
    overflow-y: auto;
    z-index: 999;
    padding: 8px 0;
  }
  .jet-mega-menu.cw-mobile-nav-open .jet-mega-menu-item {
    width: 100%;
  }
  .jet-mega-menu.cw-mobile-nav-open .jet-mega-menu-item__inner {
    padding: 14px 20px;
    min-height: 44px;
    display: flex;
    align-items: center;
  }
  .jet-mega-menu.cw-mobile-nav-open .jet-mega-menu-item__link,
  .jet-mega-menu.cw-mobile-nav-open .jet-mega-menu-item__title {
    color: #082729 !important;
    font-weight: 600;
    text-decoration: none;
  }
}
/* --- end WP header section --------------------------------------------- */

body.single-post {
  --cw-teal-text: #00717f;
  --cw-h2: #08777d;
  --cw-orange: #f1770a;
  --cw-dark: #082729;
  --cw-peach: #ffc693;
  --cw-ink: #0c1416;
  --cw-muted: #5a6b6d;
  --cw-border: rgba(8, 39, 41, 0.14);
  --cw-teal-bg: #eff7f9;
  /* Fix 7 (independent review): DESIGN-SYSTEM.md's 4th light-tone band tint,
     --warm-bg #fdf2e7 — used to make the clickable .cw-softcta visually distinct
     from the non-clickable .cw-inshort/.cw-pullquote boxes below, which stay teal. */
  --cw-warm-bg: #fdf2e7;
  --cw-warm-border: rgba(241, 119, 10, 0.3);
}

/* --- A. Reading column ------------------------------------------------ */
/* Reading width 760px + tighter top gap (finding 10). The old per-post
   block set max-width:760px on .entry-content with no top-gap fix — we
   replicate the width (so a post WITHOUT the old block still gets it)
   and add the gap fix the old block never had. */
body.single-post .entry-content {
  max-width: 760px;
  margin-left: auto;
  margin-right: auto;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  color: var(--cw-ink);
}

/* The template pads .inside-article 80px top on every post (finding 10:
   "big empty gap between hero and first H2"). The hero already carries
   its own margin-bottom, so stacking a further 80px reads as dead air.
   Tightened to a third of that, kept generous enough not to feel cramped. */
body.single-post .inside-article {
  padding-top: 28px !important;
}

@media (max-width: 768px) {
  body.single-post .inside-article {
    padding-top: 20px !important;
  }
}

/* Round 4 (Dan): the .inside-article fix above was only HALF the gap. A
   SEPARATE, outer ancestor — .site-content — carries its own
   padding:80px 30px 20px 30px, unconditionally (the theme's own
   @media(max-width:768px) block repeats the SAME 80px top value rather than
   reducing it — checked directly in live.html's cached CSS, it is not a
   responsive value at all). That rule fires because body carries the
   theme's "one-container" class (.one-container .site-content{...}), not
   because of the "separate-containers" class .inside-article's rule needs —
   two different ancestors, two different theme rules, both landing on the
   SAME visual gap. The two paddings stack: 80px (.site-content) + 20-28px
   (.inside-article) ≈ the ~100px Dan measured. Zeroed here rather than
   reduced, since .inside-article's own (intentionally kept) padding above
   already supplies the actual gap this template wants. */
body.single-post .site-content {
  padding-top: 0 !important;
}

/* --- B. Typography ----------------------------------------------------- */
body.single-post .entry-content h1,
body.single-post .entry-content h2,
body.single-post .entry-content h3,
body.single-post .entry-content h4,
body.single-post .gb-headline {
  font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif !important;
  font-weight: 600;
}

body.single-post .entry-content h2 {
  color: var(--cw-h2) !important;
  font-weight: 600;
  font-size: clamp(1.5rem, 3.4vw, 2rem);
  letter-spacing: -0.01em;
  margin-top: 2em;
}

body.single-post .entry-content h3 {
  color: var(--cw-dark);
  font-weight: 600;
  font-size: clamp(1.15rem, 2.4vw, 1.4rem);
}

/* Fix 4 (independent review): .saboxplugin-wrap (the author box, TrustBuilder/SABox
   plugin) and .rmp-widgets-container (the star-rating widget) are BOTH direct children
   of .entry-content in the live template (checked against the scratchpad's live.html —
   see the spec's Build notes), so the broad p/li/a rules below were reaching into
   third-party plugin markup that ships its own font, size and colour rules and was
   never meant to be restyled. Excluded with :not() rather than restructured, since
   these two plugin blocks are the only offenders found and a full restructure would
   touch every rule in this section for no extra benefit. */
body.single-post .entry-content p:not(.saboxplugin-wrap p, .rmp-widgets-container p),
body.single-post .entry-content li:not(.saboxplugin-wrap li, .rmp-widgets-container li) {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  line-height: 1.65;
}

/* --- C. Links (finding 2 — was #0caab3 on white, ~2.8:1, fails AA) ----- */
/* Fix 4: same plugin exclusion as p/li above — .saboxplugin-wrap's own social-icon
   links and .rmp-widgets-container's rating links must keep their own plugin styling,
   not inherit the article's teal link colour/underline. */
body.single-post .entry-content a:not(.saboxplugin-wrap a, .rmp-widgets-container a) {
  color: var(--cw-teal-text) !important;
  text-decoration: underline;
  text-underline-offset: 2px;
}
body.single-post .entry-content a:not(.saboxplugin-wrap a, .rmp-widgets-container a):hover {
  color: var(--cw-dark) !important;
}

/* --- D. Focus ring (finding 4 — template set a:focus{outline:0}) -------
   Fix 5 (independent review): the orange ring (#f1770a) measured ~2.84:1 on white and
   ~2.62:1 on #eff7f9 — both fail the 3:1 non-text-contrast floor (WCAG 2.4.11/1.4.11).
   Replaced with a dark #082729 outline plus a white halo (box-shadow) on every LIGHT
   surface — the dark ring alone measures ~15.8:1 on white and ~14.5:1 on #eff7f9 (sRGB
   relative-luminance contrast, both comfortably clear 3:1), and the white halo keeps it
   legible over the hero photo/scrim area too. The dark CTA band is a dark background
   (`--cw-dark` gradient) where a dark ring would vanish, so it gets its own light ring
   instead — a white outline against that background also clears 3:1 by a wide margin
   (the same luminance pair, inverted). */
body.single-post a:focus-visible,
body.single-post button:focus-visible,
body.single-post summary:focus-visible {
  outline: 3px solid var(--cw-dark);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px #ffffff;
  border-radius: 4px;
}
body.single-post .cw-cta-band a:focus-visible {
  outline: 3px solid #ffffff;
  outline-offset: 2px;
  box-shadow: none;
  border-radius: 4px;
}

/* --- E. Reduced motion -------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  body.single-post *,
  body.single-post *::before,
  body.single-post *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- F. Hero scrim (finding 5 — white title/byline on a light-teal-washed
   photo, ~1.6-2.7:1). The template's own `.gb-container-8b6d1c4b::before`
   paints the photo at opacity:0.34 over `background-color:var(--p100)`
   (a light teal), which is the wash that fails contrast. We cannot safely
   overwrite that ::before (it also carries the image itself), so we add
   an ::after darkening layer at the same stacking level, which — because
   it comes later in generated-content order — paints on top of ::before
   while the actual content (`.gb-container-3f517a84`, z-index:1) stays
   above both.

   Round 3 (Dan, mobile): Dan flagged the flat near-black hero as not
   working ("that black hero background doesn't seem to work"). The scrim
   was a flat rgba(--cw-dark) wash; the container's own fallback background
   was flat --cw-dark too — on a post whose featured image is itself a
   text-free brand-background image (no real photo), the whole hero read as
   one uniform black slab. Both are now the SAME two-colour brand gradient
   already used elsewhere on the static site (about.html's .ab-phonebeat,
   how-it-works.html's .pillar.dark: linear-gradient(135deg,#0e3a3e,--dark))
   — not a new gradient invented for this fix, the one the brand already
   uses. brand-assets/design.md's "no heavy gradients" rule governs FLOATING
   decorative gradients on card/button chrome; this is a two-stop dark tonal
   wash behind white hero text, the same category as the CTA band's existing
   gradient a few rules down, and matches the established site pattern
   above rather than inventing a new look.

   Contrast math (worst case: a pure-white photo behind the scrim, the
   hardest case for a DARK overlay — recomputed for the gradient scrim, not
   just asserted, sRGB relative-luminance per WCAG). Scrim is now
   linear-gradient(135deg, rgba(14,58,62,.72), rgba(8,39,41,.72)) — checked
   at BOTH stops since text can sit anywhere along the gradient:
     stop 1 (14,58,62 @ 72%) alpha-blended onto white →
       R 14×.72+255×.28=81.5, G 58×.72+255×.28=113.2, B 62×.72+255×.28=116.0
       → rgb(82,113,116) vs white text = 5.28:1
     stop 2 (8,39,41 @ 72%) alpha-blended onto white →
       R 8×.72+255×.28=77.2, G 39×.72+255×.28=99.5, B 41×.72+255×.28=100.9
       → rgb(77,100,101) vs white text = 6.35:1
   Worse of the two (5.28:1) still clears the 4.5:1 floor with margin, even
   in the pure-white-photo extreme; against any real (non-white) trade photo
   the margin is larger — and against THIS piece's flat brand-background
   featured image (much darker than white) it is larger still. */
body.single-post .gb-container-8b6d1c4b {
  background: linear-gradient(135deg, #0e3a3e, var(--cw-dark)) !important;
}
body.single-post .gb-container-8b6d1c4b::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(135deg, rgba(14, 58, 62, 0.72), rgba(8, 39, 41, 0.72));
  pointer-events: none;
}
body.single-post .gb-container-8b6d1c4b .gb-container-3f517a84 {
  position: relative;
  z-index: 1;
}
body.single-post .gb-container-8b6d1c4b h1,
body.single-post .gb-container-8b6d1c4b p,
body.single-post .gb-container-8b6d1c4b a {
  color: #ffffff !important;
  font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif !important;
}

/* Round 3 (Dan, mobile): compact hero. The template hard-codes
   height:700px + padding:150px 5px + margin-bottom:50px on the container,
   and the H1 inside carries its own margin-top:300px (both from the
   template's cached CSS, not this file) — together they push the title
   near the BOTTOM of a 700px-tall block and leave a ~50px dead gap under
   it before .inside-article's own top padding even starts. Desktop keeps
   that layout (not asked for); only mobile is compacted, to 220-300px per
   spec, title vertically centred with flexbox so it stays centred whatever
   the title's line count, gap after removed. */
@media (max-width: 768px) {
  /* min-height, not a hard height: a fixed height:clamp(220px,58vw,300px)
     (Round 3's first attempt) caps out at 300px regardless of content — on
     THIS piece's 3-line wrapped title plus the byline/avatar row, that cap
     left too little room and the title's top line sat hard against the
     header bar. min-height gives the SAME 220-300px range for the common
     1-2-line title (nothing stretches it further than it needs), while a
     longer title simply grows the box instead of overflowing or clipping —
     never a hard ceiling a real post's title can be caught against.

     Round 5 (Dan, mobile — Checkatrade draft 13322's real-iPhone-viewport render,
     30 Sep): min-height alone did NOT actually fix the "sat hard against the header
     bar" problem above — it was still real, confirmed by measuring rendered pixel
     positions (getBoundingClientRect), not by eye: the H1's own top edge landed at
     y≈59px while the fixed header (the WP HEADER section above, "WP HEADER — mobile
     nav collapse") occupies y:0–64px, so the header's white bar overlapped the
     first ~5px of the title's top line. Root cause: the "WP HEADER" section's own
     comment records that the fixed header is "rendered TWICE... once position:fixed
     ...and once as an invisible 'spacer' twin that reserves the equivalent space in
     normal document flow" — checked directly this round (both elements'
     getBoundingClientRect()) and the spacer twin's own box sits ABOVE the viewport
     (top -75px to bottom -11px), reserving nothing at its expected position. Rather
     than depend on that spacer behaving correctly — the same "don't wait on a script
     / a mechanism that doesn't hold up" choice already made for JetMenu's dropdown
     class elsewhere in this file — this rule now adds the header's own 64px height
     to its top padding directly, deterministically, regardless of what the spacer
     twin does. Verified after the fix (not just asserted): H1 top edge now clears
     the header's y:64 bottom edge with room to spare. Sides/bottom padding
     unchanged (32px, as before) — only the top gained the extra 64px clearance. */
  body.single-post .gb-container-8b6d1c4b {
    height: auto !important;
    min-height: clamp(220px, 58vw, 300px);
    padding: 96px 20px 32px 20px !important;
    margin-bottom: 0 !important;
    display: flex !important;
    align-items: center;
    justify-content: center;
  }
  body.single-post .gb-container-8b6d1c4b .gb-container-3f517a84 {
    width: 100%;
  }
  body.single-post .gb-container-8b6d1c4b h1 {
    margin-top: 0 !important;
    font-size: clamp(1.5rem, 6vw, 2rem) !important;
  }
}

/* =========================================================================
 * F2. Card hero — BLENDED (Round 7, Dan, 2026-09-30) — replaces the boxed-card
 * design below with one continuous image.
 *
 * Round 5/6 built the hero as a small 1200x630 Dave CARD (rounded corners, shadow,
 * 42% width) floating on top of the container's own separate brand-gradient
 * background. Dan's read on the live Checkatrade preview: "we've almost created an
 * image with the space for the text to go on, and then stuck that on the back of a
 * background with all the text on it, rather than it being just the image with the
 * text on it in one image" — plus a request for a bottom fade like the Vidalux blog
 * template (photo eases into the page; title sits in real text, never printed into
 * the picture).
 *
 * THE FIX, two parts:
 *   1. The artwork itself is no longer a small boxed card — it is
 *      projects/contentworks/social-covers/dave-hero.html's output, a WIDE image
 *      that bakes in the SAME gradient this container's own base `background`
 *      already paints (F's `linear-gradient(135deg,#0e3a3e,var(--cw-dark))`, the two
 *      exact stops), full-bleed, Dave+Sonny on the right, the left ~55% left empty
 *      for this text. `::before` is sized to the container's full box (inset:0,
 *      cover) instead of a 42%-wide floating rectangle — no rounded corners, no
 *      shadow, because it is not a card any more, it IS the background.
 *   2. THE FADE — `mask-image` on `::before` ITSELF (never on the container, which
 *      also holds the title: masking the container would fade the WORDS too, not
 *      just the picture) dissolves the artwork's bottom band to transparent. The
 *      container's own `background` is set to `#fff` here (overriding F's dark
 *      gradient — that gradient is now baked into the image, not needed as a
 *      fallback), so what shows through the fade is genuine page white, the same
 *      technique named in `30-vidalux-fade-reference.png` (gradient-to-page-colour
 *      over the image's own bottom band).
 *
 * DETECTION unchanged: worker.js's CardHeroDetector/`cw-card-hero` body class, same
 * signal as before (og:image carrying a "-card-featured"/"-card-share" filename).
 * article-preview.py's is_card_hero() mirrors it, unchanged.
 *
 * A post with no Dave card never gets the class, so every rule below stays INERT for
 * it — proved the same way Round 5 did, by rendering an old real-photo post before
 * and after this section changed and diffing the two PNGs (byte-identical).
 *
 * Text stays WHITE (F's own `h1,p,a{color:#fff}` rule, inherited, unchanged) because
 * it still sits on the same dark gradient (top ~76% of the box, above the fade) —
 * the exact contrast math in F's own comment (5.28:1–6.35:1) still applies, since
 * it's the identical two-stop gradient, just now painted inside the image instead of
 * as the container's CSS fallback. `align-items:flex-start` (not `center`, Round
 * 5/6's choice) keeps the whole text block anchored near the TOP of the box,
 * comfortably clear of the fade band, regardless of how many lines the title wraps
 * to — center-aligning a tall text block would risk its own bottom line drifting
 * into the fading-to-white band, where white text would start losing contrast.
 * ========================================================================= */
@media (min-width: 769px) {
  body.single-post.cw-card-hero .gb-container-8b6d1c4b {
    height: auto !important;
    /* Round 5 (Dan): bumped 560px -> 575px to open up the gap below the byline for
       the new bottom-left Sonny (see .cw-hero-sonny below). First attempt bumped
       PADDING-bottom instead (88px -> 112px) — measuring the actual rendered page
       afterward showed the gap unchanged (still 11px at 1440), because this
       container's intrinsic height (padding + content) was already comfortably
       under the 560px min-height floor, so extra padding was silently absorbed by
       slack that min-height was already supplying, never reaching the container's
       own bottom edge where Sonny sits. min-height is the lever that actually moves
       the floor Sonny (bottom:0) sits on, independent of how tall the content block
       itself is — padding-bottom reverted to 88px.
       The ceiling here is Dave, not Sonny: this container's own height also sets the
       background-size:cover SCALE for Dave's image (same cover-crop math this file's
       "Round 3" comment already documents), and past a certain height the scale
       crosses over into cropping MORE off Dave's sides at 1024px width than his own
       safe margin allows. Solved by measuring, not just computing by hand after the
       padding attempt's own arithmetic turned out to disagree with the rendered
       page: 575px keeps Dave's own measured right edge (x=1693 canvas) comfortably
       inside the 1024-width visible boundary (~1712 canvas, ~19px to spare) while
       widening the byline-to-Sonny gap to 26px at 1440 / ~50px at 1024 — both
       confirmed by re-measuring the actual rendered page after this change, not
       asserted from the arithmetic alone. */
    min-height: 575px;
    padding: 104px 64px 88px 64px !important;
    margin-bottom: 0 !important;
    display: flex !important;
    align-items: flex-start;
    justify-content: flex-start;
    text-align: left !important;
    overflow: hidden !important;
    /* Was F's dark gradient (the correct fallback for section F's real-photo posts).
       Here the SAME gradient is already baked into the artwork at full opacity, so
       the container's own background only matters at its very edges (cover-crop
       rounding) — effectively invisible either way now there is no fade to reveal it
       through. */
    background: #ffffff !important;
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b::before {
    opacity: 1 !important;
    background-size: cover !important;
    background-position: center top !important;
    background-color: transparent !important;
    inset: 0 !important;
    left: 0 !important;
    top: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    width: 100%;
    height: 100%;
    transform: none;
    border-radius: 0;
    box-shadow: none;
    /* NO FADE (Round 5, Dan, verbatim: "do away with that fade completely. It just
       doesn't work on a solid background... go solid"). Rounds 2-4 progressively
       shortened a mask-image fade on this layer; Round 5 removes it outright — a
       solid brand-gradient band with a clean straight bottom edge. No mask-image
       property is set here at all (not even "none" — simply absent, so there is
       nothing for a later rule to have to override). */
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b::after {
    display: none;
  }
  /* Round 5 — Sonny, bottom-left corner of the HERO ITSELF (the whole band, under
     the title/byline area — not beside Dave any more). Dan, verbatim: "The little
     Sonny robot needs to be in the bottom left-hand corner... his feet could almost
     be right on the very bottom of it, just not rammed right into the corner.
     Padding to the left, but right on that bottom... Let's maybe reduce it 10% in
     size."
     THIS IS A REAL ELEMENT, not part of the baked background image any more (see
     dave-hero.html's own header comment for why: a single cover-cropped image cannot
     land the SAME canvas position at the same on-page pixel at both 1440 and 1024,
     and this round's ask — exact left-edge alignment with the real HTML title — needs
     exactly that). worker.js's HeroSonnyInjector appends
     <img class="cw-hero-sonny" src="/assets/sonny-hero.png"> as this container's own
     last child, gated behind the same cw-card-hero signal as everything else here;
     article-preview.py's inject_hero_sonny() mirrors it for preview.
     left:64px matches the container's own padding-left above — the SAME box the
     title's text sits inside, so this is pixel-exact alignment with the title's left
     edge, not an approximation (unlike the old baked-image Sonny, which could only
     ever be "close" at one breakpoint and off at the other).
     bottom:0 — flush with the container's own bottom edge ("right on that bottom"),
     with left:64px supplying the "not rammed right into the corner" padding Dan asked
     for on the one axis that needed it.
     Size: 154px (Round 6, Dan: "too big... reduce that by 25%" — Round 5's 205px
     reduced 25%: 205*0.75=153.75). Same placement, unchanged: feet flush with the
     band's own bottom edge, left edge pixel-aligned with the title's. Natural aspect
     0.6396:1 (724x1132 source), so width is never set explicitly — only height, with
     object-fit:contain, same pattern as Dave's own sizing throughout this file. */
  body.single-post.cw-card-hero .gb-container-8b6d1c4b .cw-hero-sonny {
    position: absolute;
    left: 64px;
    bottom: 0;
    height: 154px;
    object-fit: contain;
    z-index: 2;
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b .gb-container-3f517a84 {
    position: relative;
    z-index: 1;
    max-width: 54%;
    margin-left: 0;
    margin-right: 0;
    text-align: left;
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b h1,
  body.single-post.cw-card-hero .gb-container-8b6d1c4b p,
  body.single-post.cw-card-hero .gb-container-8b6d1c4b a {
    text-align: left;
  }
  /* Fix (found by the design-review panel + verified independently by measuring the
     rendered box, not just trusted from the screenshot — getBoundingClientRect on the
     ACTUAL "after" render showed the meta row's own bottom edge at 644px inside a
     732px-tall container whose fade starts at 556px (76%): the meta row was genuinely
     inside the fade, at ~3.7:1 contrast, a real WCAG 1.4.3 fail, not a false alarm.
     Root cause, also found by the panel: the template's own base rule gives EVERY
     `.gb-headline` an inherited `margin-top:300px` (this file's "F. Hero scrim"
     section names it directly — it exists so the theme's tall 700px real-photo hero
     can sit the title low against a tall image). The mobile card-hero block already
     resets this (`margin-top:0 !important`, below); the desktop block never did,
     so the H1 pushed 300px down inside the flex box, the `height:auto` container
     ballooned past its 560px min-height to fit it, and the meta row landed inside the
     fade band as a direct consequence — one root cause, two symptoms. Reset here
     removes both: the H1 sits near the top of the padded box as intended, the
     container settles back near its intended height, and the meta row stays inside
     the top 76% where the artwork (and therefore the text sitting on it) is at full,
     unfaded contrast. `font-size` also gets an explicit fluid clamp (matching the
     rest of the static site's heading pattern, `clamp(...)`) — the panel separately
     flagged that this box was inheriting a fixed 50px size with no defined behaviour
     in the 769-1024px tablet band; the clamp's own ceiling (3.125rem = 50px) keeps
     full desktop unchanged while giving the tablet band a floor. */
  body.single-post.cw-card-hero .gb-container-8b6d1c4b h1 {
    margin-top: 0 !important;
    font-size: clamp(1.9rem, 3.6vw, 3.125rem) !important;
  }
  /* Round 6 fix 2 (kept): the author/date row (.gb-container-f49b9f49, confirmed
     live — a flex row wrapping the avatar/name/date, direct child of
     .gb-container-3f517a84) sets its OWN justify-content:center in the template's
     base CSS, which text-align:left on its parent/siblings cannot override (a flex
     alignment property, not a text property). Left-align it to match the title,
     desktop card-hero only — mobile keeps the template's own centred meta row. */
  body.single-post.cw-card-hero .gb-container-8b6d1c4b .gb-container-f49b9f49 {
    justify-content: flex-start;
  }
}

/* The subtitle span (see worker.js's HeroSubtitleSplitter — splits the WP title on
   its first "?" and wraps the remainder in this span) — smaller text under the main
   line, both breakpoints, sized in `em` off the h1's own clamp() so it scales with
   the title rather than needing its own separate media queries. Inert on any post
   whose title carries no "?" (the span is never emitted at all — see worker.js).

   Desktop contrast at opacity:.86 (independent review, checked rather than assumed
   safe): the subtitle's own rendered bounding box was mapped from display space back
   to this post's actual featured-image canvas (2000x800), at both 1440px and 1024px
   — the two widths use different background-size:cover scales/crop offsets, so the
   box lands on a DIFFERENT patch of the gradient at each width — then every pixel in
   that patch was sampled directly from the real PNG (not estimated) for its darkest
   and lightest point. Worst case across both widths: white-at-86%-opacity against
   the LIGHTEST pixel found at 1440px, 8.78:1 — comfortably clear of the 4.5:1 floor;
   every other combination measured higher still (up to 10.69:1). No opacity change
   needed. Re-check this if the artwork, the crop math, or this rule's opacity value
   ever changes — the number is a property of all three together, not fixed. */
body.single-post.cw-card-hero .gb-container-8b6d1c4b h1 .cw-hero-subtitle {
  display: block;
  font-size: 0.46em;
  font-weight: 500;
  opacity: .86;
  margin-top: 10px;
  letter-spacing: -0.005em;
}

/* Mobile: the artwork sits in its OWN top strip (fixed viewport-relative height),
   fading into the white panel below via the SAME `::before` mask technique as
   desktop — then the title/subtitle render in plain dark text underneath, never
   over the image. This mirrors the Vidalux reference exactly (photo on top, fading
   at its own bottom edge, plain heading below in solid text) rather than Round 5/6's
   card-on-background shape, and it sidesteps the mobile-contrast risk of text over a
   busy image entirely: title text on mobile is never on top of art, only on white.
   `order:-1` still puts the image first regardless of its source position in the
   markup (same `::before` pseudo the browser would otherwise paint where the
   template originally placed it). Header clearance moves to the CONTAINER's own
   top padding (the image is now the first visual child, not the text) — same 64px
   fixed-header reasoning as the plain compact-hero rule above. */
@media (max-width: 768px) {
  body.single-post.cw-card-hero .gb-container-8b6d1c4b {
    height: auto !important;
    min-height: 0 !important;
    /* Round 6 fix (Dan: "close the white strip between the fixed header bar and the
       hero band... hero should start directly under the header"). Rounds 2-5 kept a
       40px gap on TOP of the header's own 64px (calc(64px+40px)=104px total) — correct
       for clearing Dave's hair when he was still full-height-anchored in the OLD
       card-hero design, but by Round 5 Dave's artwork already carries its own
       generous internal top margin (the Round-3 safe-zone redesign, documented in
       dave-hero.html), so the EXTRA 40px was just padding on top of padding — and
       since this container's own `background:#fff` paints straight through its
       padding box, that extra 40px rendered as a visible white strip between the
       header's bottom edge and the gradient image starting underneath it.
       padding-top is now EXACTLY the header's own height (64px, nothing added): the
       padding box still exists (so the image content isn't hidden behind the fixed
       header), but it adds no MORE than the header already covers, so nothing
       extra is ever visible — the gradient begins the instant the header ends.
       Dave's own already-generous internal clearance (confirmed by rendering and
       looking, not just computing) is what keeps his head/hair clear of the header,
       the same as it always has done. */
    padding: 64px 0 0 0 !important;
    margin-bottom: 0 !important;
    display: flex !important;
    flex-direction: column !important;
    align-items: stretch;
    text-align: left !important;
    background: #ffffff !important;
    overflow: hidden !important;
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b::before {
    position: relative !important;
    inset: auto !important;
    opacity: 1 !important;
    background-size: cover !important;
    /* Round 6 finding (go-live prep, caught before shipping): THERE IS ONLY ONE
       WordPress featured image. Every preview up to this point rendered mobile with
       a SEPARATE, dedicated 900x700 composition (dave-hero.html's own mode-mobile,
       applied via article-preview.py's --featured-override-mobile flag) — a real
       asset this build generated, but one with no production delivery path: the
       theme sets this container's background-image from the post's single
       featured_media, with no per-device swap mechanism anywhere in worker.js. Set
       featured_image to the 2000x800 DESKTOP composition alone (the one this post
       will actually carry at --apply) and this rule's own cover-crop math changes
       completely at mobile's narrow width: scale becomes HEIGHT-dominant (box
       ~390x242 vs that wide canvas), cropping horizontally around whatever
       `background-position`'s X value centres on. `center top` (50%) centred the
       crop on the canvas MIDPOINT — mostly empty gradient, with Dave (anchored well
       right of centre) partly cut off down his own right side. Checked directly by
       rendering both the "centre" and this value before choosing: `82%` centres the
       crop on Dave's own actual position (his measured bbox midpoint is ~77% across
       the 2000px canvas) instead of the canvas's geometric centre, which is a
       different thing once the composition itself is right-biased — this brings the
       whole figure, and Sonny's own real HTML element beside him, into frame
       together. Not pixel-identical to the dedicated mobile composition those
       earlier previews showed (Dave reads a little smaller/more distant, more
       gradient visible above him) but a genuine, shippable single-image result,
       confirmed by rendering and looking — not merely computed. Flagged to Dan as
       its own open item in the go-live report: a true per-device image needs a real
       swap mechanism (UA-based, in worker.js) that this session did not build. */
    background-position: 82% top !important;
    background-color: transparent !important;
    width: 100%;
    height: 62vw;
    max-height: 300px;
    border-radius: 0;
    box-shadow: none;
    margin-bottom: 0;
    order: -1;
    /* NO FADE (Round 5, Dan) — same removal as the desktop rule above. */
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b::after {
    display: none;
  }
  /* Round 5 — Sonny, mobile. Same "bottom-left of the hero BAND" ask as desktop, but
     on mobile "the hero band" is only the image STRIP (the gradient), not the whole
     flex column — the white text panel is a separate box below it, and Sonny must
     stay inside the gradient strip, never drifting down into the white panel or up
     past the fixed header.
     left:20px matches the text panel's own `.gb-container-3f517a84{padding:20px...}`
     left padding below it — "the page gutter on mobile", the same reference point the
     title's own text uses.
     `top` (not `bottom`) is used here deliberately, computed with `min()` so it stays
     correct across the SAME `min(62vw,300px)` expression the image strip's own height
     already uses, rather than a value hard-coded for one specific viewport:
       top = (container's own top padding) + (image strip height) - (Sonny's own
       height) = min(62vw,300px) - <padding - height>.
     Round 6 update: the container's own top padding dropped from 104px to 64px (the
     "close the white strip" fix just above), and Sonny himself shrank 25%
     (143px -> 107px, Dan: "too big... reduce that by 25%"), so the constant term is
     recomputed for BOTH changes together, not just one: 64 - 107 = -43. At the 390px
     iPhone width this resolves to 241.8 - 43 = 198.8px, verified against the actual
     rendered page (Sonny's feet sit exactly on the image strip's own bottom edge, not
     the container's, with no white gap above him from the old larger padding). */
  body.single-post.cw-card-hero .gb-container-8b6d1c4b .cw-hero-sonny {
    position: absolute;
    left: 20px;
    top: calc(min(62vw, 300px) - 43px);
    height: 107px;
    object-fit: contain;
    z-index: 2;
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b .gb-container-3f517a84 {
    width: 100%;
    margin-left: 0;
    margin-right: 0;
    text-align: left;
    padding: 20px 20px 28px 20px;
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b h1,
  body.single-post.cw-card-hero .gb-container-8b6d1c4b p,
  body.single-post.cw-card-hero .gb-container-8b6d1c4b a {
    /* White text was correct when this box sat over the dark gradient (desktop
       still does). On mobile the text panel below the image strip is the
       container's own `#fff` background — white-on-white is unreadable, so this
       specific breakpoint overrides F's `color:#fff` to the brand dark colour. */
    color: var(--cw-dark) !important;
  }
  body.single-post.cw-card-hero .gb-container-8b6d1c4b h1 {
    margin-top: 0 !important;
    font-size: clamp(1.5rem, 6vw, 2rem) !important;
    text-align: left !important;
  }
  /* Round 2 fix: the subtitle's shared, both-breakpoints rule sizes it at 0.46em of
     the H1's OWN font-size — fine on desktop (H1 ≥ ~50px, subtitle lands ≥ 23px), but
     on mobile the H1 itself clamps down to a 1.5rem (24px) floor, so 0.46em worked out
     to ~11px — confirmed by measuring the rendered page, not just read off the CSS.
     Well under any comfortable reading size. Given its own explicit size + full
     opacity here (mobile only; desktop keeps the shared 0.86 opacity against its dark
     gradient, already checked for contrast). 1.1rem measured at 17.6px rendered
     (comfortably clears the ≥17px ask, not just rounds up to it) on var(--cw-dark) at
     opacity:1 — measures the same ~14-15:1 contrast as the H1 above it (both
     var(--cw-dark) on the container's own white background). */
  body.single-post.cw-card-hero .gb-container-8b6d1c4b h1 .cw-hero-subtitle {
    font-size: 1.1rem;
    opacity: 1;
  }
  /* Round 2 fix: the meta/byline row (.gb-container-f49b9f49) inherits the template's
     own justify-content:center — invisible on desktop's version of this fix (already
     applied above) but never mirrored for mobile, so the byline sat centred under a
     left-aligned title. Same fix, same reasoning, this breakpoint. */
  body.single-post.cw-card-hero .gb-container-8b6d1c4b .gb-container-f49b9f49 {
    justify-content: flex-start;
  }
}

/* --- G. Component blocks (spec §B) -------------------------------------- */

/* In short (key points) — :::inshort fence */
body.single-post .cw-inshort {
  background: var(--cw-teal-bg);
  border: 1px solid var(--cw-border);
  border-radius: 12px;
  padding: 20px 24px;
  margin: 28px 0;
}
body.single-post .cw-inshort p.cw-inshort-label {
  font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: .78rem;
  color: var(--cw-h2);
  margin: 0 0 10px;
}
body.single-post .cw-inshort ul {
  margin: 0;
  padding-left: 1.2em;
}
body.single-post .cw-inshort li {
  margin-bottom: 6px;
  line-height: 1.55;
}
body.single-post .cw-inshort li:last-child {
  margin-bottom: 0;
}

/* Callout / stat — > blockquote line */
body.single-post .cw-pullquote {
  border-left: 4px solid var(--cw-h2);
  background: var(--cw-teal-bg);
  padding: 18px 24px;
  margin: 28px 0;
  font-size: 1.1em;
  line-height: 1.55;
  font-weight: 600;
  color: var(--cw-dark);
  border-radius: 0 10px 10px 0;
}

/* Soft CTA (top, after the intro) — existing shape, restyled.
   Fix 7 (independent review): this box is CLICKABLE (it's a CTA to the Scorecard) while
   .cw-inshort and .cw-pullquote below are NOT — but all three used the same teal-tinted
   box before this fix, so the one that actually does something didn't stand out. Given
   the warm background (DESIGN-SYSTEM.md's own meaning convention: "teal = promise/
   proof, warm = ..." — the action/CTA read) and a bold, arrow-suffixed link so it reads
   as clickable at a glance, not just on hover. Teal stays reserved for the two
   non-clickable info boxes. */
body.single-post .cw-softcta {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--cw-warm-bg);
  border: 1px solid var(--cw-warm-border);
  border-radius: 10px;
  padding: 14px 18px;
  margin: 20px 0 28px;
  font-weight: 600;
  line-height: 1.5;
}
body.single-post .cw-softcta a {
  color: var(--cw-dark);
  font-weight: 700;
  text-decoration: underline;
  text-decoration-color: var(--cw-orange);
  text-underline-offset: 3px;
}
/* Fix 7 follow-up, caught in visual QA on the live post (7536) mobile render: an
   earlier version of this rule set `white-space: nowrap` on the anchor to keep the
   arrow glued to the last word. Old posts already carry a live .cw-softcta with longer
   CTA copy than this file's own default ("See where your business stands, free, in 4
   minutes"), and nowrap forced that whole phrase onto one line, overflowing the 390px
   mobile viewport and pushing the ENTIRE page 142px wider (532px) — a real layout
   break, not a cosmetic one. Removed nowrap; text wraps normally, the arrow reads as
   part of the sentence either way. */
/* No CSS ::after arrow: old posts' soft CTA text already ends in "→", so a CSS arrow
   doubled it ("→ →", caught on post 7536 in round 4). New posts get the arrow in the
   link text from wp_publish.SOFTCTA_HTML instead. */

/* End CTA band — orange Scorecard button + reassurance note + secondary
   text link. This is the ONE orange button on the page (design-system
   rule, brand-assets/design.md "one orange button per page"). */
body.single-post .cw-cta-band {
  background: linear-gradient(135deg, #0b3336, var(--cw-dark) 60%);
  border-radius: 16px;
  padding: 32px 28px;
  margin: 40px 0;
  text-align: center;
  color: #ffffff;
}
body.single-post .cw-cta-band p.cw-cta-band-lede {
  margin: 0 0 18px;
  font-size: 1.05rem;
  color: var(--cw-peach);
  font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif;
  font-weight: 600;
}
/* Orange button text: NOT white. Checked against DESIGN-SYSTEM.md's own
   .btn-primary rule (cw-design-system.css) — the design system's actual
   answer for text-on-orange is --dark (#082729), not white. White on
   #f1770a measures ~2.8:1 (fails 4.5:1 even at large-text 3:1 in most
   button sizes); dark-on-orange (#082729 on #f1770a) measures 5.54:1
   (corrected 29 Sep per independent review — the file previously claimed
   "~9.9:1", which the maths does not support; 5.54:1 still clears 4.5:1
   normal-text AA with margin). Using the system's own answer rather than
   inventing a new one.

   Fix 6 (independent review): flattened from a 3-stop gradient
   (#ff9036 → --cw-orange → #df6907) to a SOLID --cw-orange fill, so the
   5.54:1 figure above is the button's contrast everywhere on its face, not
   just at the one gradient stop it was measured against — a gradient
   necessarily has darker/lighter regions than the flat colour the number
   was computed from. */
body.single-post .cw-cta-band a.cw-cta-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--cw-orange);
  color: var(--cw-dark) !important;
  font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif;
  font-weight: 700;
  font-size: 1.02rem;
  padding: 15px 30px;
  border-radius: 999px;
  text-decoration: none !important;
  min-height: 44px;
  box-shadow: 0 12px 30px -10px rgba(241, 119, 10, .5);
}
body.single-post .cw-cta-band a.cw-cta-btn:hover {
  /* A uniform brightness dip (not a colour swap) keeps the button solid and
     keeps the 5.54:1 text contrast intact within a fraction of a point —
     no separate contrast figure needed for the hover state. */
  filter: brightness(0.94);
}
body.single-post .cw-cta-band p.cw-cta-note {
  margin: 14px 0 0;
  font-size: .85rem;
  color: rgba(255, 255, 255, .7);
}
body.single-post .cw-cta-band p.cw-cta-alt {
  margin: 10px 0 0;
  font-size: .9rem;
}
body.single-post .cw-cta-band p.cw-cta-alt a {
  color: var(--cw-peach) !important;
  text-decoration: underline;
}

/* FAQ — details/summary cards (finding 7: was plain bold paragraphs) */
body.single-post .cw-faq {
  margin: 36px 0;
}
body.single-post .cw-faq details.cw-faq-item {
  border: 1px solid var(--cw-border);
  border-radius: 12px;
  padding: 4px 20px;
  margin-bottom: 12px;
  background: #ffffff;
}
body.single-post .cw-faq details.cw-faq-item[open] {
  background: var(--cw-teal-bg);
}
body.single-post .cw-faq summary.cw-faq-q {
  cursor: pointer;
  list-style: none;
  font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif;
  font-weight: 600;
  color: var(--cw-dark);
  padding: 16px 32px 16px 0;
  min-height: 44px;
  display: flex;
  align-items: center;
  position: relative;
}
body.single-post .cw-faq summary.cw-faq-q::-webkit-details-marker {
  display: none;
}
body.single-post .cw-faq summary.cw-faq-q::after {
  content: "+";
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.4rem;
  color: var(--cw-h2);
  font-weight: 400;
}
body.single-post .cw-faq details.cw-faq-item[open] summary.cw-faq-q::after {
  content: "\2212"; /* minus sign */
}
body.single-post .cw-faq .cw-faq-a {
  padding: 0 0 18px;
  line-height: 1.6;
  color: var(--cw-ink);
}

/* Repeat link after the FAQ cards (fix 9) — a plain TEXT link, deliberately not a
   second button: the one-orange-button-per-page rule is already used by the CTA band
   above. Picks up the ordinary link colour/underline from section C; this block only
   handles the paragraph's own centring and spacing. */
body.single-post .entry-content p.cw-repeat-link {
  text-align: center;
  margin: 8px 0 32px;
  font-weight: 700;
}

/* Sources note — :::sources fence, replaces the plain "—" divider */
body.single-post .cw-sources {
  margin: 40px 0 24px;
  padding-top: 18px;
  border-top: 1px solid var(--cw-border);
  font-size: .85rem;
  line-height: 1.6;
  color: var(--cw-muted);
  font-style: italic;
}
body.single-post .cw-sources a {
  color: var(--cw-teal-text);
}

/* --- H. Tables (GFM pipe tables -> wp:table) ---------------------------- */
body.single-post .entry-content .wp-block-table,
body.single-post .entry-content table {
  width: 100%;
  border-collapse: collapse;
  margin: 28px 0;
  font-size: .95rem;
}
body.single-post .entry-content table th,
body.single-post .entry-content table td {
  border: 1px solid var(--cw-border);
  padding: 10px 14px;
  text-align: left;
  vertical-align: top;
}
body.single-post .entry-content table th {
  background: var(--cw-teal-bg);
  color: var(--cw-dark);
  font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif;
  font-weight: 600;
}
body.single-post .entry-content table tr:nth-child(even) td {
  background: rgba(239, 247, 249, .4);
}
/* Tables can overflow a narrow viewport — scroll the table, never the page. */
body.single-post .entry-content .wp-block-table {
  overflow-x: auto;
  display: block;
}
body.single-post .entry-content .wp-block-table table {
  display: table;
}

/* --- I. In-body figures / images ---------------------------------------- */
body.single-post .entry-content .wp-block-image,
body.single-post .entry-content figure {
  margin: 28px 0;
}
body.single-post .entry-content .wp-block-image img,
body.single-post .entry-content figure img {
  width: 100%;
  height: auto;
  border-radius: 10px;
  display: block;
}
/* figcaption: .85rem, --cw-muted (#5a6b6d on white = 5.59:1, clears the 4.5:1 AA text
   floor), centred to match the figure/image above it, 8px gap under the image. Scoped
   to `figure figcaption` (not a bare `figcaption`) to match wp_publish.py's caption
   feature, which only ever nests a figcaption inside a wp-block-image figure. */
body.single-post .entry-content figure figcaption {
  font-size: .85rem;
  color: var(--cw-muted);
  margin-top: 8px;
  text-align: center;
}

/* Screenshots (wp_publish.py's `screenshot-` filename rule): a light frame so a
   cropped browser capture reads as a deliberate proof image, not a stray graphic. */
body.single-post .entry-content img[src*="screenshot-"] {
  border: 1px solid #d9e4e6;
  border-radius: 8px;
}

/* --- J. No horizontal scroll at any width ------------------------------- */
body.single-post .entry-content {
  overflow-wrap: break-word;
  word-wrap: break-word;
}
body.single-post .entry-content img,
body.single-post .entry-content iframe,
body.single-post .entry-content video {
  max-width: 100%;
}

/* --- K. Mobile ----------------------------------------------------------- */
/* No extra horizontal padding added here on purpose: `.inside-article`
   (the ancestor) already carries 30px side padding at every width per
   the template's own mobile rule, so a second padding layer here would
   just narrow the reading column further. Horizontal-scroll safety comes
   from J above (max-width:100% on media, overflow-wrap on text). */
@media (max-width: 480px) {
  body.single-post .cw-cta-band {
    padding: 24px 18px;
  }
  body.single-post .cw-cta-band a.cw-cta-btn {
    width: 100%;
    justify-content: center;
    padding: 15px 20px;
  }
}
