/*
Theme Name: boothwyn
Theme URI: https://libertyconcepts.com/
Author: Liberty Concepts
Author URI: https://libertyconcepts.com/
Description: A neutral, block-ready starter theme for the lc-blocks plugin. Ships chrome (header, footer, navigation, base typography); blocks self-style from the lc-blocks plugin.
Version: 1.1.1793216081
Requires at least: 6.4
Requires PHP: 8.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: blockstart
*/

/* The text domain above is deliberately `blockstart`, not the project slug that
   boothwyn substitutes into `Theme Name:`. It is hard-coded in 50 places
   across 9 PHP files; changing it here alone silently orphans every one of them
   with no error. If you rename it, rename the literals in the same commit. */

/* =========================================================================
   Tokens / fallbacks (mirror theme.json so chrome tracks the palette)
   ========================================================================= */
/* Slugs below are deliberately the STARTER's, not Boothwyn's token names: the
   lc-blocks plugin makes 276 references to `primary`, `secondary`, `text`,
   `dark`, `subtle` and friends, so remapping the values re-skins every block
   with no per-block work. theme.json carries the real Boothwyn token name in
   each palette entry's `name` field, which is what an editor sees in the
   colour picker. Source: BP_Master_Token_Sheet_v3 §1.

   Two slugs are worth knowing about because their starter names now mislead:
     --bs-primary-light  is Accent/Mid-green #056752 — a DARK green. Every
                         lc-blocks use of it is a `color:` (eyebrow, links,
                         in-block accents), so it has to be contrast-safe on
                         white; the light green would fail there.
     --bs-secondary      is Accent/Light-green #A7DBBA. Every use is a
                         background, border, or text-on-dark. */
:root {
	--bs-primary: var(--wp--preset--color--primary, #4EC278);             /* Brand/Primary         */
	--bs-primary-light: var(--wp--preset--color--primary-light, #056752); /* Accent/Mid-green      */
	--bs-secondary: var(--wp--preset--color--secondary, #A7DBBA);         /* Accent/Light-green    */
	--bs-subtle: var(--wp--preset--color--subtle, #EBF5F2);               /* Neutral/100           */
	--bs-dark: var(--wp--preset--color--dark, #0C3330);                   /* Brand/Dark            */
	--bs-text: var(--wp--preset--color--text, #232626);                   /* Neutral/900           */
	--bs-white: var(--wp--preset--color--white, #FFFFFF);                 /* Neutral/White         */
	--bs-off-white: var(--wp--preset--color--off-white, #F5F5F5);         /* Surface/Gradient-Base */
	--bs-neutral-200: var(--wp--preset--color--neutral-200, #CFE5DF);     /* Neutral/200           */
	--bs-neutral-300: var(--wp--preset--color--neutral-300, #BCCCC7);     /* Neutral/300           */
	--bs-muted: var(--wp--preset--color--muted, #464D4B);                 /* Neutral/700           */

	--bs-heading: var(--wp--preset--font-family--heading, "Geist", sans-serif);
	--bs-body: var(--wp--preset--font-family--body, "Inter", sans-serif);
	--bs-mono: var(--wp--preset--font-family--mono, "Geist Mono", ui-monospace, monospace);

	--bs-content: var(--wp--style--global--content-size, 1280px);
	--bs-header-h: var(--bp-header-h);

	/* Licensure status colours: one green ramp, dark → light (PM's per-step
	   table, 28 Oct 2026; it replaced the 14 Oct categorical set). Shared by the
	   map block and the State pages, so they cannot drift apart. Measured — see
	   lc-blocks licensure-map/style.scss. */
	--bp-status-licensed: #0C3330;
	--bp-status-partial: #056752;
	--bp-status-renewal: #5FA38F;
	--bp-status-application: #A8CEC2;
	--bp-status-none: #EBF5F2;
	--bp-status-none-edge: #B9CFC8;
}

/* =========================================================================
   Font-size preset aliases — NOT optional, and not cosmetic
   =========================================================================
   WordPress kebab-cases preset slugs when it generates custom properties, so
   the slug `h1` emits `--wp--preset--font-size--h-1`, with a hyphen before the
   digit. Nothing warns.

   Every consumer writes the name the obvious way. lc-blocks references
   `--wp--preset--font-size--h3`, `--h4`, `--h5`, `--h2`, `--h1` in 33 places,
   and theme.json's own `styles.elements.h1..h6` do the same. All of them were
   resolving to nothing and silently using their hardcoded fallbacks — which is
   why it looked fine: the fallbacks are close to the right values, and the hero
   H1's `clamp(32px, 5vw, 62px)` happens to equal the real token at exactly 1440
   and 375, the two widths first measured.

   Defining both spellings here fixes every consumer in one place, without
   renaming slugs that block code already references. Discovered 13 Aug 2026
   when the audience heading rendered at its 28px fallback at all three widths.
   ========================================================================= */
:root {
	--wp--preset--font-size--h1: var(--bp-fs-h1);
	--wp--preset--font-size--h2: var(--bp-fs-h2);
	--wp--preset--font-size--h3: var(--bp-fs-h3);
	--wp--preset--font-size--h4: var(--bp-fs-h4);
	--wp--preset--font-size--h5: var(--bp-fs-h5);
	--wp--preset--font-size--h6: var(--bp-fs-h6);
}

/* =========================================================================
   Responsive scale — type, spacing, chrome
   =========================================================================
   Why this is piecewise rather than one clamp() per token.

   The design states exact values at four built frames (375 / 768 / 1024 /
   1440 — token sheet §8, read from the frames, not derived). Those values are
   NOT linear in viewport width: H1 rises 32→44 over the first range, 44→48
   over the second, 48→62 over the third. A single clamp() would hit the two
   ends and drift everywhere between, which is exactly the "close but not
   exact" drift dev-rules §E calls out. Interpolating per range hits every
   stated value exactly at its own breakpoint and stays fluid in between.

   Above 1440 the rule is different and comes from project-log.md's cross-phase
   decision: everything scales proportionally to 1920, then stops. That is
   driven by ONE variable, --bp-u — a length equal to 1px at 1440 and 1.3333px
   at 1920+ — so `calc(62 * var(--bp-u))` is H1 at any width in that range.

   Side margin is the deliberate exception: it stays 80px above 1440. That is
   not a judgement call, it is forced — the log fixes the container lock at
   1760px, and 1920 − (2 × 80) = 1760 exactly. Had the margin scaled with
   everything else the container would land at 1707 and contradict the lock.
   ========================================================================= */
:root {
	/* Zoom unit — only meaningful at ≥1440; 1px keeps it inert below. */
	--bp-u: 1px;

	/* Type — 375 values held below 375 (the smallest designed frame). */
	--bp-fs-h1: 32px;
	--bp-fs-h2: 28px;
	--bp-fs-h3: 24px;
	--bp-fs-h4: 20px;
	--bp-fs-h5: 20px;
	--bp-fs-h6: 18px;
	--bp-fs-eyebrow: 14px;
	/* Text/Eyebrow-S. It tracks Eyebrow below 1024 and then diverges: the built
	   frames put it at 16 across both tablet widths and 14 at desktop, in BOTH
	   places it appears (Quality's ACCREDITATIONS & CREDENTIALS at
	   BP Homepage Tablet.dc.html:87, and Technology's FOCUSED THERAPEUTIC AREAS).
	   Non-monotonic, but consistent across two independent sections. */
	--bp-fs-eyebrow-s: 14px;
	--bp-fs-body: 16px;
	--bp-fs-small: 14px;
	--bp-fs-caption: 12px;   /* never scales below 1440 — and 12px is the floor */
	--bp-fs-button: 13px;    /* never scales below 1440 */

	/* Line-height (token sheet §2). P-Long is the one token that changes. */
	--bp-lh-tight: 1.2;      /* H1–H3, eyebrow, category, caption, tag, button */
	--bp-lh-snug: 1.4;       /* H4–H6                                          */
	--bp-lh-body: 1.6;       /* Text/P-Short                                   */
	--bp-lh-long: 1.8;       /* Text/P-Long at 375 — see the note at ≥768      */

	/* Letter-spacing (token sheet §2, unit is %). */
	--bp-ls-none: 0;
	--bp-ls-eyebrow: 0.04em; /* 4%  — Text/Eyebrow, Text/Eyebrow-S */
	--bp-ls-wide: 0.12em;    /* 12% — Text/Category, Text/Link, Text/Button */
	--bp-ls-caption: 0.02em; /* 2%  — Text/Caption */

	/* Layout (token sheet §4 / §8). */
	--bp-side: 20px;
	--bp-inset: 8px;
	--bp-section: 60px;
	--bp-header-h: 64px;
	--bp-detail: 900px;

	/* Header chrome. --bp-header-pad is the BAR's inset and is deliberately not
	   --bp-side: the comps inset the bar 40px at 1440 where the content grid uses
	   80px (BP Header.dc.html:15). Holding it at 40px above 1440 preserves that
	   relationship exactly — the bar stays 80px wider than the content container
	   at both 1440 and 1920 — which is why it does not scale with --bp-u. */
	--bp-header-pad: 20px;
	--bp-logo-h: 34px;
	--bp-burger-bar-w: 22px;
	--bp-burger-bar-gap: 5px;

	/* Buttons. The standard button IS responsive — 16/28 below desktop, 18/32 at
	   desktop — confirmed three ways: the Figma component (101:30) draws every one
	   of its eight filled variants at 18/32; the tablet frame uses 16/28 for all
	   sixteen of its standard buttons; and 18+18+~15.6 is the 52px height §5a
	   itself states. The `14px 24px` in §5a is the HEADER CTA variant, not the
	   standard one — the tablet frame uses it exactly twice, on its two headers.
	   Consumed by lc-blocks' buttons block, which carries its own fallbacks. */
	--bp-btn-pad-y: 16px;
	--bp-btn-pad-x: 28px;
	--bp-btn-gap: 12px;      /* stacked full-width at mobile (§5a) */

	/* Light_Gradient_BG — one export for every breakpoint, pinned to the bottom
	   of its section over Surface/Gradient-Base. It SUPERSEDES three retired
	   exports (HP_Quality_Gradient BG, Multi-content Mesh, the 375 light export);
	   do not build against those names. Set here rather than in the block so the
	   uploaded media URL stays out of block markup. */
	/* ── Delivered design assets ── QA Q30, 20 Aug 2026 ──────────────────────
	   `--bp-light-gradient-bg`, `--bp-telehealth-gradient-bg`,
	   `--bp-watermark-molecule`, `--bp-keypage-gradient-bg` and
	   `--bp-logomark-improvement` are NOT declared here any more. They are built
	   in `functions.php` (`bs_enqueue_assets`) from `get_theme_file_uri()`,
	   because their URLs cannot be written into a stylesheet correctly:
	   an absolute URL is environment-specific, and a relative url() does not work
	   inside a custom property — it resolves against the document or the
	   substituting stylesheet, not this one. Corrected defect #19 has the
	   measurements. The files themselves are in `assets/images/`, beside the
	   logos and favicons.
	   `style.css` now contains no `url()` at all, which is the invariant worth
	   keeping: any asset this theme needs gets its URL built in PHP. */

	/* HP_Telehealth_Gradient BG — the delivered 1440 × 1450 export, whose height
	   is exactly the Technology section's own at desktop. Same reasoning as
	   above: kept out of block markup so the media URL is not baked into save()
	   output and does not have to survive migration. */

	/* HP Molecule Watermark — the delivered 370 × 896 export, with the crop of
	   the wider vector already baked in. Consumed by lc/fancy-container. */

	/* Key page top gradient BG — the delivered 1440 × 780 export. §9c item A: the
	   ASSET at desktop, the CSS radial below it, and the value is identical
	   either way. The band itself is 870 (Figma 425:2288), so the asset covers. */

	/* Feature Split-Full-width-Gradient BG — REMOVED 20 Aug 2026, QA Q9.
	   The delivered 2880 × 1162 export scales to 2560 × 1033 / 976 KB on upload,
	   and that is what was loading as a desktop section background on two pages.
	   The token sheet reads "asset at desktop, CSS gradient below" and that is
	   how it was built — but the sheet also says the CSS value "scales to any
	   height without stretching the artwork", and the identical CSS gradient was
	   already running below 768. So the export bought ~1 MB per page and nothing
	   else. Developer decision: CSS at every width. `lc/feature-split`'s
	   `--bg-gradient` variant now paints its own `linear-gradient` unconditionally.
	   ⚠️ Note what went with it: this custom property held an ABSOLUTE
	   `boothwyn.lndo.site` URL, which would have shipped to production pointing
	   at a developer's local machine. The three properties still below carry the
	   same hazard — see §9c item A for the key-page gradient, which is the one
	   still open. */

	/* Logo Mark_Improvement Section — the delivered 33 × 49 export. A background
	   rather than inline markup: it carries three multi-stop gradients, and this
	   keeps the media URL out of block save() output. */
	--bp-gap-intra: 30px;
	--bp-gap-inter: 60px;
	--bp-radius: 4px;
	--bp-radius-pill: 9999px;

	/* Content container: viewport minus both side margins, capped at 1760. */
	--bp-container-max: 1760px;
	--bp-container: min(calc(100vw - (2 * var(--bp-side))), var(--bp-container-max));

	/* Horizontal inset for FULL-BLEED bands — the interior heroes, the filter bar,
	   the card grid. Those paint edge to edge and inset their content with padding
	   rather than centring it in `--bp-container`, so above 1920 their copy kept
	   hugging the viewport while every ordinary section sat in a 400px gutter.
	   Design review, 31 Aug 2026: "apply the maximum container width 1760px rule
	   above 1920px screens".
	   ⚠️ NO MEDIA QUERY, and none is needed: at 1920 `(1920 - 1760) / 2` is exactly
	   80, which is `--bp-side` at that width, so the two arms meet and the curve is
	   continuous. Below 1920 `--bp-side` always wins and nothing moves; above it the
	   gutter opens so the content lines up with the 1760 container. */
	--bp-gutter: max(var(--bp-side), calc((100vw - var(--bp-container-max)) / 2));
}

/* ── 375 → 768 ─────────────────────────────────────────────────────────── */
@media (min-width: 375px) {
	:root {
		--bp-fs-h1: calc(32px + 12 * (100vw - 375px) / 393);
		--bp-fs-h2: calc(28px + 10 * (100vw - 375px) / 393);
		--bp-fs-h3: calc(24px + 4 * (100vw - 375px) / 393);
		--bp-fs-h4: calc(20px + 8 * (100vw - 375px) / 393);
		--bp-fs-h5: calc(20px + 2 * (100vw - 375px) / 393);
		--bp-fs-h6: calc(18px + 2 * (100vw - 375px) / 393);
		--bp-fs-eyebrow: calc(14px + 2 * (100vw - 375px) / 393);
		--bp-fs-eyebrow-s: calc(14px + 2 * (100vw - 375px) / 393);
	}
}

/* ── 768 → 1024 (tablet) ───────────────────────────────────────────────── */
@media (min-width: 768px) {
	:root {
		--bp-fs-h1: calc(44px + 4 * (100vw - 768px) / 256);
		--bp-fs-h2: 38px;
		--bp-fs-h3: calc(28px + 6 * (100vw - 768px) / 256);
		--bp-fs-h4: 28px;
		--bp-fs-h5: 22px;
		--bp-fs-h6: 20px;
		--bp-fs-eyebrow: 16px;
		--bp-fs-eyebrow-s: 16px;

		/* Body steps rather than scales — the log's "body text does not
		   shrink" holds from here up; the step to 16 happens only at mobile.
		   Confirmed against BP Standard Pages Mobile.dc.html:109. */
		--bp-fs-body: 18px;
		--bp-lh-long: 2;

		--bp-side: 40px;
		--bp-inset: 16px;
		--bp-section: 80px;
		--bp-header-h: 76px;

		--bp-header-pad: 40px;
		--bp-logo-h: 40px;
		--bp-burger-bar-w: 24px;
		--bp-burger-bar-gap: 6px;

		--bp-btn-gap: 16px;  /* side by side, content width */
	}
}

/* ── 1024 → 1440 ───────────────────────────────────────────────────────── */
@media (min-width: 1024px) {
	:root {
		--bp-fs-h1: calc(48px + 14 * (100vw - 1024px) / 416);
		--bp-fs-h2: calc(38px + 16 * (100vw - 1024px) / 416);
		--bp-fs-h3: calc(34px + 10 * (100vw - 1024px) / 416);
		--bp-fs-h4: calc(28px + 4 * (100vw - 1024px) / 416);
		--bp-fs-h5: calc(22px + 2 * (100vw - 1024px) / 416);
		--bp-fs-eyebrow: calc(16px + 2 * (100vw - 1024px) / 416);
		--bp-fs-eyebrow-s: calc(16px - 2 * (100vw - 1024px) / 416);

		/* §8 states 76px at 1024 and 90px at 1440 and nothing between. Interpolating
		   rather than stepping keeps the bar consistent with the type scale, which
		   glides across the same range — a step would snap the whole header 14px
		   taller at exactly 1440. Same reasoning for the logo, 40 → 50. */
		--bp-header-h: calc(76px + 14 * (100vw - 1024px) / 416);
		--bp-logo-h: calc(40px + 10 * (100vw - 1024px) / 416);
	}
}

/* ── 1440 → 1920: proportional zoom ────────────────────────────────────── */
@media (min-width: 1440px) {
	:root {
		--bp-u: calc(100vw / 1440);

		--bp-fs-h1: calc(62 * var(--bp-u));
		--bp-fs-h2: calc(54 * var(--bp-u));
		--bp-fs-h3: calc(44 * var(--bp-u));
		--bp-fs-h4: calc(32 * var(--bp-u));
		--bp-fs-h5: calc(24 * var(--bp-u));
		--bp-fs-h6: calc(20 * var(--bp-u));
		--bp-fs-eyebrow: calc(18 * var(--bp-u));
		--bp-fs-eyebrow-s: calc(14 * var(--bp-u));
		--bp-fs-body: calc(18 * var(--bp-u));
		--bp-fs-small: calc(14 * var(--bp-u));
		--bp-fs-caption: calc(12 * var(--bp-u));
		--bp-fs-button: calc(13 * var(--bp-u));

		/* ⚠️ The measure scales WITH the type, and it did not before.
		   `--bp-detail` was a flat 900px while every font size above steps onto
		   `--bp-u`, so at 1920 the headings were 33% larger inside an unchanged
		   900px column and wrapped an extra line — the Learning Hub h1 went from 2
		   lines at 1440 to 3 at 1920, with dead space beside it. Design review, 31
		   Aug 2026: "the text container can be wider so there's less white spacing
		   on right". Scaling it keeps the measure in CHARACTERS constant, which is
		   the whole point of the `--bp-u` model, and resolves to exactly 900 at
		   1440 so no drawn frame moves. */
		--bp-detail: calc(900 * var(--bp-u));

		/* Side margin stays 80px — see the header note. */
		--bp-side: 80px;
		--bp-inset: calc(16 * var(--bp-u));
		--bp-section: calc(100 * var(--bp-u));
		--bp-header-h: calc(90 * var(--bp-u));
		--bp-logo-h: calc(50 * var(--bp-u));
		--bp-gap-intra: calc(30 * var(--bp-u));
		--bp-gap-inter: calc(60 * var(--bp-u));
	}
}

/* ── ≥1920: scaling stops, container locked at 1760 ────────────────────── */
@media (min-width: 1920px) {
	:root {
		--bp-u: 1.3333px;
	}
}

/* =========================================================================
   Base
   ========================================================================= */
/* Element defaults live in the `base` layer, which is ordered BELOW `blocks`.
   These are bare tag selectors, so without a layer they would beat a block's own
   class-based styling — unlayered CSS wins over any layer regardless of
   specificity. An `h1..h6` rule here would then silently restyle every heading
   inside every block. Anything that should *win* over blocks (theme chrome,
   project overrides) stays unlayered, below. */
@layer base {
	*, *::before, *::after { box-sizing: border-box; }
	/* ⚠️ Motion, so it is switched off when the reader asks for less. A smooth
	   scroll is exactly the kind of movement `prefers-reduced-motion` is for, and
	   the anchor handler in theme.js passes `behavior: 'auto'` explicitly, so
	   nothing depends on this being on. */
	html { scroll-behavior: smooth; }

	@media (prefers-reduced-motion: reduce) {
		html { scroll-behavior: auto; }
	}
	body {
		margin: 0;
		font-family: var(--bs-body);
		font-size: var(--bp-fs-body);
		line-height: var(--bp-lh-body);
		color: var(--bs-text);
		background: var(--bs-white);
		-webkit-font-smoothing: antialiased;
	}
	body.has-mobile-menu { overflow: hidden; }
	img { max-width: 100%; height: auto; display: block; }
	/* Accent/Mid-green. NOT --bs-primary: Brand/Primary #4EC278 is a bright
	   green that fails contrast as text on white. Token sheet §2b gives the
	   link colour as Accent/Mid-green on light, Brand/Primary on dark. */
	a { color: var(--bs-primary-light); }
	/* Deliberately no line-height or letter-spacing here. theme.json sets both
	   per-element (1.2 on H1–H3, 1.4 on H4–H6, tracking 0 throughout), and its
	   generated CSS lands in this same `base` layer at the same specificity —
	   so a blanket rule here would be decided by stylesheet order, not intent.
	   The starter's -0.01em tracking was a starter default; every Heading token
	   in §2a specifies 0. */
	h1, h2, h3, h4, h5, h6 { font-family: var(--bs-heading); font-weight: 600; }
}

.site-wrapper { overflow-x: clip; }
.site-main { min-height: 40vh; }

/* -------------------------------------------------------------------------
 * Prose typography — cascade layer
 * -------------------------------------------------------------------------
 * Everything that styles the_content() output by ELEMENT (p, headings, lists,
 * blockquote, figure…) belongs in this layer.
 *
 * Why: prose rules are written against bare tags, so they also match the markup
 * inside an LC block and restyle it by accident. lc-blocks puts its CSS in the
 * `blocks` layer, which is ordered above `prose`, so block styling always wins —
 * no `:not([class*="lc-"])` guard needed on each selector.
 *
 * Note what is NOT in here: the alignment breakout below is structural layout
 * that must beat block CSS, so it stays unlayered. Theme chrome stays unlayered
 * too — layering it would hand the cascade to unlayered plugin CSS (Gravity
 * Forms, ACF) and to WordPress core's own block styles.
 *
 * A project's own theme CSS is unlayered and therefore beats both layers, which
 * is the intended way to re-skin a block.
 * ---------------------------------------------------------------------- */
@layer prose {
	/* Centered content width for non-block template bodies (prose pages, single body) */
	.article-content { max-width: var(--bs-content); margin-inline: auto; padding-inline: 24px; }

	/* Inter-element rhythm. If per-element margins are added later, drop this owl —
	   a more specific per-element rule silently overrides it. */
	.article-content > * + * { margin-top: 1.2em; }

	/* LC blocks carry their own vertical spacing (block.json `style.spacing.margin`),
	   so the prose rhythm must not stack on top of it.
	   This reset is NOT redundant with the layer order. Layers only arbitrate a
	   *conflict* — where both sides declare the same property. Here the owl declares
	   margin-top and the block declares none (only 2 of 21 block roots declare any
	   margin), so there is nothing to arbitrate and prose applies unopposed. An
	   explicit reset is the only thing that stops it. */
	.article-content > [class*="wp-block-lc-"] { margin-top: 0; }
}

/* -------------------------------------------------------------------------
 * Block alignment — the theme owns this (lc-blocks' fallback option is off).
 * Full blocks fill the viewport; wide blocks cap at the wide size. lc blocks
 * self-constrain their own inner content, so block-driven pages render in a
 * full-width flow (front-page.php / page.php block-hero mode) and these rules
 * size the aligned blocks. Normal (un-aligned) content stays contained.
 * ---------------------------------------------------------------------- */
.alignwide {
	max-width: var(--wp--style--global--wide-size, 1760px);
	margin-left: auto;
	margin-right: auto;
	box-sizing: border-box;
}
.alignfull {
	width: 100%;
	max-width: none;
}
/* Colored separators between showcase sections render as a thin rule. */
.wp-block-separator.has-background:not(.is-style-dots) { border: 0 !important; height: 1px; }
.wp-block-separator.has-background:not(.is-style-dots):not(.alignwide):not(.alignfull) { width: 100%; }

/* -------------------------------------------------------------------------
 * Aligned blocks inside a constrained content column
 * -------------------------------------------------------------------------
 * The rules above size aligned blocks in a full-width flow. Inside a container
 * that caps the_content() at a content width, they cannot escape it: `.alignfull`
 * resolves to the column width, not the viewport, and alignleft/alignright get no
 * float at all. These rules break them back out.
 *
 * Written ONCE for every such container via :is(). To cover a new container, add
 * it to the two lists below — do NOT copy the rule bodies. (Duplicating these per
 * container is how they drift out of sync.)
 * ---------------------------------------------------------------------- */
/* Default (align-none) content in the FULL-BLEED flow.
 * -------------------------------------------------------------------------
 * `.page-blocks` is deliberately unconstrained — the lc sections inside it are
 * mostly align-none and take their width from the container, then self-constrain
 * with `--bp-container`. That left ordinary content with no width at all.
 * Measured on `/human/for-providers/` at 1440 before this rule: paragraph,
 * heading and list all `x=0 w=1440`, running edge to edge — and because the wide
 * rule below clamps to `100vw - 2 * gutter`, **`.alignwide` came out NARROWER
 * than align-none**, 1392 against 1440. The whole ladder was inverted, which is
 * what the content-entry note of 8 Sep 2026 reported.
 *
 * `--bp-detail` is the site's reading column and the same token
 * `.page-content__inner` uses, so a paragraph now measures identically whether
 * or not the page has a block hero — the two page modes finally agree. Chosen
 * over `--bp-container` (which would have aligned prose with the sections'
 * inner content) on the developer's call, 8 Sep 2026: measure won over
 * alignment, and it is also what engagys does for the same case.
 *
 * ⚠️ `:not([class*="wp-block-lc-"])` IS LOAD-BEARING. On the key and landing
 * pages `feature-split`, `accordion`, `related-posts` and `contact-form` are all
 * align-none — measured, not assumed — so capping every align-none child would
 * collapse every section on three pages to the reading width. engagys can put a
 * plain `max-width` on its own `.page-blocks` because its lc blocks are marked
 * wide or full; ours are not.
 *
 * The `100% - 2 * --bp-side` term is the gutter: `.page-blocks` has no padding
 * of its own, so without it the text would touch the screen edges below 900.
 * ---------------------------------------------------------------------- */
.page-blocks > :not([class*="wp-block-lc-"]):not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright) {
	max-width: min(var(--bp-detail), calc(100% - 2 * var(--bp-side)));
	margin-inline: auto;
}

/* Wide: the lc sections' content width, so a wide core block lines up with the
 * content inside the lc blocks around it. Every lc section centres its inner
 * content in `--bp-container`; this is the same token, so the two edges are the
 * same number at every width (scrollbar term included).
 *
 * Before (PM request, Oct 2026): `min(1760, 100vw - 2 * 24px)`, and `width: auto`
 * at ≤1024 — which in the full-width `.page-blocks` flow meant the screen edge.
 * Measured on a probe page, wide heading / paragraph / columns against the
 * Contact Form's inner content: x=0 vs 20 at 375, 0 vs 33 at 768, 17 vs 73 at
 * 1440. They only agreed at ≥1920, where both cap at 1760. */
:is(.article-content, .page-blocks) > .alignwide {
	--bs-wide-w: var(--bp-container);
	width: var(--bs-wide-w);
	max-width: none;
	margin-left: calc((100% - var(--bs-wide-w)) / 2);
	margin-right: calc((100% - var(--bs-wide-w)) / 2);
	box-sizing: border-box;
}

/* Full: reach the viewport edges. */
:is(.article-content, .page-blocks) > .alignfull,
/* lc blocks are full-bleed sections that own their inner width via the
   block-inner mixin, so their wide variant is forced full-viewport too — else
   their painted background stops short of the edge. */
:is(.article-content, .page-blocks) > .alignwide[class*="wp-block-lc-"] {
	width: 100vw;
	max-width: 100vw;
	margin-left: calc(50% - 50vw);
	margin-right: 0;
}

/* ⚠️ `100vw` INCLUDES THE SCROLLBAR, and `.page-blocks` does not — so the pair
 * above is wrong by half a scrollbar in a browser that reserves space for one.
 * Reported 8 Sep 2026 on `/human/for-providers/`: the full-width paragraph's
 * first character was cut off at the left edge. Simulated with a 15px
 * scrollbar and measured at a 1440 viewport:
 *
 *     .page-blocks  1425      (the layout width)
 *     .alignfull    x=-7.5  w=1440  right=1432.5
 *
 * 7.5px lost off the left, 7.5px hanging past the right. ⚠️ IT WAS NEVER ONLY
 * THE PARAGRAPH — the lc full-bleed sections measured identically, so every
 * band on the site was half a scrollbar off-centre and a whole one too wide.
 *
 * ⚠️ NO HARNESS CAN SEE THIS. Headless Chromium uses overlay scrollbars, so
 * `innerWidth - clientWidth` is 0 in CI and the geometry looks perfect. It has
 * to be simulated (`html,body{max-width:calc(100vw - 15px)}`) or seen in a real
 * browser. Same blind spot as the 1200px touch-header overflow.
 *
 * `.page-blocks` already spans the layout width, so a full-bleed child there
 * needs no breakout at all — `100%` is exactly right and cannot include a
 * scrollbar. Kept separate rather than folded into the `:is()` above because
 * the two containers genuinely differ: `.article-content` sits in a 900 column
 * and does need to break out.
 *
 * ⚠️ `.article-content` STILL HAS THIS BUG and is not fixed here. There is no
 * pure-CSS breakout from a narrow centred column that avoids `100vw` — the
 * options are a JS-set `--bs-vw`, or `container-type: inline-size` on a
 * full-width ancestor so `100cqw` can be used, which brings `contain: layout`
 * and its own consequences. A decision, not a patch. */
.page-blocks > .alignfull,
.page-blocks > .alignwide[class*="wp-block-lc-"] {
	width: 100%;
	max-width: 100%;
	margin-left: 0;
	margin-right: 0;
}

/* Prose-level lc blocks sit in the reading column, like a paragraph — on every
 * page type. They are not sections: a quote, a callout, a row of buttons, an
 * eyebrow, a list of links, the share row and FAQ List read as part of the text
 * around them. Every other lc block is a full-width section (above).
 *
 * ⚠️ MUST STAY AFTER the rule above. A legacy `alignwide` class (blocks no
 * longer have an Align control, but stored content keeps the class) would
 * otherwise win: `:is()` takes its most specific argument, so this selector is
 * (0,3,0) — the same as `.page-blocks > .alignwide[class*=…]` — and the later
 * rule wins the tie. Add a new prose-level block to this list. */
.page-blocks > :is(
	.wp-block-lc-quote,
	.wp-block-lc-callout,
	.wp-block-lc-buttons,
	.wp-block-lc-eyebrow,
	.wp-block-lc-direct-links,
	.wp-block-lc-share,
	.wp-block-lc-test-results-lookup,
	.wp-block-lc-accordion.is-style-inline
) {
	width: auto;
	max-width: min(var(--bp-detail), calc(100% - 2 * var(--bp-side)));
	margin-inline: auto;
}

/* The EDITOR CANVAS has no `.page-blocks`: its root container is the well there.
 * WordPress gives it a constrained layout from theme.json's contentSize (1280) at
 * zero specificity, which used to be overruled per block by the Align support.
 * With no Align control, every section would sit at 1280 in the editor and every
 * paragraph at 1280 too. These mirror the three front-end rules above, so a block
 * is the same width in the canvas as on the page: prose and prose-level blocks in
 * the 900 column, lc sections full width. (theme.json's contentSize is left at
 * 1280 on purpose: core group blocks use it on the front end.) */
.is-root-container > :not([class*="wp-block-lc-"]):not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright) {
	max-width: min(var(--bp-detail), calc(100% - 2 * var(--bp-side)));
}
.is-root-container > [class*="wp-block-lc-"] {
	max-width: none;
}
/* Wide core blocks at the lc content width, as on the front end. Without this,
   core's canvas rule caps them at theme.json's wideSize (1760), which is wider
   than any canvas, so they ran edge to edge. The first :not() keeps lc sections
   full; the second is for WEIGHT — core's rule,
   `.block-editor-block-list__layout.is-root-container > .alignwide`, is (0,3,0)
   and comes later, so at (0,3,0) this one measured no change at all. */
.is-root-container > .alignwide:not([class*="wp-block-lc-"]):not(.alignfull) {
	max-width: var(--bp-container);
	margin-inline: auto;
}
/* ⚠️ The two :not()s are for WEIGHT: core's canvas layout rule (the constrained
   root, capping at contentSize 1280) is (0,2,0) and comes later. This rule used to
   win only because one item below has two classes and :is() takes its heaviest
   argument, so removing FAQ List from the list would have put every Quote and
   Callout at full width in the editor. (Found porting this to the starter, whose
   list is all single classes: the Quote measured full canvas width there.) */
.is-root-container > :not(.alignleft):not(.alignright):is(
	.wp-block-lc-quote,
	.wp-block-lc-callout,
	.wp-block-lc-buttons,
	.wp-block-lc-eyebrow,
	.wp-block-lc-direct-links,
	.wp-block-lc-share,
	.wp-block-lc-test-results-lookup,
	.wp-block-lc-accordion.is-style-inline
) {
	max-width: min(var(--bp-detail), calc(100% - 2 * var(--bp-side)));
	margin-inline: auto;
}

/* Floated images. */
:is(.article-content, .page-blocks) > :is(.wp-block-image, figure).alignleft {
	float: left;
	margin: 0.5em 2em 1em 0;
	max-width: 50%;
}
:is(.article-content, .page-blocks) > :is(.wp-block-image, figure).alignright {
	float: right;
	margin: 0.5em 0 1em 2em;
	max-width: 50%;
}
@media (max-width: 768px) {
	:is(.article-content, .page-blocks) > :is(.wp-block-image, figure).alignleft,
	:is(.article-content, .page-blocks) > :is(.wp-block-image, figure).alignright {
		float: none;
		margin-inline: 0;
		max-width: 100%;
	}
}

/* Visually hidden, still announced and still focusable. Standard WP pattern. */
.screen-reader-text {
	border: 0;
	clip-path: inset(50%);
	height: 1px;
	margin: -1px;
	overflow: hidden;
	padding: 0;
	position: absolute;
	width: 1px;
	word-wrap: normal !important;
}
/* The skip link is the exception: it must become visible when focused. */
.skip-link:focus {
	clip-path: none;
	height: auto;
	width: auto;
	margin: 0;
	padding: 12px 20px;
	top: 8px;
	left: 8px;
	z-index: 1000;
	background: var(--bs-white);
	color: var(--bs-text);
	border-radius: 4px;
	box-shadow: 0 2px 12px rgba(0,0,0,.2);
	text-decoration: underline;
}

/* =========================================================================
   Header
   =========================================================================
   Three drawn frames — BP Header.dc.html (1440/90), BP Homepage Tablet.dc.html:52
   (1024/76, abbreviated variant), BP Homepage Mobile.dc.html:50 (375/64). Heights,
   bar inset, logo height and hamburger bar metrics all come from the responsive
   scale at the top of this file.

   The desktop nav switches in at 1200, not 1024: the tablet comp measures the six
   labels at ~620px, which does not fit beside the logo and CTA at 1024. 1200 is
   also the log's desktop breakpoint.
   ========================================================================= */
.site-header {
	position: sticky;
	top: 0;
	z-index: 100;
	background: var(--bs-white);
	/* Only `transform` — the shadow's arrival stays instant, which is the
	   behaviour the design review signed off on. */
	transition: transform .35s ease;
}

/* Hide on scroll down, reveal on scroll up — design review, 8 Sep 2026. The class
   is toggled in theme.js; see the guards there for when it deliberately will not
   apply. `sticky` means the translate slides the bar out of its stuck position
   with no body-padding compensation needed. */
.site-header.is-hidden {
	transform: translateY(-100%);
}

/* ⚠️ Reduced motion keeps the header put. The JS never adds `is-hidden` in that
   case, and this is the belt to that braces — a stray class must not move a
   landmark for a reader who asked for less motion. */
@media (prefers-reduced-motion: reduce) {
	.site-header {
		transition: none;
	}

	.site-header.is-hidden {
		transform: none;
	}
}
.site-header__inner {
	height: var(--bp-header-h);
	box-sizing: border-box;
	padding-inline: var(--bp-header-pad);
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 24px;
}

.site-header__brand { display: inline-flex; align-items: center; flex: 0 0 auto; }
.site-header__logo { height: var(--bp-logo-h); width: auto; }

/* Desktop nav — hidden until the labels fit. */
.site-header__nav { display: none; }
.site-header__menu {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	align-items: center;
	gap: 50px;
}
.site-header__menu a {
	font-family: var(--bs-heading);
	font-size: 16px;
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	color: var(--bs-dark);
	text-decoration: none;
	white-space: nowrap;
	transition: color .2s ease;
}
.site-header__menu .current-menu-item > a { color: var(--bs-primary-light); }
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.site-header__menu a:hover { color: var(--bs-primary-light);
	}
}

/* ── Desktop dropdowns ────────────────────────────────────────────────────
 * ⚠️ NOT DESIGNED — no frame, component or annotation in the package draws a
 * desktop submenu (the package's only "dropdown" is the Learning Hub filter).
 * Built 14 Aug 2026 on the developer's instruction. Every value below is reused
 * rather than invented: Neutral/White surface, Neutral/200 hairline, the single
 * 4px system radius, the top-level label's own Geist 16/500, and the drawer's
 * subordinate 12px row rhythm. No shadow — the only shadow the token sheet
 * sanctions is the Nav TEXT shadow at 30%, which is far too heavy for a panel,
 * and a lighter one would be a value from nowhere. Flagged in dev-log.md.
 *
 * The <li> stretches to the full bar height so the panel can hang off its bottom
 * edge with no dead gap for the pointer to cross on the way down.
 */
/* The nav and menu have to carry the bar's full height for `top: 100%` on the
   panel to mean "the bottom of the header bar". Without this the menu is only as
   tall as its own text line — it is a centred flex item of `__inner` — and the
   panel hangs from mid-bar, overlapping it by 29–48px depending on frame.
   `align-items: center` on the menu keeps the labels where they were. */
.site-header__nav { align-self: stretch; }
.site-header__menu { height: 100%; }
.site-header__menu > li { position: relative; display: flex; align-items: center; gap: 4px; }
.site-header__menu > li.has-children { align-self: stretch; }

.site-header__expand {
	flex-shrink: 0;
	width: 24px;
	height: 24px;
	padding: 0;
	margin-right: -4px;
	background: none;
	border: 0;
	cursor: pointer;
	color: var(--bs-dark);
	display: flex;
	align-items: center;
	justify-content: center;
}
.site-header__expand svg { transform: rotate(90deg); transition: transform .2s ease; }
.site-header__expand[aria-expanded="true"] svg { transform: rotate(-90deg); }

/* Design review, 31 Aug 2026: no caret at desktop — with sub-items coming under
   every top-level item, six carets cost ~168px of bar (28 each) and, once every
   item has one, distinguish nothing.
   ⚠️ GATED ON `hover: hover`, NOT on width alone, and that is the whole point.
   The button is the ONLY way a touch device opens a submenu: hovering opens it
   for a mouse, `focusin` opens it for a keyboard, and the click handler exists
   precisely for `(hover: none)` — see the note in theme.js. Hiding it on width
   alone would strand a touch laptop or an iPad in landscape at >=1200 with no way
   to reach a submenu at all.
   `display: none` rather than a zero-size box: the button must leave the tab
   order with the caret, or it becomes an invisible focus stop. It stays IN THE
   DOM, which matters — `setSub()` reads `aria-expanded` off it and returns early
   if it is missing, so removing it in PHP would disable hover and focus opening
   too. */
@media (min-width: 1200px) and (hover: hover) {
	.site-header__expand { display: none; }
}

.site-header__submenu {
	position: absolute;
	top: 100%;
	left: -16px;
	z-index: 10;
	list-style: none;
	margin: 0;
	padding: 8px 0;
	width: max-content;
	min-width: 200px;
	background: var(--bs-white);
	border: 1px solid var(--bs-neutral-200);
	border-radius: var(--bp-radius);
}
.site-header__submenu[hidden] { display: none; }
.site-header__submenu a {
	display: block;
	padding: 12px 20px;
	color: var(--bs-dark);
	text-shadow: none;
}
/* Hover gets the row wash; the current page gets colour only, which is how the
   top-level menu already marks itself (dev-rules §B — one treatment, not two).
   Worth knowing while the menu is still seeded with placeholders: four of Human's
   five children currently point at `/`, so on the homepage WordPress marks them
   all `current-menu-item` and colours the lot. That is the seed, not the rule. */
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.site-header__submenu a:hover { background: var(--bs-subtle);
	}
}
.site-header__submenu .current-menu-item > a { color: var(--bs-primary-light); }
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.site-header__submenu a:hover { color: var(--bs-primary-light);
	}
}

/* Hover and focus open the panel via JS, not via a CSS `:hover > { display: block }`
   rule. A CSS rule can reveal the panel but cannot touch `hidden` or
   `aria-expanded`, so the panel would be visible to a mouse user while still
   hidden from assistive tech and reported as collapsed. theme.js drives all three
   together — hover, focus and the button — so there is one state, not two.
   Hover is gated to `(hover: hover)` and ≥1200 in the script (§12). */

.site-header__actions { display: flex; align-items: center; gap: 40px; flex: 0 0 auto; }

/* "Careers". Inter 12 here, not the Geist Mono 12 of Text/Link — read from
   BP Header.dc.html:26. The token sheet flags it as an unexplained off-pattern
   case (§2b); built as drawn, confirm with design. */
.site-header__secondary {
	display: none;
	font-family: var(--bs-body);
	font-size: 12px;
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	color: var(--bs-dark);
	text-decoration: underline;
	text-underline-offset: 4px;
	white-space: nowrap;
}

/* CTA. Padding is 12/20 at desktop and 14/24 at tablet, read from the two frames.
   Neither is the §5a "Standard" 16/28 — the header CTA is its own compact size.
   Hover swaps fill for the Accent/CTA-Hover token; nothing moves or resizes. */
.site-header__cta {
	display: none;
	box-sizing: border-box;
	align-items: center;
	justify-content: center;
	padding: 14px 24px;
	border-radius: var(--bp-radius);
	font-family: var(--bs-body);
	/* ⚠️ A LITERAL 13, not `--bp-fs-button` — design review, 8 Sep 2026: "The
	   Contact text is huge". The token is `calc(13 * var(--bp-u))` above 1440,
	   and this button was the ONLY text in the bar reading it: the nav is a
	   hard 16 and `.site-header__secondary` a hard 12, both as drawn. So the
	   drawn hierarchy inverted on wide screens — measured CONTACT 13 / 14.4 /
	   17.3 at 1440 / 1600 / 1920 against a nav that stayed at 16, i.e. the CTA
	   went from smaller than the menu to larger than it, and the box from
	   115x40 to 140x45.

	   `BP Header.dc.html:27` draws this button as a FIXED box — `width: 114px;
	   height: 40px; padding: 12px 20px`, Inter 13/600/0.12em — so a literal is
	   what the comp states, and it is the same reasoning `--bp-header-pad`
	   already carries: header chrome holds its drawn size above 1440 while the
	   content grid scales. ⚠️ The bar's GEOMETRY still scales (height 90 -> 120,
	   logo 50 -> 67); that is deliberate and unchanged, so above 1440 a taller
	   bar holds drawn-size text.

	   Scoped to this one selector, and here is where each of `--bp-fs-button`'s
	   three other consumers stands. `.site-drawer__cta` never renders above 1200,
	   where --bp-u is 1px, so it is already 13 and nothing to do.
	   `.prefooter__btn` sits in a band whose heading DOES scale
	   (`calc(44 * var(--bp-u))`), so it is consistent as it is.

	   ⚠️ `.site-footer__subscribe input[type="submit"]` WAS THE SAME DEFECT and is
	   pinned the same way — found by sweeping the footer at 1440 vs 1920 rather
	   than assuming: heading 20/20, subscribe title 16/16, copyright 14/14, all
	   flat, while that button went 13 → 17.3 exactly as this one did. Approved and
	   fixed in the same session; the note lives on that rule. */
	font-size: 13px;
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	color: var(--bs-dark);
	text-decoration: none;
	white-space: nowrap;
}

/* 44px target at every width; only the bars inside change size. */
.site-header__burger {
	width: 44px;
	height: 44px;
	margin-right: -10px;
	flex-shrink: 0;
	background: none;
	border: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
	gap: var(--bp-burger-bar-gap);
	cursor: pointer;
	color: var(--bs-dark);
}
.site-header__burger span {
	display: block;
	width: var(--bp-burger-bar-w);
	height: 2px;
	background: currentColor;
}

@media (min-width: 768px) {
	.site-header__secondary,
	.site-header__cta { display: inline-flex; }
	.site-header__actions { gap: 28px; }
}

/* Coming-soon mode (inc/coming-soon.php) has no drawer, which is where the
   Contact button lives below 768, so the bar keeps it at every width. */
.is-coming-soon .site-header__cta { display: inline-flex; }
/* Beside the 163px logo it measured 10px past the screen edge at 320. The bar
   has 280px inside its gutters there: 163 logo + 12 gap + 103 button fits. */
@media (max-width: 374px) {
	.is-coming-soon .site-header__inner { gap: 12px; }
	.is-coming-soon .site-header__cta { padding-inline: 14px; }
}

@media (min-width: 1200px) {
	.site-header__nav { display: flex; flex: 1 1 auto; justify-content: center; }
	.site-header__burger { display: none; }
	.site-header__actions { gap: 40px; }
	.site-header__cta { padding: 12px 20px; }

	/* The two dropdown chevrons add 56px to the menu, and at 1200 the bar was
	   already close: measured 1224 needed of 1200 (212 logo + 703 nav + 220
	   actions + 40 padding + 48 inner gaps). The 50px label gap is a 1440 value
	   and the design has no 1200 frame, so it interpolates across the range
	   (pattern-inventory §15) rather than being cut flat. */
	.site-header__menu { gap: calc(40px + 10 * (100vw - 1200px) / 240); }
}

/* Interpolation stops at the frame it was read from. */
@media (min-width: 1440px) {
	.site-header__menu { gap: 50px; }
}

/* Standard button steps up at desktop. 1200 rather than 1440 to match where the
   nav switches — the design states values at 1024 and 1440 and nothing between,
   and switching both at one breakpoint keeps the bar internally consistent. */
@media (min-width: 1200px) {
	:root {
		--bp-btn-pad-y: 18px;
		--bp-btn-pad-x: 32px;
		--bp-btn-gap: 8px;
	}
}

/* ── Variants ─────────────────────────────────────────────────────────────
   The design draws two: light-bg/dark (solid) and transparent-bg/light over
   hero photography. `light` is the light-COLOURED mark for dark grounds and
   `dark` the dark-coloured one for the white bar — the starter's naming. */
.site-header__logo--light { display: none; }
.site-header__logo--dark { display: block; }

.site-header--solid { background: var(--bs-white); }

/* ⚠️ FIXED, NOT ABSOLUTE, and it must stay out of flow in BOTH states.
   This was `absolute`, flipping to `sticky` once `.is-scrolled` landed — and that
   flip put the header INTO the document flow, pushing everything below it down by
   its own height. Measured on `/for-veterinary-providers/` at the 40px threshold:
   document height 6781 → 6872 (+91px, the header's height) with a 0.0632 layout
   shift on `.site-main` four milliseconds later. That was the jump.

   It also meant the bar scrolled away for the first 40px and then snapped back to
   the top, which is the second half of what the jank looked like.

   `fixed` keeps it out of flow throughout, so `.is-scrolled` now changes only paint
   — background, colours, shadow — and never layout. Clearance is unchanged: these
   heroes already sit under the bar at rest, and the key-page hero pads itself by
   `calc(var(--bp-header-h) + var(--bp-section))` for exactly this reason.

   Only this variant is affected. `--solid` and `--dark` declare no position and
   inherit `sticky` from the base, which is right for them: they have no hero to
   overlay, so occupying flow space is what should happen. */
.site-header--transparent {
	position: fixed;
	inset-inline: 0;
	background: transparent;
	border-bottom: 1px solid rgba(255, 255, 255, .18);
}
.site-header--transparent .site-header__logo--light { display: block; }
.site-header--transparent .site-header__logo--dark { display: none; }
/* `> li >` matters: these were written when the desktop menu was depth-1, so a
   bare `a` meant "a top-level label". Now that parents carry a dropdown, an
   unscoped selector would paint the panel's links white-on-white and give them a
   text shadow they must not have. */
.site-header--transparent .site-header__menu > li > a,
.site-header--transparent .site-header__secondary { color: var(--bs-white); }
.site-header--transparent .site-header__burger,
.site-header--transparent .site-header__expand { color: var(--bs-white); }
/* Text Shadow effect token (§2f) — #0A211F @ 30%, offset (0,2), radius 20. */
.site-header--transparent .site-header__menu > li > a,
.site-header--transparent .site-header__secondary,
.site-header--transparent .site-header__brand {
	text-shadow: 0 2px 20px rgba(10, 33, 31, .30);
}
.site-header--transparent .site-header__burger span {
	box-shadow: 0 2px 20px rgba(10, 33, 31, .30);
}

/*
 * Scrolled state for the transparent header — NOT designed.
 * The design shows the transparent bar over hero photography only, and says
 * nothing about what happens once the hero scrolls past. Left alone, a sticky
 * transparent bar puts white labels on white page content, which is a legibility
 * failure rather than a style choice. It reverts to the solid treatment instead.
 * CONFIRMED BY DESIGN 31 Aug 2026 — the review asked for a shadow ON this state,
 * which settles that the state itself is wanted. The reversion to solid still has
 * no drawn frame; only its existence is now agreed.
 */
/* Design review, 31 Aug 2026: a light shadow so the stuck bar reads as separate
   from the page content.
   Modelled on the reference site's `.is-peeking` rule but NOT copied from it —
   that shadow is `#201a3d0f`, a purple keyed to that brand. Same geometry and
   the same 6% weight, in Boothwyn's own Brand/Dark.
   ⚠️ On `.site-header`, not on `--transparent`. The reference scopes it to its
   transparent variant, but here EVERY page's header is sticky and `is-scrolled`
   is toggled regardless of variant, so scoping it that way would leave the
   interior templates stuck against their content with no separation — the exact
   thing this was asked for.
   ⚠️ This is the token sheet's SECOND sanctioned shadow; the only other is the
   Nav text shadow at 30%. The dropdown panel is still deliberately shadowless
   (see the note above it) — raise the two together with design. */
.site-header.is-scrolled {
	box-shadow: 0 2px 8px rgba(12, 51, 48, .06);
}

.site-header--transparent.is-scrolled {
	/* No `position` here on purpose — see the note above. Changing it was the jump. */
	background: var(--bs-white);
	border-bottom-color: transparent;
}
.site-header--transparent.is-scrolled .site-header__logo--light { display: none; }
.site-header--transparent.is-scrolled .site-header__logo--dark { display: block; }
.site-header--transparent.is-scrolled .site-header__menu > li > a,
.site-header--transparent.is-scrolled .site-header__secondary { color: var(--bs-dark); text-shadow: none; }
.site-header--transparent.is-scrolled .site-header__burger,
.site-header--transparent.is-scrolled .site-header__expand { color: var(--bs-dark); }
.site-header--transparent.is-scrolled .site-header__burger span { box-shadow: none; }
.site-header--transparent.is-scrolled .site-header__brand { text-shadow: none; }

/* =========================================================================
   Drawer (mobile + tablet navigation)
   =========================================================================
   Full-screen Brand/Dark overlay. The top bar repeats the collapsed header
   height so the logo does not move when it opens. Item metrics from
   BP Homepage Mobile.dc.html:516 and :521.
   ========================================================================= */
.site-drawer {
	position: fixed;
	inset: 0;
	z-index: 200;
	background: var(--bs-dark);
	overflow-y: auto;
	display: flex;
	flex-direction: column;
}
.site-drawer[hidden] { display: none; }

.site-drawer__top {
	height: var(--bp-header-h);
	flex-shrink: 0;
	box-sizing: border-box;
	padding-inline: var(--bp-header-pad);
	display: flex;
	align-items: center;
	justify-content: space-between;
}
.site-drawer__brand img { height: var(--bp-logo-h); width: auto; display: block; }
.site-drawer__close {
	width: 44px;
	height: 44px;
	margin-right: -10px;
	flex-shrink: 0;
	background: none;
	border: 0;
	padding: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	color: var(--bs-white);
}

.site-drawer__menu,
.site-drawer__submenu { list-style: none; margin: 0; padding: 0; }

/* Each row is followed by a hairline that runs edge to edge with no gutter, so
   the rule sits on the <li> and the padding lives on the row inside it. */
.site-drawer__menu > li { border-bottom: 1px solid var(--bs-primary-light); }

.site-drawer__row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	padding: 16px var(--bp-header-pad);
}
.site-drawer__link {
	flex: 1 1 auto;
	font-family: var(--bs-body);
	font-size: 20px;
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	color: var(--bs-white);
	text-decoration: none;
}
.site-drawer__expand {
	flex-shrink: 0;
	width: 44px;
	height: 44px;
	margin-right: -10px;
	background: none;
	border: 0;
	padding: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	color: var(--bs-white);
}
/* Chevron-right closed, chevron-up open. */
.site-drawer__expand svg { transition: transform .2s ease; }
.site-drawer__expand[aria-expanded="true"] svg { transform: rotate(-90deg); }

/* Sub-items: indented to the 20px gutter plus a 20px step, no dividers, and a
   tighter 12px rhythm so they read as subordinate to the 16px parent rows. */
.site-drawer__submenu { padding-bottom: 8px; }
.site-drawer__submenu[hidden] { display: none; }
.site-drawer__submenu .site-drawer__row { padding: 12px var(--bp-header-pad) 12px calc(var(--bp-header-pad) + 20px); }
.site-drawer__submenu .site-drawer__link { font-size: 16px; }

.site-drawer__actions {
	margin-top: auto;
	padding: 24px var(--bp-header-pad) 40px;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: 20px;
}
.site-drawer__cta {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 16px 28px;
	border-radius: var(--bp-radius);
	font-family: var(--bs-body);
	font-size: var(--bp-fs-button);
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	color: var(--bs-dark);
	text-decoration: none;
}
/* Design review, 31 Aug 2026 — match the drawn treatment.
   `BP Homepage Mobile.dc.html:449` (the Nav — Expanded frame) draws this as an
   ordinary drawer row that happens to be underlined: Inter 20 / 500, sentence
   case, left aligned. The build had it as a small centred uppercase label —
   12 / 600 with wide tracking — which is the TABLET bar's treatment
   (`.site-header__secondary`), not the drawer's. The two share a name and a
   purpose but not a type ramp.
   It now carries its own `padding: 16px var(--bp-header-pad)` — the comp's
   `padding: 16px 20px`, and the same declaration `.site-drawer__row` uses, so it
   lines up with the rows above it. It gained that padding when it MOVED OUT of
   `.site-drawer__actions` (which had been supplying the horizontal half) to sit
   under the menu where the comp draws it.
   ⚠️ No bottom border, deliberately: the comp's last divider belongs to the menu
   row above, and `.site-drawer__menu > li` already draws it. */
.site-drawer__secondary {
	display: block;
	padding: 16px var(--bp-header-pad);
	font-family: var(--bs-body);
	font-size: 20px;
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	color: var(--bs-white);
	text-decoration: underline;
	text-underline-offset: 4px;
}

/* CONTACT and Careers stay in the tablet bar, so the drawer does not repeat them.
   ⚠️ BOTH selectors are required. Careers used to live inside `__actions`, so
   hiding that one block covered it; it now sits under the menu where the comp
   draws it (design review, 31 Aug 2026) and needs hiding in its own right.
   Without the second line the tablet drawer and the tablet bar both render
   Careers — measured, before this line was added. */
@media (min-width: 768px) {
	.site-drawer__actions,
	.site-drawer__secondary { display: none; }
}
@media (min-width: 1200px) {
	.site-drawer { display: none; }
}

/* =========================================================================
   Pre-footer CTA
   ========================================================================= */
/* The band is drawn as part of the FOOTER component in Figma (`Footer CTA`,
   1440 × 220 at the top of the Desktop Footer instance), not as a page section —
   which is why it lives here rather than in a block. The larger contact section
   with the form is a separate, page-placed block; a page that carries that block
   switches this band off with `hide_prefooter`.

   Desktop arithmetic closes exactly: 40 + max(106, 140) + 40 = 220, with both
   columns centred against each other. */
.prefooter { background: var(--bs-subtle); }
.prefooter__inner {
	max-width: var(--bp-container);
	margin-inline: auto;
	padding-block: 60px;
	display: flex;
	flex-direction: column;
	gap: 24px;
}
.prefooter__aside {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 24px;
}
/* 28 / 38 / 38 / 44 across the frames — H2's row below 1024 and H3's value at
   1440, so it belongs to no single token. Interpolated across the top range so
   both stated ends land and nothing jumps. Flagged in dev-log with the
   Continuous Improvement statement, which is off-token the same way. */
.prefooter__heading { font-size: var(--bp-fs-h2); margin: 0; color: var(--bs-dark); }
.prefooter__description {
	margin: 0;
	font-family: var(--bs-body);
	font-size: var(--bp-fs-body);
	font-weight: 400;
	line-height: var(--bp-lh-body);
	color: var(--bs-text);
	text-wrap: pretty;
}
@media (min-width: 768px) {
	.prefooter__inner {
		flex-direction: row;
		align-items: flex-start;
		justify-content: space-between;
		gap: 40px;
	}
	.prefooter__heading { flex: 1; min-width: 0; }
	/* 324 at 768, 452 at 1024, 520 at 1440 — interpolated so each stated frame
	   lands and the heading never gets squeezed. At a flat 452 the 768 frame
	   left the heading 196px and wrapped it to four lines. */
	.prefooter__aside { width: calc(324px + 128 * (100vw - 768px) / 256); flex-shrink: 0; }
}
@media (min-width: 1024px) {
	.prefooter__heading { font-size: calc(38px + 6 * (100vw - 1024px) / 416); }
	.prefooter__aside { width: calc(452px + 68 * (100vw - 1024px) / 416); }
}
@media (min-width: 1200px) {
	.prefooter__inner {
		padding-block: 40px;
		align-items: center;
	}
	.prefooter__heading { max-width: 404px; }
	.prefooter__aside { width: 520px; gap: var(--bp-gap-intra); }
}
@media (min-width: 1440px) {
	.prefooter__heading { font-size: calc(44 * var(--bp-u)); max-width: calc(404 * var(--bp-u)); }
	.prefooter__inner { padding-block: calc(40 * var(--bp-u)); }
	.prefooter__aside { width: calc(520 * var(--bp-u)); }
}
.prefooter__btn {
	padding: 16px 28px;
	color: var(--bs-dark);
	border-radius: var(--bp-radius);
	text-decoration: none;
	font-family: var(--bs-body);
	font-size: var(--bp-fs-button);
	font-weight: 600;
	/* Was MISSING, so this button alone inherited the body's 1.6 and rendered
	   52.8px tall where every other CTA on the site is 47.6. Five comp instances
	   of this exact button state `line-height: 1.2`, and `.site-header__cta`,
	   `.site-drawer__cta` and `lc/buttons --cta` all already declared it. QA Q39. */
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
}
/* =========================================================================
   CTA hover wipe — the theme-side half
   =========================================================================
   `BP Hover Effects.dc.html` §01: the CTA-Hover gradient wipes in left-to-right
   and the label turns Neutral/White.

   These three are every theme control on the CTA colour scheme — `background:
   var(--bs-primary)` with `color: var(--bs-dark)`. They are the same button in
   three places (all three read the same Site Settings Contact CTA), so they get
   one definition rather than three copies (dev-rules §B).

   ⚠️ NOT `.error-404__btn`, which also uses `--bs-primary`: it sets `color: #fff`
   and `border-radius: 999px`, i.e. it never had a Boothwyn pass and is not on this
   scheme. Logged rather than folded in — see dev-log QA Q37.

   ⚠️ `.site-drawer__cta` had NO hover rule at all before this, so for that one this
   ADDS behaviour rather than animating what was there. Deliberate, on the
   developer's call: it is the drawer's copy of the header's Contact button and
   behaving differently would read as a bug.

   ── Why background-size and not §01's `::before` + `scaleX` ──
   §01 codes the wipe as an absolutely-positioned pseudo-element scaled with
   `scaleX`, with the label lifted above it on `z-index: 1`. Built that way first,
   it BROKE the label: a `::before` at `z-index: 0` paints over its parent's in-flow
   content, and **a bare text node cannot be given a z-index**. All three of these
   buttons emit their label as bare text (`header.php:85`, `header.php:132`,
   `footer.php:82`), as do seven of the eight blocks that emit `lc-buttons__btn`.
   A background layer always paints behind text, whatever the markup — so the
   gradient is a second background on the button, revealed by animating
   `background-size` off a `left center` position.

   Genuinely lost: `scaleX` resolves the gradient in the unscaled box then scales
   the result, while `background-size` resolves it in the scaled box, so the 83°
   ramp differs subtly DURING the wipe. Identical at rest. `background-size` is
   also not compositor-accelerated. Both accepted 21 Aug 2026 against a label that
   vanishes.

   ⚠️ The plugin carries the SAME mechanism for `.lc-buttons__btn--cta`
   (`lc-blocks/src/blocks/buttons/style.scss`). A theme cannot share a Sass mixin
   with a plugin — two copies, two codebases. Keep them in step.
   ========================================================================= */
.site-header__cta,
.site-drawer__cta,
.prefooter__btn {
	/* Longhands throughout: the `background` shorthand resets `background-size`
	   and would silently kill the wipe. */
	background-color: var(--bs-primary);
	background-image: var(--wp--preset--gradient--cta-hover, linear-gradient(83deg, #0C3330 0%, #4EC278 90%));
	background-repeat: no-repeat;
	background-position: left center;
	background-size: 0% 100%;
	transition: background-size 0.3s ease, color 0.3s ease;
}

/* :focus-visible matches :hover. ⚠️ NOT in the design — §01 specifies hover only.
   Developer's call, 21 Aug 2026; logged as QA Q34. */
.site-header__cta:focus-visible,
.site-drawer__cta:focus-visible,
.prefooter__btn:focus-visible {
	background-size: 100% 100%;
	color: var(--bs-white);
}
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.site-header__cta:hover,
	.site-drawer__cta:hover,
	.prefooter__btn:hover {
		background-size: 100% 100%;
		color: var(--bs-white);
	}
}

/* §03's closing media query, honoured. The transition goes; the hover STATE stays,
   so these users still get feedback — it just arrives instantly. */
@media (prefers-reduced-motion: reduce) {
	.site-header__cta,
	.site-drawer__cta,
	.prefooter__btn {
		transition: none;
	}
}

/* =========================================================================
   Footer
   =========================================================================
   BP Footer.dc.html (1440) and BP Footer Mobile.dc.html (375). Tablet is not
   drawn: the desktop row needs 213 + 660 + gap ≈ 900px, so it holds at 1024
   (944 content) and stacks below that. Stated assumption — see dev-log.md.
   ========================================================================= */
.site-footer { background: var(--bs-dark); color: var(--bs-white); }

/* 8px brand bar. Four stops, 90deg. Not a token — it appears in no palette or
   gradient table, and #A6E6BD is not in §1. Flagged for design. */
.site-footer__bar-top {
	height: 8px;
	background: linear-gradient(90deg, #4EC278 0%, #056752 31.25%, #4EC278 66.83%, #A6E6BD 100%);
}

.site-footer__inner {
	box-sizing: border-box;
	padding: 60px var(--bp-side) 40px;
	display: flex;
	flex-direction: column;
	gap: 40px;
}

.site-footer__top { display: flex; flex-direction: column; gap: 40px; }

.site-footer__brand { display: flex; flex-direction: column; gap: 24px; align-items: flex-start; }
.site-footer__logo { height: 80px; width: auto; display: block; }
.site-footer__brand-meta { display: flex; flex-direction: column; gap: 24px; align-items: flex-start; }

/* nowrap is from the comp, and it is load-bearing: at 20px the line is ~237px
   against a 213px brand column, so it deliberately runs into the column gap
   rather than breaking the number across two lines. */
.site-footer__phone {
	margin: 0;
	font-family: var(--bs-body);
	font-size: 16px;
	font-weight: 400;
	line-height: var(--bp-lh-body);
	color: var(--bs-white);
	white-space: nowrap;
}
.site-footer__phone a { color: var(--bs-white); text-decoration: underline; text-underline-offset: 4px; }

.site-footer__social { list-style: none; margin: 0; padding: 0; display: flex; gap: 10px; align-items: center; }
.site-footer__social a { display: block; text-decoration: none; }
.site-footer__social img { width: 40px; height: 40px; display: block; }

.site-footer__main { display: flex; flex-direction: column; gap: 32px; }

/* Three columns from one menu at depth 2: top-level item = heading, children =
   links. Rendering follows the menu, so a fourth column is an editor action. */
.site-footer__columns { display: block; }
.site-footer__menu { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 32px; }
/* Column headings. `> a` is kept alongside the span deliberately — the walker
   emits a span at depth 0 today (design review: the three categories are not
   clickable), but if a column ever gets a real destination the heading must not
   lose its type. See BP_Footer_Walker for why this is an element change rather
   than `pointer-events: none`. */
.site-footer__menu > li > a,
.site-footer__menu > li > .site-footer__heading {
	display: block;
	font-family: var(--bs-body);
	font-size: 18px;
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	color: var(--bs-primary);
	text-decoration: none;
	margin-bottom: 16px;
}
.site-footer__menu .sub-menu { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.site-footer__menu .sub-menu a {
	font-family: var(--bs-heading);
	font-size: 16px;
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	color: var(--bs-white);
	text-decoration: none;
}
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.site-footer__menu .sub-menu a:hover { color: var(--bs-secondary);
	}
}

.site-footer__subscribe { display: flex; flex-direction: column; gap: 16px; }
.site-footer__subscribe-title {
	margin: 0;
	font-family: var(--bs-body);
	font-size: 16px;
	font-weight: 400;
	line-height: var(--bp-lh-snug);
	letter-spacing: -0.01em;
	color: var(--bs-white);
	text-wrap: pretty;
}

/* ── Subscribe form (Gravity Forms) ───────────────────────────────────────
   The design draws a bare input and a Subscribe button; Gravity Forms ships a
   labelled, validated form in its "orbital" theme. Restyled rather than
   restructured — the label is hidden from sight but kept for screen readers,
   because the comp's placeholder-only treatment would otherwise leave the field
   with no accessible name once the placeholder is typed over.
   Values from BP Footer.dc.html:62-63 and BP Footer Mobile.dc.html:64-65. */
.site-footer__subscribe .gform_wrapper { margin: 0; }
.site-footer__subscribe .gform_wrapper .gform_fields {
	display: flex;
	flex-direction: column;
	gap: 16px;
	grid-template-columns: none;
}
.site-footer__subscribe .gfield { width: auto; padding: 0; }
.site-footer__subscribe .gfield--type-submit { margin: 0; }

/* Visually hidden, still announced. */
.site-footer__subscribe .gfield_label {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	white-space: nowrap;
}

/*
 * The form opts out of Gravity Forms' framework theme via
 * `gform-theme__disable-framework` on its container (see footer.php), so these
 * plain rules are uncontested. Values from BP Footer.dc.html:62-63 (desktop) and
 * BP Footer Mobile.dc.html:64-65 (mobile).
 *
 * The label is hidden from sight but kept for screen readers: the comp shows a
 * placeholder only, and a placeholder disappears as soon as it is typed over,
 * which would leave the field with no accessible name.
 */
.site-footer__subscribe .gform_wrapper { margin: 0; }
.site-footer__subscribe .gform_wrapper .gform_fields {
	display: flex;
	flex-direction: column;
	gap: 16px;
	margin: 0;
	padding: 0;
	list-style: none;
	grid-template-columns: none;
}
.site-footer__subscribe .gform_wrapper .gfield { width: auto; padding: 0; margin: 0; }

.site-footer__subscribe .gfield_label {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	white-space: nowrap;
}

.site-footer__subscribe input[type="email"],
.site-footer__subscribe input[type="text"]:not([type="hidden"]) {
	box-sizing: border-box;
	width: 100%;
	height: 51px;
	border: 1px solid #D1D1D1;
	border-radius: var(--bp-radius);
	padding: 16px 20px;
	background: transparent;
	font-family: var(--bs-body);
	font-size: 16px;
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	color: var(--bs-white);
}
.site-footer__subscribe input[type="email"]:focus { outline: 2px solid var(--bs-secondary); outline-offset: 1px; }
.site-footer__subscribe input::placeholder { color: var(--bs-white); opacity: .6; }

/* ⚠️ `text-align: center` is REQUIRED, not redundant. Chrome centres an
   `input[type="submit"]` by default, but something upstream resets it to `start`
   here, and at 375 the button is full width — so the label sat 28px from the left
   with 229px of space after it (design review, 31 Aug 2026). It hides at desktop
   only because the button shrinks to fit its text.
   Every other full-width button escapes this by being `display: flex` with
   `justify-content: center`; this one is a `block`-level input, so alignment is
   what governs it. Measured across the site: this button is the only one affected. */
/* ⚠️ BOTH ELEMENTS, because Gravity Forms changed which one it emits. Up to
   2.10.3 the submit was `<input type="submit">`; 3.x emits
   `<button type="submit" class="gform-button gform-button--white button">` —
   `get_form_button()` used to reach a `<button>` only for a `type: link` button,
   and then with an `_link` id suffix, so the 2.10.x markup could not produce this.

   WP Engine auto-updates plugins on the live site, so production moved to 3.1.1
   while the repo still carried 2.10.3, and every declaration below silently stopped
   applying: measured at 1440 the button rendered **85.8 x 18, transparent, no
   radius, no padding, 18px/400** — plain text on the dark footer. Reported
   16 Sep 2026 as "lost its buttonyness". The plugins are updated in the repo now.

   Matching on element + type rather than on `.gform-button` deliberately: it covers
   both the old and the new markup, so this rule survives the version moving again
   in either direction. Specificity stays flat — the container carries
   `gform-theme__disable-framework`, so there is nothing to out-specify
   (pattern-inventory §7). */
.site-footer__subscribe input[type="submit"],
.site-footer__subscribe button[type="submit"] {
	box-sizing: border-box;
	/* `<button>` brings its own UA appearance; `<input type=submit>` did not. */
	appearance: none;
	width: 100%;
	text-align: center;
	border: 0;
	border-radius: var(--bp-radius);
	padding: 16px 28px;
	background: var(--bs-subtle);
	font-family: var(--bs-body);
	/* ⚠️ A LITERAL 13, for the same reason as `.site-header__cta` — see the long
	   note there. This button was the only text in the FOOTER reading
	   `--bp-fs-button`: swept at 1440 vs 1920, the heading is 20/20, the
	   subscribe title 16/16 and the copyright 14/14, all flat, while this went
	   13 → 17.3. `BP Footer.dc.html:63` draws it as a FIXED box — `width: 150px;
	   height: 52px; padding: 18px 32px`, Inter 13/600/0.12em — beside a fixed
	   51px field with 16px placeholder text, so the whole row is drawn at fixed
	   sizes and the literal is what the comp states. */
	font-size: 13px;
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	color: var(--bs-dark);
	cursor: pointer;
	/* Was MISSING, so this button alone SNAPPED between fills where every other
	   button on the site eases — QA Q40. This is the Secondary treatment
	   (`--bs-subtle` → `--bs-neutral-200`), the same pair `lc/buttons --secondary`
	   uses, so it takes that block's transition string verbatim rather than a new
	   one (dev-rules §B: same pattern, same solution). `background-image` is in the
	   list for that reason — this button paints no image, but forking the house
	   string per element is how a system drifts. */
	transition: background-color 0.2s ease, background-image 0.2s ease, color 0.2s ease;
}
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.site-footer__subscribe input[type="submit"]:hover,
	.site-footer__subscribe button[type="submit"]:hover { background: var(--bs-neutral-200);
	}
}

/* GF's validation chrome, kept but brought into the palette. */
.site-footer__subscribe .gfield_required { color: var(--bs-secondary); }
.site-footer__subscribe .gfield_description,
.site-footer__subscribe .validation_message {
	font-family: var(--bs-body);
	font-size: var(--bp-fs-small);
	color: var(--bs-secondary);
	background: none;
	border: 0;
	padding: 4px 0 0;
}
.site-footer__subscribe .gform_validation_errors { display: none; }

.site-footer__bottom { display: flex; flex-direction: column; gap: 24px; }
.site-footer__rule { height: 1px; background: var(--bs-subtle); opacity: .2; }

.site-footer__legal { display: flex; flex-direction: column; gap: 24px; }
.site-footer__legal-main { display: flex; flex-direction: column; gap: 24px; }
.site-footer__legal-menu {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 12px 20px;
	align-items: center;
}
.site-footer__legal-menu a,
.site-footer__copyright {
	font-family: var(--bs-body);
	font-size: 14px;
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	color: var(--bs-white);
	text-decoration: none;
	white-space: nowrap;
}
.site-footer__copyright { margin: 0; }

/* The comp marks "Patient Portal" with a ↗ because it leaves the site. Keyed to
   target="_blank" rather than to that one label, so it stays true for whatever
   the editor puts in the menu. Metrics are Text/Caption, per the comp. */
.site-footer__legal-menu a[target="_blank"]::after {
	content: "↗";
	margin-left: 8px;
	font-size: var(--bp-fs-caption);
	letter-spacing: var(--bp-ls-caption);
}

.site-footer__accreditations { list-style: none; margin: 0; padding: 0; display: flex; gap: 8px; align-items: flex-start; }
/* Badge size is set by the LegitScript seal, 15 Oct 2026: its snippet draws it at
   73 × 79, and the others match its 79px height (the comp drew all at 40). Three
   full-size tiles are 446px wide, more than a phone has, so below 768 the row
   shrinks instead: each tile shrinks in proportion to its width, so the badges
   stay one height (56 at 375, 45 at 320). */
.site-footer__accreditations li {
	box-sizing: border-box;
	flex: 0 1 auto;
	min-width: 0;
	border-radius: var(--bp-radius);
	background: var(--bs-white);
	padding: 8px;
	display: flex;
	align-items: center;
	justify-content: center;
}
.site-footer__accreditations img { display: block; width: auto; height: auto; max-width: 100%; max-height: 79px; object-fit: contain; }

@media (min-width: 1024px) {
	.site-footer__inner { padding: 80px var(--bp-side) 40px; gap: 80px; }
	.site-footer__top { flex-direction: row; justify-content: space-between; align-items: flex-start; gap: 40px; }
	.site-footer__brand { width: 213px; flex-shrink: 0; gap: 60px; }
	.site-footer__logo { height: 100px; }
	.site-footer__brand-meta { gap: 40px; }
	.site-footer__phone { font-size: 20px; line-height: 2; }

	.site-footer__main { width: 660px; flex-shrink: 0; gap: 60px; }
	.site-footer__menu { flex-direction: row; justify-content: space-between; gap: 40px; }
	.site-footer__menu > li > a,
	.site-footer__menu > li > .site-footer__heading { font-size: 20px; margin-bottom: 40px; }

	/* Input and button sit on one row: input grows, button is a fixed 150x52.
	   Padding here is 18/32, which is what the desktop comp draws — NOT the §5a
	   "Standard" 16/28 the mobile comp uses. Both are built as drawn; the
	   discrepancy is recorded in dev-log.md, not averaged away. */
	/* Input grows, button is a fixed 150x52. Padding here is 18/32, which is what
	   the desktop comp draws — NOT the §5a "Standard" 16/28 the mobile comp uses.
	   Both are built as drawn; see dev-log.md, they are not averaged. */
	.site-footer__subscribe .gform_wrapper .gform_fields { flex-direction: row; align-items: center; gap: 8px; }
	.site-footer__subscribe .gform_wrapper .gfield--type-email { flex: 1 1 auto; }
	.site-footer__subscribe .gform_wrapper .gfield--type-submit { flex: 0 0 150px; }
	/* Both elements — see the GF 3.x note on the base rule. This override carries
	   the DRAWN desktop box, so leaving it on `input` alone left the `<button>` at
	   the mobile 16/28 and 47.6px tall beside a 51px field. Measured side by side
	   in the same row: input 52.0, button 47.6, identical font and line-height. */
	.site-footer__subscribe input[type="submit"],
	.site-footer__subscribe button[type="submit"] { padding: 18px 32px; height: 52px; }

	.site-footer__bottom { gap: 30px; }
}

/* The legal row's own frames. It used to take the 1440 values from
   `min-width: 1024`, which is pattern-inventory §15's warning — desktop values
   from a tablet breakpoint — and it overflowed: 1104 against a 1024 viewport,
   clipping the credential marks with no scroll. Both tablet frames draw
   `gap: 80px`, `flex: 1; min-width: 0` and a WRAPPING links row; only 1440
   draws 120px and a single line. `min-width: 0` is the load-bearing part —
   without it a flex item cannot shrink below its content's intrinsic width. */
@media (min-width: 768px) {
	.site-footer__legal-main { flex: 1; min-width: 0; gap: 20px; }
	.site-footer__legal-menu { gap: 12px 24px; }
	.site-footer__accreditations { flex-shrink: 0; }
	.site-footer__accreditations li { height: 95px; flex-shrink: 0; }
	.site-footer__accreditations img { height: 79px; max-height: none; max-width: none; }
}

/* ⚠️ The tablet frames draw the badges BESIDE the links (row, gap 80). With the
   full-size badges (446px) that crushed the links to 162px and five lines at 768,
   so the badges sit on their own row under the links (the base column layout)
   until there is room beside them. That is 1300, not the 1200 breakpoint: at
   1200–1299 the links wrapped with "References" alone on a second line. */
@media (min-width: 1200px) {
	.site-footer__legal-menu { gap: 20px; }
}
@media (min-width: 1300px) {
	.site-footer__legal { flex-direction: row; align-items: center; gap: 120px; }
}

/* =========================================================================
   Default page hero / archives / single / 404
   ========================================================================= */
/* Interior heroes sit below a sticky header (which already reserves its space),
   so they don't add header height — only the transparent/fixed header overlaps,
   and that variant only appears on block-driven pages without these hero classes. */
/* ── Detail hero — Standard Detail and News Detail share it ──────────────────
   BP Interior Pages pages 1 and 2 draw the same band: Neutral/100, min-height
   400, `40px 80px`, and a `1fr 600px` grid with 80 between when there is a
   featured photo. The text-only frame is the same band with one column.
   The 600 column is a fraction below 1440 so the pair cannot overflow between
   breakpoints (defect #13); it resolves to exactly 600 at 1440. */
/* QA Q13. This band was `display: flex` with no direction, so below the 1200
   grid rule the text column and the photo were flex-ROW siblings — and because
   `.detail-hero__media` is `width: 100%`, the photo took the whole row and
   `.detail-hero__text` computed to **0px wide** at 375, 768, 1000 and 1199. The
   copy wrapped one character per line. It was invisible for six days because no
   page carried a featured image and every seeded post had empty content, so the
   photo variant had never rendered anywhere.
   Stacked is not a guess: the 375 comp draws exactly this — `flex-direction:
   column; gap: 24px; align-items: stretch`, photo last — and project-log's
   full-bleed rule states the editorial stacking order for a top-of-page hero as
   "text over image". No frame exists between 768 and 1199 (Style Guide §07), so
   the stack carries up to the 1200 grid. `align-items` must be `stretch` here,
   not `center`: centring a column makes both children shrink to their content
   width. The grid rule below sets its own `center` for the row case.
   ⚠️ Do NOT "restore" `align-items: center` here for the text-only variant. In a
   flex COLUMN `align-items` is the horizontal axis, so `center` shrink-wraps the
   copy and centres it — the 1440 text-only frame draws it left-aligned at x=80.
   Vertical centring in a column is `justify-content`, which is why it is below. */
.detail-hero {
	box-sizing: border-box;
	min-height: 400px;
	padding: 40px var(--bp-gutter);
	background: var(--bs-subtle);
	display: flex;
	flex-direction: column;
	gap: 24px;
	align-items: stretch;
	justify-content: center;
}
.detail-hero__text {
	display: flex;
	flex-direction: column;
	gap: 24px;
	padding: 30px 0;
	/* The token, not a literal — it scales above 1440 and this must scale with it. */
	max-width: var(--bp-detail);
	min-width: 0;
}
.detail-hero__eyebrow {
	margin: 0;
	font-family: var(--bs-mono);
	font-size: var(--bp-fs-eyebrow);
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-eyebrow);
	text-transform: uppercase;
	/* Q26, decided 20 Aug 2026. The interior comps draw this eyebrow Brand/
	   Primary-light at 1440 and Neutral/900 at 375 — both Standard Detail and
	   News List, at both frames, so it is a deliberate step rather than a slip.
	   ⚠️ The 375 value is the one that agrees with the SITEWIDE eyebrow rule
	   established 13 Aug ("Neutral/900 on every light surface … Brand/Primary is
	   explicitly no longer an eyebrow colour"), which makes the desktop green the
	   outlier against the general rule. Built to the interior comps because they
	   are the authoritative source for these templates; flagged for design. */
	color: var(--bs-text);
}
@media (min-width: 768px) {
	.detail-hero__eyebrow { color: var(--bs-primary-light); }
}
.detail-hero__title {
	margin: 0;
	font-size: var(--bp-fs-h1);
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	color: var(--bs-dark);
	text-wrap: pretty;
}
.detail-hero__desc,
.detail-hero__meta {
	margin: 0;
	font-size: var(--bp-fs-body);
	line-height: var(--bp-lh-body);
	color: var(--bs-text);
}
/* QA Q27, both halves.
   Crop: §7 gives this photo 5:4 (600 × 480), but that is the DESKTOP crop. The
   band re-crops to 4:3 below 768 — 335 × 251 in `BP Standard Pages Mobile`, and
   project-log's aspect-ratio decision states it in words: "At 375 the hero band
   re-crops to 4:3 (335 × 251). That is deliberate, not a conflict." The build
   carried 5:4 everywhere, so the mobile photo rendered 335 × 268.
   Radius: none. Every frame draws this photo as `width … height … object-fit:
   cover` and nothing else — it is the one image in the design that is NOT
   rounded, which is why the starter's `--bp-radius` looked plausible here. Card
   images ARE rounded, and they get it from `.card__media`'s clip, not from the
   img (see QA Q24, which was a false positive for exactly that reason). */
.detail-hero__media {
	display: block;
	width: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
}
@media (min-width: 768px) {
	.detail-hero__media { aspect-ratio: 5 / 4; }
}

@media (min-width: 1200px) {
	/* `1fr 600px`, as drawn. Unlike the two-fixed-column case that defect #13
	   covers, a single fixed column beside a flexible one cannot overflow — the
	   1fr absorbs whatever is left. `minmax(0, …)` on both so a long unbroken word
	   in the copy cannot push the row wider than the container.
	   NOT `1fr 600fr`: that is a 1:600 ratio, which gave the text 2px and the
	   photo 1198. */
	.detail-hero--split {
		display: grid;
		grid-template-columns: minmax(0, 1fr) minmax(0, 600px);
		gap: 80px;
		align-items: center;
	}
}

/* ── News List (Learning Hub) ─────────────────────────────────────────────── */
.news-list__hero {
	box-sizing: border-box;
	min-height: 400px;
	padding: 40px var(--bp-gutter);
	background: var(--bs-subtle);
	display: flex;
	align-items: center;
}
.news-list__hero-inner {
	display: flex;
	flex-direction: column;
	gap: 24px;
	padding: 30px 0;
	/* The token, not a literal — see `--bp-detail`. */
	max-width: var(--bp-detail);
}
.news-list__eyebrow {
	margin: 0;
	font-family: var(--bs-mono);
	font-size: var(--bp-fs-eyebrow);
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-eyebrow);
	text-transform: uppercase;
	/* Q26 — same step as `.detail-hero__eyebrow`; the News List hero draws it the
	   same way at both frames. See the note there. */
	color: var(--bs-text);
}
@media (min-width: 768px) {
	.news-list__eyebrow { color: var(--bs-primary-light); }
}
.news-list__title {
	margin: 0;
	font-size: var(--bp-fs-h1);
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	color: var(--bs-dark);
	text-wrap: pretty;
}
.news-list__desc { margin: 0; font-size: var(--bp-fs-body); line-height: var(--bp-lh-body); color: var(--bs-text); }

/* The 6px rule under the band, in the pill gradient. Shared by all three
   interior templates rather than restated per template (dev-rules §B): the comps
   draw it under EVERY interior hero — all four screens at 1440
   (BP Interior Pages.dc.html :43, :256, :281, :435) and all four at 375
   (BP Standard Pages Mobile.dc.html :67, :276, :307, :436). Only the News List
   had one until 20 Aug 2026 (QA Q14). */
.news-list__rule,
.detail-hero__rule { height: 6px; background: var(--wp--preset--gradient--pill, linear-gradient(99deg, #056752 0%, #B8F0CD 100%)); }

/* ── Hero headline reveal: line by line (Style Guide §08) ────────────────────
   Structure is injected by `text-reveal.js` — one mask per visual line, each
   holding an inner span that starts below its mask and slides up into it. Nothing
   here applies until that script has run, so a heading renders normally with JS
   off and the harnesses measure it at rest.

   ⚠️ The mask is 0.1em taller than the line, pulled back by an equal negative
   margin. Without it `overflow: hidden` clips descenders — the tails of g, y and p
   sit below the line box. The negative margin keeps the heading's own height
   unchanged, so the reveal costs no layout. */
.text-reveal__line-mask {
	display: block;
	overflow: hidden;
	padding-bottom: 0.1em;
	margin-bottom: -0.1em;
}

.text-reveal__line-inner {
	display: block;
	transform: translateY(110%);
	transition: transform 0.75s cubic-bezier(0.16, 1, 0.3, 1);
	will-change: transform;
}

.text-reveal.is-revealed .text-reveal__line-inner {
	transform: translateY(0);
}

/* Set for one frame while a heading is re-split after a resize, so the restored
   revealed state does not replay the animation. A class rather than an inline
   `transition: none`, because clearing that shorthand afterwards would also clear
   the per-line `transition-delay` and cost every line its stagger. */
.text-reveal.is-resplitting .text-reveal__line-inner {
	transition: none !important;
}

@media (prefers-reduced-motion: reduce) {
	.text-reveal__line-inner {
		transform: none !important;
		transition: none !important;
	}
}

/* ── Lenis (Style Guide §08, scroll momentum) ────────────────────────────────
   Lenis' own required stylesheet. We vendored the library without it, and the
   omission produced a real bug rather than a cosmetic one.

   ⚠️ `scroll-behavior: auto` is the load-bearing line. Lenis calls
   `window.scrollTo()` every frame toward its own animated position; with
   `scroll-behavior: smooth` set on `html` — which `@layer base` does — the BROWSER
   then eases toward each of those per-frame targets as well. The two smoothings
   compound, and the page crawls and then lurches: measured, one 500px wheel sat at
   7px for 700ms and then covered the remaining 490 in 400. It looked like a tuning
   problem and no Lenis option changed it, because the cause was outside Lenis.

   These rules only bite while Lenis is actually running — it puts `.lenis` and
   `.lenis-smooth` on `<html>` itself, so under reduced motion, where Lenis is never
   constructed, none of this applies. */
html.lenis {
	height: auto;
}

.lenis.lenis-smooth {
	scroll-behavior: auto !important;
}

/* Keeps a rail's own scrolling from chaining up to the page once it hits its end. */
.lenis.lenis-smooth [data-lenis-prevent] {
	overscroll-behavior: contain;
}

.lenis.lenis-stopped {
	overflow: hidden;
}

/* ── Scroll entrance: slide-up (Style Guide §08) ──────────────────────────────
   `slide-up.js` adds `.slide-up-ready` to targeted elements, which IS the hidden
   state — it is never authored here, so with JS off the page renders finished and
   nothing can be left invisible. `transition-delay` is set inline per element to
   build the cascade; `.is-visible` arrives from the IntersectionObserver.

   ⚠️ The transition lives on `.is-visible`, NOT on `.slide-up-ready`. CSS decides
   a transition from the after-change style, so putting it on the hidden class
   would make adding that class animate the element OUT — a fade the observer then
   immediately cancels, leaving no entrance at all. */
.slide-up-ready {
	opacity: 0;
	transform: translateY(28px);
}

.slide-up-ready.is-visible {
	opacity: 1;
	transform: none;
	transition:
		opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1),
		transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}

/* §08: "Wrap all animations in prefers-reduced-motion." This is also what the
   measurement harnesses rely on — they run with reduced motion so every element
   sits at its resting geometry. */
@media (prefers-reduced-motion: reduce) {
	.slide-up-ready,
	.slide-up-ready.is-visible {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}
}

/* ── Share row (lc/share) ─────────────────────────────────────────────────────
   UNLAYERED on purpose. The block ships its own styling inside `@layer blocks`
   and documents this as the override path — unlayered CSS beats a layer whatever
   the load order, so nothing here needs `!important` or manufactured
   specificity. See the block's style.scss header.

   ⚠️ THE COMP DRAWS ONE CONTROL, NOT A ROW OF PLATFORM ICONS. Both instances
   (BP Interior Pages :275 in the hero, :310 at the foot of the article) are a
   glyph plus the word "Share" and nothing else. The platform links stay in the
   DOM as the trigger's popover — which is what opens where the Web Share API is
   missing, Firefox desktop included — so hiding the inline row costs no
   functionality: every target is still reachable through the trigger.

   The menu's CONTENTS are not a gap in the build. BP Standard Pages Mobile
   lists "Share menu contents" under "Separate deliverables — not in these
   frames", so which platforms appear is a decision, taken in
   Settings → LC Blocks, and deliberately not drawn. */
.lc-share--hero > .lc-share__item,
.lc-share--body > .lc-share__item {
	display: none;
}

/* ⚠️ Type and colour go on the CONTAINER, not on the trigger.
   The trigger declares `font: inherit` and `color: inherit` in `@layer blocks`,
   so setting them here is what those two declarations were written to pick up —
   the values land without any rule outranking the block's own. Restating them on
   `.lc-share__trigger` computes identically and is what
   `check-block-cascade.mjs` exists to catch: the block staked a claim and
   something outside decided it. Same reason there is no hover colour here — the
   design draws no hover state for this control (BP Hover Effects covers CTA
   buttons, text links and photo zoom, not this), so inventing one would be a
   divergence, not a polish. Recorded for design rather than guessed.
   14 / 500 / LH 1.2 in Brand/Dark, glyph 20 — the comp's values at both
   placements. */
.lc-share--hero,
.lc-share--body {
	--lc-share-icon: 20px;
	font-family: var(--bs-body);
	font-size: 14px;
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	color: var(--bs-dark);
}

/* The one real override: the block's own 8px between glyph and word against the
   comp's 10px. `gap` is not a property the block claims through `font`/`color`
   inheritance, so it has to be stated. */
.lc-share--hero .lc-share__trigger--both,
.lc-share--body .lc-share__trigger--both {
	gap: 10px;
}

/* Date · divider · Share on one line. The mobile frame keeps all three together
   at 375 ("Date, divider and Share stay on one line under the H1"), so this row
   does NOT stack — it is the same rule at every width. */
.detail-hero__metarow {
	display: flex;
	align-items: center;
	gap: 20px;
	flex-wrap: wrap;
}

.detail-hero__metasep {
	width: 1px;
	height: 20px;
	flex: none;
	background: var(--bs-neutral-300, #BCCCC7);
}

/* State page (single-bp_state.php): News Detail's split hero, the licensure
   facts in the photo's column (PM mockup, 19 Oct 2026). No design source of
   its own; built from the hero's tokens and the map panel's badge. */
.state-hero__aside {
	display: flex;
	flex-direction: column;
	gap: 24px;
	min-width: 0;
}
.state-hero__aside .state-hero__facts { flex-direction: column; }
/* The split's second column is News Detail's 600px photo; the facts need less,
   and the mockup gives the title the room — at 600 the H1 was squeezed to 440px
   at 1200. The longest licence line ("Pharmacy (222 Boothwyn Pharmacy)") fits. */
@media (min-width: 1200px) {
	.single--state .detail-hero--split { grid-template-columns: minmax(0, 1fr) minmax(0, 400px); }
}

.state-hero__badge {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	padding: 6px 12px;
	border-radius: 999px;
	background: var(--bs-white);
	font-family: var(--bs-body);
	font-size: 14px;
	font-weight: 600;
	line-height: 1.2;
	color: var(--bs-text);
}
.state-hero__swatch {
	flex-shrink: 0;
	width: 14px;
	height: 14px;
	border-radius: 3px;
}
.state-hero__swatch.is-licensed { background: var(--bp-status-licensed); }
.state-hero__swatch.is-partial { background: var(--bp-status-partial); }
.state-hero__swatch.is-renewal { background: var(--bp-status-renewal); }
.state-hero__swatch.is-application { background: var(--bp-status-application); }
.state-hero__swatch.is-none { background: var(--bp-status-none); box-shadow: inset 0 0 0 1px var(--bp-status-none-edge); }

.state-hero__facts {
	margin: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 20px 60px;
}
.state-hero__facts dt {
	margin: 0 0 6px;
	font-family: var(--bs-mono);
	font-size: 12px;
	font-weight: 500;
	line-height: 1.2;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--bs-muted);
}
.state-hero__facts dd {
	margin: 0;
	font-size: var(--bp-fs-body);
	line-height: var(--bp-lh-body);
	color: var(--bs-text);
}
.state-hero__licenses { margin: 0; padding-left: 1.1em; list-style: disc; }
.state-hero__licenses li { margin: 0; }
.state-hero__as-of {
	margin: 0;
	font-size: 14px;
	line-height: 1.4;
	color: var(--bs-muted);
}

/* The end-of-article repeat sits in the 900px body column and is separated from
   the last paragraph by the comp's 12px, not by the prose rhythm. */
.lc-share--body {
	max-width: var(--bp-detail, 900px);
	padding-top: 12px;
}

/* ── Filter bar ───────────────────────────────────────────────────────────────
   The comp's own note: the fields follow the contact-form input treatment,
   since no filter component exists — 56px tall at desktop (51 at mobile), 1px
   Neutral/300, 4px radius.

   The panels open INLINE, in flow, full container width, pushing the results
   down — BP Interior Pages.dc.html:452 renders them as siblings of the field
   row, and BP Standard Pages Mobile says it in words: "Audience is shown open
   inline, pushing the results down." They were absolutely-positioned 320px
   dropdowns until 21 Aug 2026 (QA Q47).

   No JS: a visually hidden checkbox per panel, read with :has(). That is also
   WHY the markup is three siblings rather than a <details> — a <details>
   panel cannot leave its summary's grid cell, which is what forced the
   dropdown in the first place. */
.news-filters { padding: 40px var(--bp-gutter) 0; }
.news-filters__form { display: flex; flex-direction: column; gap: 12px; }
.news-filters__row { display: flex; flex-direction: column; gap: 12px; align-items: stretch; }

.news-filters__search,
.news-filters__control {
	box-sizing: border-box;
	height: 51px;
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 0 20px;
	border: 1px solid var(--bs-neutral-300, #BCCCC7);
	border-radius: var(--bp-radius);
	background: var(--bs-white);
	color: var(--bs-muted);
	font-size: 16px;
	cursor: pointer;
}
.news-filters__search { cursor: text; }
.news-filters__search input {
	flex: 1 1 auto;
	min-width: 0;
	border: 0;
	padding: 0;
	background: none;
	font-family: var(--bs-body);
	font-size: 16px;
	color: var(--bs-text);
}
.news-filters__search input:focus { outline: none; }
.news-filters__search:focus-within { border-color: var(--bs-primary-light); }

/* The toggle itself. Out of flow so it never takes a grid track, but still
   focusable — this is the control a keyboard reaches, and the <label> beside
   it is its accessible name. */
.news-filters__toggle {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	border: 0;
	opacity: 0;
	overflow: hidden;
	clip-path: inset(50%);
}
.news-filters__control { justify-content: space-between; }
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.news-filters__control:hover { border-color: var(--bs-primary-light);
	}
}
.news-filters__toggle:focus-visible + .news-filters__control {
	outline: 2px solid var(--bs-primary-light);
	outline-offset: 2px;
}

/* Plus / minus rather than the drawn chevron — stakeholder change, 21 Aug 2026.
   Open drops the vertical bar, leaving the minus. */
.news-filters__pm { flex-shrink: 0; color: var(--bs-text); }
.news-filters__pm-bar { transform-origin: center; transition: opacity .2s ease, transform .2s ease; }
.news-filters__toggle:checked + .news-filters__control .news-filters__pm-bar {
	opacity: 0;
	transform: rotate(90deg);
}

.news-filters__options {
	box-sizing: border-box;
	padding: 20px;
	border: 1px solid var(--bs-neutral-200);
	border-radius: var(--bp-radius);
	display: flex;
	flex-direction: column;
	gap: 16px;
}
/* Closed is the default state — Q48: everything loads closed. */
.news-filters__row:not(:has(#nf-audience:checked)) .news-filters__options--audience,
.news-filters__row:not(:has(#nf-category:checked)) .news-filters__options--category { display: none; }

.news-filters__options-label {
	margin: 0;
	font-family: var(--bs-mono);
	font-size: 12px;
	font-weight: 600;
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	color: var(--bs-text);
}
.news-filters__options-list { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; }
.news-filters__option { display: flex; align-items: center; gap: 12px; cursor: pointer; }
.news-filters__option input { position: absolute; opacity: 0; width: 0; height: 0; }
.news-filters__box {
	flex-shrink: 0;
	width: 20px;
	height: 20px;
	box-sizing: border-box;
	border: 2px solid var(--bs-neutral-300, #BCCCC7);
	border-radius: 2px;
	display: flex;
	align-items: center;
	justify-content: center;
}
.news-filters__option input:checked + .news-filters__box {
	background: var(--bs-primary-light);
	border-color: var(--bs-primary-light);
}
.news-filters__option input:checked + .news-filters__box::after {
	content: "";
	width: 5px;
	height: 9px;
	margin-top: -2px;
	border: solid var(--bs-white);
	border-width: 0 2px 2px 0;
	transform: rotate(45deg);
}
.news-filters__option input:focus-visible + .news-filters__box { outline: 2px solid var(--bs-primary-light); outline-offset: 2px; }
.news-filters__option-label { font-size: 16px; color: var(--bs-text); }

.news-filters__submit {
	box-sizing: border-box;
	padding: var(--bp-btn-pad-y) var(--bp-btn-pad-x);
	border: 0;
	border-radius: var(--bp-radius);
	background: var(--bs-dark);
	color: var(--bs-white);
	font-family: var(--bs-body);
	font-size: 13px;
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	cursor: pointer;
	/* The house transition string, verbatim — the same one `lc/buttons
	   --secondary` and the footer Subscribe button carry (dev-rules §B). This
	   button is NOT on the CTA colour scheme, so it does not take §01's gradient
	   wipe: the comp gives it a flat `style-hover="background: #056752"`, and
	   what was missing was only the easing. `background-image` is in the list to
	   keep the string identical across every button that uses it. */
	transition: background-color 0.2s ease, background-image 0.2s ease, color 0.2s ease;
}
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.news-filters__submit:hover { background-color: var(--bs-primary-light);
	}
}
.news-filters__meta { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-top: 8px; }
.news-filters__count { margin: 0; font-size: 14px; font-weight: 500; line-height: var(--bp-lh-body); color: var(--bs-muted); }
/* Clear all — the Text-Link treatment from `BP Hover Effects.dc.html` §02: two
   stacked 1px rules, the default wiping OUT to the right while the hover rule
   wipes IN from the left one beat later, and the colour changing on the same
   0.2s delay so it turns with the incoming rule.

   ⚠️ TWO CORRECTIONS came with it, both against the comp:
   1. The colours were INVERTED — this link was `#056752` at rest, which is the
      HOVER colour. Both frames draw `color: #0C3330` with an explicit
      `style-hover="color: #056752"`. Same defect shape as QA Q33, found the
      same way: by cross-checking the comp rather than by trusting the build.
   2. The family was `--bs-mono`; both frames draw **Inter** 13/600/1.2.
      `lc/buttons --link-*` is mono 12 — that is a different control, and this
      one is not it.

   ⚠️ `bottom: -5px` is §02's value, not the comp's `text-underline-offset: 4px`
   — a pseudo-element's offset from the box and an underline's offset from the
   baseline do not measure the same thing, so the comp does not arbitrate it.
   Same reasoning as the plugin's copy.

   ⚠️ SECOND COPY of a plugin mechanism, like the CTA wipe above: `lc/buttons`
   carries this in `blocks/buttons/style.scss` and a theme cannot share a Sass
   mixin with a plugin. Two codebases, two copies — keep them in step. */
.news-filters__clear {
	position: relative;
	font-family: var(--bs-body);
	font-size: 13px;
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	color: var(--bs-dark);
	text-decoration: none;
	transition: color 0.2s ease 0.2s;
}
.news-filters__clear::before,
.news-filters__clear::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: -5px;
	height: 1px;
	background-color: currentColor;
}
/* Default rule — wipes OUT to the right. */
.news-filters__clear::after {
	transform: scaleX(1);
	transform-origin: right center;
	transition: transform 0.2s ease;
}
/* Hover rule — wipes IN from the left, one beat later. */
.news-filters__clear::before {
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform 0.2s ease 0.2s;
}
/* :focus-visible matches :hover — same developer call as the CTA and the
   plugin's text links, and the same caveat: §02 specifies hover only (Q34). */
.news-filters__clear:focus-visible {
	color: var(--bs-primary-light);
}
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.news-filters__clear:hover {
		color: var(--bs-primary-light);
	}
}
.news-filters__clear:focus-visible::after { transform: scaleX(0); }
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.news-filters__clear:hover::after { transform: scaleX(0);
	}
}
.news-filters__clear:focus-visible::before { transform: scaleX(1); }
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.news-filters__clear:hover::before { transform: scaleX(1);
	}
}

/* §03's closing media query, honoured for this copy the way the plugin honours
   it for its own — the ANIMATION goes, the hover STATE stays.
   ⚠️ It has to live HERE, after the rules it turns off, not in the CTA block
   further up: same layer, equal specificity, so it wins on source order only.
   Placed up there first and measured 0.2s under emulated reduced motion —
   the media query was losing silently. Measure it, do not assume the cascade. */
@media (prefers-reduced-motion: reduce) {
	.news-filters__clear,
	.news-filters__clear::before,
	.news-filters__clear::after {
		transition: none;
	}
}

@media (min-width: 1200px) {
	.news-filters { padding-top: 56px; }
	/* Section gap 20 between the row, the panels and the results line. */
	.news-filters__form { gap: 20px; }
	/* 1fr 260 260 auto, as drawn; 16 across, 20 down to a panel. */
	.news-filters__row {
		display: grid;
		grid-template-columns: 1fr 260px 260px auto;
		column-gap: 16px;
		row-gap: 20px;
		align-items: stretch;
	}
	.news-filters__search,
	.news-filters__control { height: 56px; }
	.news-filters__submit { height: 56px; padding: 0 32px; }
	/* Panels drop below the whole row, full width. `order` (not an explicit
	   grid-row) is what keeps a CLOSED panel from leaving an empty track
	   behind: a display:none item is not placed at all, so the open one
	   always lands on the row directly under the fields. */
	.news-filters__options {
		order: 1;
		grid-column: 1 / -1;
		padding: 24px 28px;
	}
	.news-filters__options-label { color: var(--bs-primary-light); }
	/* Audience wraps and spreads; Category is a 4-column grid. Both as drawn. */
	.news-filters__options--audience .news-filters__options-list {
		flex-direction: row;
		flex-wrap: wrap;
		justify-content: space-between;
		gap: 14px 20px;
		/* Drawn as a literal 1180px inside a 1224px box. Held exact at 1440,
		   scaled with --bp-u above it, and capped so it can never overflow
		   below. Flagged to design (Q47). */
		width: min(100%, calc(1180 * var(--bp-u)));
	}
	.news-filters__options--category .news-filters__options-list {
		display: grid;
		grid-template-columns: repeat(4, 1fr);
		gap: 14px 24px;
	}
	.news-filters__meta { padding-top: 0; gap: 24px; }
}

.news-list__inner { padding: 40px var(--bp-gutter) 60px; }
.news-list__grid { display: grid; grid-template-columns: 1fr; gap: 40px; }
.news-list__empty { padding: 40px var(--bp-gutter); color: var(--bs-muted); }

@media (min-width: 768px) {
	.news-list__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1200px) {
	.news-list__inner { padding: 56px var(--bp-gutter) 100px; }
	/* 3-up at a 60 gap, 12 per page. */
	.news-list__grid { grid-template-columns: repeat(3, 1fr); gap: 60px; }
}

/* ── Card — the Learning Hub / News List card ─────────────────────────────────
   Field order is fixed (§5c): image → title → date → category. No panel, no
   border: the card is the photo and the text under it. */
.card { background: none; border: 0; border-radius: 0; overflow: visible; }
.card__link { display: flex; flex-direction: column; gap: 20px; text-decoration: none; color: inherit; }
.card__media { border-radius: var(--bp-radius); overflow: hidden; }
.card__media img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
.card__body { display: flex; flex-direction: column; gap: 16px; padding: 0; }
.card__title {
	margin: 0;
	font-family: var(--bs-heading);
	font-size: var(--bp-fs-h6);
	font-weight: 600;
	line-height: var(--bp-lh-snug);
	/* Brand/Dark — every card title in BP Interior Pages.dc.html is #0C3330. */
	color: var(--bs-dark);
	text-wrap: pretty;
	/* The hover below changed colour with no transition, so the title SNAPPED —
	   the same defect shape as QA Q40, and newly visible once the §03 photo zoom
	   started easing over 0.6s beside it. `color .2s ease` is the house string
	   for a link-colour fade, taken verbatim from `.site-header__menu a`
	   (dev-rules §B). NOT scoped to `.news-list__grid` like the zoom: this hover
	   already existed on every card context, so easing it fixes what is there
	   rather than adding behaviour to archive, search or index.
	   ⚠️ No reduced-motion cancel, deliberately — for parity with every other
	   colour fade in this file (header nav, Subscribe, pagination). A 0.2s colour
	   change is not the kind of motion §03's media query is aimed at, and killing
	   it would leave those users with no hover feedback on the title at all. */
	transition: color .2s ease;
}
.card__date {
	margin: 0;
	font-size: 12px;
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-caption);
	color: var(--bs-muted);
}
.card__cat {
	margin: 0;
	font-family: var(--bs-mono);
	font-size: 12px;
	font-weight: 600;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	color: var(--bs-primary-light);
}
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.card__link:hover .card__title { color: var(--bs-primary-light);
	}
}

/* ── Photo zoom on hover — BP Hover Effects.dc.html §03 ───────────────────────
   "img { transform: scale(1) → scale(1.05); transition: transform 0.6s ease }"
   with `overflow: hidden` on the container — `.card__media` already had both
   that and the 4px radius, so only the transform is new.

   §03 names its own targets: "news cards, feature split photos, primary content
   block photos". This is the news card, so applying it here is the spec, not an
   extension. The other two landed 21 Aug on `lc/feature-split` and
   `lc/multi-content`, with `lc/related-posts` taking the same treatment.

   Trigger is the whole CARD LINK, not the image container. §03's specimen puts
   `:hover` on the `.bp-zoom` span, but its own markup wraps the entire card in
   one `<a>`, and `lc/related-posts` — the closest built analogue — already
   resolved this the same way (dev-rules §B). Hovering a card's title and having
   its photo sit still would read as a bug.

   0.6s is §03's value. `lc/focus-cards` carries 0.4s; that is starter code on a
   DISABLED block, not a Boothwyn-verified source — do not copy it.

   ⚠️ SCOPED TO `.news-list__grid` on the developer's instruction. `.archive__grid`
   renders the identical `template-parts/card` on archive, search and index and
   deliberately does NOT get this — those three are starter templates with no
   Boothwyn design pass. Extending it is adding one selector here, not new code.
   ---------------------------------------------------------------------------- */
.news-list__grid .card__media img { transition: transform 0.6s ease; }
.news-list__grid .card__link:focus-visible .card__media img { transform: scale(1.05); }
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.news-list__grid .card__link:hover .card__media img { transform: scale(1.05);
	}
}

/* Reduced motion kills the transition AND the transform: unlike a button hover,
   a zoom conveys nothing, so there is no state worth preserving instantly. §03's
   own media query does exactly this.
   ⚠️ Placed AFTER the rules it cancels — see corrected defect #26. */
@media (prefers-reduced-motion: reduce) {
	.news-list__grid .card__media img { transition: none; }
	.news-list__grid .card__link:focus-visible .card__media img { transform: none; }
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.news-list__grid .card__link:hover .card__media img { transform: none;
	}
}
}

/* ── Pagination ───────────────────────────────────────────────────────────────
   ⚠️ No pagination component or token exists in the package. The comp assembles
   the control from button tokens and says "confirm or replace"; these are those
   values. Developer's call, 14 Aug 2026: keep bs_pagination() (paginate_links)
   rather than adding WP-PageNavi — it already emits exactly this control, and
   the scaffold check that every listing goes through bs_pagination() stays
   meaningful. */
/* ⚠️ NO padding-bottom, deliberately, 21 Aug 2026. The space above the footer
   belongs to the listing wrapper — `.news-list__inner` and `.archive__inner`
   both carry it — so it is there whether or not a listing paginates. A page
   with 12 posts and a page with 13 now end the same distance above the footer.
   Developer's call; moved rather than duplicated, so nothing double-counts. */
.archive__pagination {
	padding: 40px var(--bp-side) 0;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	margin-top: 0;
}
.archive__pagination .page-numbers {
	box-sizing: border-box;
	height: 48px;
	min-width: 44px;
	padding: 0 12px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	margin: 0;
	border-radius: var(--bp-radius);
	background: var(--bs-subtle);
	color: var(--bs-dark);
	font-family: var(--bs-body);
	font-size: 13px;
	font-weight: 600;
	letter-spacing: var(--bp-ls-wide);
	text-transform: uppercase;
	text-decoration: none;
	transition: background-color .2s ease, color .2s ease;
}
.archive__pagination .page-numbers.current {
	background: var(--bs-dark);
	color: var(--bs-white);
}
/* §08: no hover on touch. Only :hover is gated — siblings must not be. */
@media (hover: hover) {
	.archive__pagination .page-numbers:hover:not(.current):not(.dots) { background: var(--bs-neutral-200);
	}
}
.archive__pagination .page-numbers.dots { background: none; min-width: 0; padding: 0 4px; }
.archive__pagination .prev,
.archive__pagination .next { padding: 0 20px; }

@media (min-width: 1200px) {
	.archive__pagination { padding: 72px var(--bp-gutter) 0; gap: 12px; }
	.archive__pagination .page-numbers { min-width: 48px; }
}

/* ── Detail-page variants of section blocks ───────────────────────────────────
 * QA Q17. `BP Interior Pages.dc.html` specimen "09 · FAQ accordion — detail-page
 * variant" is explicit about what changes and what does not:
 *
 *   "Existing component, stacked inside the 900px container: no left button, no
 *    outer background or padding. Rows, type, toggles and the Text-Link in the
 *    panel are unchanged."
 *
 * and the 375 frame repeats it: "Existing component, no outer background or
 * padding. Rows, type and toggles unchanged."
 *
 * So this is a CONTEXT variant, not a new block state — which is why it lives
 * here rather than in the plugin. The trigger is a theme container class the
 * plugin has no business knowing about, and the theme's own CSS is unlayered and
 * therefore beats the block's `@layer blocks` by design (dev-rules: "a project's
 * own theme CSS is unlayered and therefore beats both layers, which is the
 * intended way to re-skin a block"). No block change, no attribute, no `save()`
 * risk, nothing to reseed.
 *
 * SCOPE (Oct 2026): FAQ List, i.e. `.lc-accordion.is-style-inline`, on any page.
 * This used to be scoped by container (`.article-content`), so the same block
 * looked different by page type, and a full Accordion section on a normal page
 * was stripped too. With one content well, the block decides: FAQ List is this
 * stacked variant in the 900 column everywhere; Accordion is the full section
 * everywhere.
 *
 * What was wrong: the block kept its 1420px two-column section layout inside the
 * 900px column, so `__inner` computed `219.95px | 380.05px` with a 140px column
 * gap and 80px of its own padding — the rows rendered 380px wide inside 900, on
 * top of another 80px of root padding. No overflow, so the overflow harness was
 * silent; just wrong.
 *
 * Deliberately NOT changed, because the annotation says "unchanged": row padding,
 * row borders, title type, toggle size and the panel's contents. Two of those look
 * like divergences against specimen 09 and are not — both are already-settled
 * history, and re-deriving them from this comp would undo real work:
 *   - **Rows.** Specimen 09 draws `padding: 28px 0` — vertical only, text flush
 *     with the container edge. The build uses `--faq-row-pad` (20 / 24 / 30) on all
 *     four sides. That is a DELIBERATE deviation made on 14 Aug 2026 (see dev-log
 *     "FAQ row padding — corrected against the Figma node"): the mobile section
 *     comp draws closed rows flush and open rows inset 20, which leaves a row
 *     disagreeing with itself as it opens, so closed rows took the open rows'
 *     horizontal padding at every frame. The rules still run the full width, as
 *     drawn. It is flagged there for design, not here.
 *   - **Toggle.** Specimen 09 draws 40 × 40 with a 20px glyph; the build is 28 × 28
 *     with Inter 16. That is token sheet §9c item E, already resolved toward the
 *     built frames — the same Figma node draws the title at 24 against the built
 *     20. Specimen 09 is a third instance of that one conflict, not a new finding.
 * ---------------------------------------------------------------------- */
.lc-accordion.is-style-inline {
	padding: 0;
}
.lc-accordion.is-style-inline .lc-accordion__inner {
	display: flex;
	flex-direction: column;
	gap: 32px;
	max-width: none;
	padding: 0;
	/* Interior Pages comp §09, the detail-page variant: "no outer background or
	   padding". The block paints §5e's card on this element since 11 Sep 2026 (fill,
	   radius, padding, the 1420 width); this override zeroed the padding and width but
	   had never needed to zero a fill, so the reading column showed a flush green box
	   on /sample-page/ and /leadership/ for three days. Every card property is off
	   here by name, the same way the block's own stacked style switches them off. */
	background: none;
	border-radius: 0;
	/* pattern-inventory §20, and it bit again on the first cut of this rule. The
	   block's section layout sets `align-items: start` for its two-column grid.
	   Rotating the container to `column` turns that into the HORIZONTAL axis, so
	   every child shrink-wraps: the rows measured 526.2px inside the 900 column
	   while the header measured 900, purely because the heading's max-content is
	   wider than the container and the rows' is not. Nothing on the rows can fix
	   it — width/max-width/flex are not cross-axis properties. */
	align-items: stretch;
}
.lc-accordion.is-style-inline .lc-accordion__header {
	display: flex;
	flex-direction: column;
	gap: 20px;
}

/* The 375 frame draws 28 between the header group and the rows, and 16 inside
   the header. Boundary at 767, not 768 — see dev-log corrected defect #18. */
@media (max-width: 767px) {
	.lc-accordion.is-style-inline .lc-accordion__inner { gap: 28px; }
	.lc-accordion.is-style-inline .lc-accordion__header { gap: 16px; }
}

/* ── Detail-body typography ── Q18, Q23 ──────────────────────────────────────
 * Both decided with the developer on 20 Aug 2026 (see dev-log, "The nine open
 * decisions"). UNLAYERED, like the prose media rules below: `theme.json` emits
 * its global type as unlayered `:root :where(...)` rules, so the same overrides
 * inside `@layer prose` would lose to it whatever their specificity.
 *
 * This is NOT a restatement of a value `theme.json` already owns — the habit
 * corrected defect #2, the heading sweep and the prose pass all warn about. It
 * applies DIFFERENT tokens in one specific context, which is exactly what the
 * design asks for: the detail body has its own body style and its own heading
 * scale, distinct from the section blocks everywhere else on the site.
 * ---------------------------------------------------------------------- */

/* ⚠️ Q31 — EVERY element selector in this unlayered group carries a
   `:not([class*="lc-"])` guard. It is not decoration and it is not optional.

   These rules are unlayered on purpose (see above: `theme.json` emits unlayered
   `:root :where(...)`, so `@layer prose` would lose to it). But unlayered also
   beats `@layer blocks`, so without a guard they reach straight INSIDE every LC
   block in the article body and restyle its internals. Measured 21 Aug 2026,
   before the guard: `lc/quote`'s text ran LH 2.0 against the 1.4 it asks for
   (64px where Q15 recorded 44.8), `lc/callout`'s heading rendered 24px at 375
   against its own `--bp-fs-h4` 20, and `lc/accordion`'s detail heading came out
   32px at 1440 where Q17 measured and recorded 44 — its whole 28/38/44 ladder
   overridden. Seven deviations across three blocks, and all three had been
   measured EXACT the day before these rules landed.

   The guards must be applied to ALL of them together, media queries included.
   `:not([attr])` adds specificity, so guarding the base rule while leaving the
   1024/1440 rules bare makes the mobile 24px outrank them and the heading stops
   scaling at all. That is a worse bug than the one being fixed.

   dev-rules.wordpress.md §G states this requirement; the Q10 list rules below
   already followed it. Only this group missed it. */

/* Q18 — `Text/P-Long`, the body style the Style Guide gives "every detail page".
   The build ran `theme.json`'s global 1.6 (which is Text/P-SHORT, correct for
   block copy) at every frame; the design draws 2.0 at 1440 and 1.8 at 375. The
   `--bp-lh-long` token already carried both steps and simply had no consumer. */
.article-content p:not([class*="lc-"]),
.article-content li:not([class*="lc-"]) {
	line-height: var(--bp-lh-long);
}

/* Q23 — the in-article heading scale is COMPRESSED against the global one. The
   Style Guide's body-blocks row: "H1 62 reserved for the page title; in-article
   headings H2 32 / H5 24 for small section titles". So an `h2` inside the detail
   body is a sub-head, not a page heading, and must not render at the global 54.
   Only `h2` and `h3` are re-scaled — the two levels the comps actually use in an
   article body. `h4`–`h6` keep the global scale, and the page `h1` is untouched.

   `h3` needs no new ladder: 20 at 375 → 24 at 1440 is exactly `--bp-fs-h5` with
   its 1.4 leading, and both frames draw precisely that (the two-column titles).

   `h2` does need one. The frames draw 24 / LH 1.2 at 375 and 32 / LH 1.4 at 1440,
   and no existing token steps 24 → 32 (`--bp-fs-h4` is 20 → 32, `--bp-fs-h5` is
   20 → 24). Shaped per pattern-inventory §15: hold the mobile value to 1024, then
   interpolate to 1440, so both stated frames land exactly and nothing jolts
   between them. The leading steps at 768, the project's mobile/tablet boundary —
   the design gives no frame in between. */
.article-content h2:not([class*="lc-"]) {
	font-size: 24px;
	line-height: var(--bp-lh-tight);
}
.article-content h3:not([class*="lc-"]) {
	font-size: var(--bp-fs-h5);
	line-height: var(--bp-lh-snug);
}
@media (min-width: 768px) {
	.article-content h2:not([class*="lc-"]) { line-height: var(--bp-lh-snug); }
}
@media (min-width: 1024px) {
	.article-content h2:not([class*="lc-"]) { font-size: calc(24px + 8 * (100vw - 1024px) / 416); }
}
@media (min-width: 1440px) {
	.article-content h2:not([class*="lc-"]) { font-size: calc(32 * var(--bp-u)); }
}

/* Q10 — prose bullet lists, decided with the developer 20 Aug 2026. The comps
   draw `list-style: none` with a glyph column, not a native disc: 20px column
   (16 below 768), 12px gap on both axes, glyph in Brand/Primary-light at the
   list's own P-Long metrics. Two sources state it identically — BP Interior
   Pages' 11 list items and the Style Guide's body-blocks row ("20px marker
   column · 12 gap · bullet in Brand/Accent #056752").
   The same model is in lc-blocks' `_prose-content` mixin for InnerBlocks
   containers (the accordion panel draws the identical list). Two copies, two
   codebases — a theme cannot `@use` a plugin Sass mixin. Keep them in step.
   Unordered only: no comp draws an ordered list, so `ol` keeps the native
   model rather than getting a synthesised counter nobody specified. */
.article-content ul:not([class*="lc-"]) {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 12px;
}
/* ⚠️ NOT a grid — see the twin note in lc-blocks' `_prose-content` mixin. A grid
   `li` makes every child a grid ITEM, so an item whose text opens with inline
   markup (`<li><em>x</em>, rest…</li>`) put the `<em>` in the 1fr column and threw
   the REST of the sentence into the 20px marker column, one word per line.
   `/hipaa-policy/`, 15 Sep 2026. Absolute marker + padding keeps the content a
   single inline flow and the drawn geometry (20 column + 12 gap = 32). */
.article-content ul:not([class*="lc-"]) > li {
	position: relative;
	padding-left: 32px;
	margin: 0;
}
/* ── Prose accent on a dark surface ──────────────────────────────────────────
   The twin of the `--bp-prose-accent` token in lc-blocks' `_prose-content`
   mixin, and it exists for the same reason the bullet model does: two copies,
   two codebases, a theme cannot `@use` a plugin Sass mixin. KEEP THEM IN STEP.

   The bullet glyph and the inline link are the only prose elements drawn in an
   accent rather than inheriting the text colour, and on a dark surface both sat
   at Accent/Mid-green #056752 over Brand/Dark #0C3330 — **2.0:1**, against the
   4.5:1 body text needs. The hover was worse: theme.json's `elements.link:hover`
   is `--dark`, which IS the surface, so a hovered link went **1:1, invisible**.
   PM note -4, 14 Sep 2026.

   White at rest, Brand/Primary #4EC278 on hover (6.08:1) — the call this
   codebase already made for the eyebrow ("Dark surfaces — the eyebrow goes
   white, not to an accent"). The defaults below resolve to exactly the values
   that were hardcoded before, so nothing on a light surface moves. */
.article-content {
	--bp-prose-accent: var(--bs-primary-light);
	--bp-prose-accent-hover: var(--bs-dark);
}
.lc-feature-split--bg-dark .article-content,
.lc-feature-split--bg-gradient .article-content,
.lc-featured-fullwidth .article-content,
.lc-hero-banner .article-content,
.lc-hero-carousel .article-content {
	--bp-prose-accent: var(--bs-white);
	--bp-prose-accent-hover: var(--bs-primary);
}

/* ⚠️ The `lc-` guard is not optional here. These rules are unlayered (see the
   note above), and unlayered author CSS beats `@layer blocks` — so without it
   this would recolour links inside every LC block in the scope, which is
   pattern-inventory §23's whole warning. */
.article-content a:not([class*="wp-block-button"]):not([class*="lc-"]) {
	color: var(--bp-prose-accent);
	text-decoration: underline;
	text-underline-offset: 2px;
}
.article-content a:not([class*="wp-block-button"]):not([class*="lc-"]):hover,
.article-content a:not([class*="wp-block-button"]):not([class*="lc-"]):focus-visible {
	color: var(--bp-prose-accent-hover);
}

.article-content ul:not([class*="lc-"]) > li::before {
	content: "\2022";
	color: var(--bp-prose-accent);
	font-size: inherit;
	line-height: inherit;
	position: absolute;
	left: 0;
	top: 0;
	width: 20px;
}
.article-content li:not([class*="lc-"])::marker { color: var(--bp-prose-accent); }
@media (max-width: 767px) {
	.article-content ul:not([class*="lc-"]) > li { padding-left: 28px; }
	.article-content ul:not([class*="lc-"]) > li::before { width: 16px; }
}

/* ── Prose media inside the detail body ──────────────────────────────────────
 * QA Q19, Q20, Q21. `.article-content` is the prose body of both detail
 * templates. These rules are UNLAYERED on purpose: WordPress core's own block
 * styles are unlayered too, so a rule for `.wp-block-image` / `.wp-block-media-
 * text` placed inside `@layer prose` would lose to core regardless of
 * specificity. Everything else about the prose column stays in the layer.
 * ---------------------------------------------------------------------- */

/* The prose column's trailing margin. The comps build the article as a flex
   column with a `gap`, so the last element contributes nothing below itself; our
   prose uses margins, so the last one escapes and lands on whatever follows —
   measured 118px above the related strip's hairline at 1440 where the comp draws
   100, and 76 at 375 against 60. Zeroing it makes the section rhythm exact and
   costs nothing anywhere else. */
.article-content > :last-child { margin-bottom: 0; }

/* Q19 — captions were inheriting body type (Inter 18/400 Neutral/900). Every
   frame draws Text/Caption: 12 / 500 / LH 1.2 / 2% in Neutral/700, 10px under
   the image. Same on the media/text pair's optional caption. */
.article-content figcaption {
	margin: 10px 0 0;
	font-family: var(--bs-body);
	font-size: var(--bp-fs-caption);
	font-weight: 500;
	line-height: var(--bp-lh-tight);
	letter-spacing: var(--bp-ls-caption);
	color: var(--bs-muted);
	text-align: left;
}

/* Q20 — prose images rendered at their natural height with `object-fit: fill`
   (900 × 751.5 for the wide photo). The comps crop them.
     375  — every prose image is 335 × 251, i.e. 4:3, unanimously: specimen 04's
            pair, specimen 06's wide photo, and both News Detail inline photos.
     1440 — specimen 06 and News Detail's first inline photo are 900 × 480,
            which is 15:8 at the 900 measure.
   Direct children only, so an inline or aligned image inside a paragraph is not
   force-cropped.
   ⚠️ The comp is not self-consistent here: News Detail's SECOND inline photo is
   drawn `aspect-ratio: 4 / 3` at 1440 while its first is 900 × 480. One rule
   cannot produce both. Built to the specimen (15:8), which is the sheet's own
   statement of the style rather than one instance of it — logged on Q20 to
   confirm. */
.article-content > .wp-block-image img {
	display: block;
	width: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
}
@media (min-width: 768px) {
	.article-content > .wp-block-image img { aspect-ratio: 15 / 8; }
}

/* Q21 — `core/media-text` renders comp specimen 04 and was completely unstyled:
   no gap where the comp draws 40, and the image at its natural height with
   `object-fit: fill` where the comp draws a 300px `cover` crop.
   The COLUMN SPLIT is deliberately left to the block's own `mediaWidth` control
   rather than forced here. The comp draws a fixed 420px media column, which is
   46.67% of the 900 measure — but core writes `mediaWidth` as an inline
   `grid-template-columns`, so overriding it needs `!important` and that would
   break the editor's own drag-to-resize handle. The fixture carries the value
   that lands 420 at 1440; an editor-authored instance keeps whatever the author
   drags to. Flagged on Q21. */
.article-content .wp-block-media-text {
	gap: 40px;
	align-items: start;
}
/* Core pads the text column by 8% of the block, which double-counts against a
   real gap and moves with the block width. The comp draws a plain 40px gutter. */
.article-content .wp-block-media-text .wp-block-media-text__content {
	padding: 0;
}
.article-content .wp-block-media-text__media img {
	display: block;
	width: 100%;
	aspect-ratio: 7 / 5;   /* 420 × 300 at 1440 */
	object-fit: cover;
}
@media (max-width: 767px) {
	/* Stacks media-first at 375 — for BOTH media positions, which is core's own
	   `is-stacked-on-mobile` behaviour and what the comp draws. 16px gap, and the
	   image takes the same 4:3 as every other prose image at this frame. */
	.article-content .wp-block-media-text { gap: 16px; }
	.article-content .wp-block-media-text__media img { aspect-ratio: 4 / 3; }
}

/* ── Article body and related ─────────────────────────────────────────────── */
/* QA Q25. The top padding was a literal 60px at every frame. The comps draw the
   900 column at `padding: 100px 0` at 1440 and `60px 20px` at 375 — which is
   exactly the `--bp-section` ladder the project already owns (60 / 80 / 100),
   so this is the token, not two hardcoded frames, and the undrawn 768/1024
   frames get the site's own rhythm rather than a value invented here.

   ONE CONTENT WELL (Oct 2026): these two are also `.page-blocks`, the same
   full-width flow as block-hero pages and the front page. They no longer cap
   their own width: `.page-blocks` holds prose and prose-level blocks to the
   same 900 column (`--bp-detail`) and lets sections run full width, so a block
   renders identically on every page type. Before, an lc section here was
   squeezed into 900, and a wide/full one broke out with `100vw` and sat half a
   scrollbar off-centre. Only the top padding is still this rule's. */
.single__body,
.page-content__inner {
	max-width: none;
	margin-inline: 0;
	padding: var(--bp-section) 0 0;
	box-sizing: content-box;
}
/* A Standard Detail page that ends on prose. `.article-content > :last-child`
   zeroes the last margin (News Detail's rhythm into its related strip), and
   nothing on this template put it back, so a page ending on a paragraph sat
   flush on the prefooter: measured 0 on Accessibility Statement, Terms and
   Privacy (found 27 Oct 2026). The value is the one an lc block ending the page
   carries: its 100px default through bs_responsive_block_margin() (functions.php),
   so the space above the footer is the same whichever kind of content ends the
   page. An lc block keeps its own margin and is left alone; so is a spacer,
   which an editor placed to set that space by hand. */
.page-content__inner > :last-child:not([class*="wp-block-lc-"]):not(.wp-block-spacer) {
	margin-bottom: clamp(60px, min(6.9444vw, 11.1111vh), 100px);
}
/* The News Detail related strip is `lc/related-posts`, rendered from single.php
   — see QA Q22. The theme's own `.single__related*` rules were deleted with the
   markup they styled (20 Aug 2026); the block owns the section's presentation
   now, including the mosaic and the mobile scroll rail. Do not reintroduce a
   theme copy: the whole point of the decision was to stop maintaining two. */

/* `:not(body)`: `single` and `archive` are also WordPress BODY classes, so the
   bare selector padded <body> too — an 80px white band under the footer on every
   single post, State page and archive (found 26 Oct 2026). The targets are the
   templates' own `article.single` and `div.archive`. */
:is(.archive, .single):not(body) { padding: 0 0 80px; }
/* Bottom padding matches what the pagination used to contribute, so archive,
   search and index keep the spacing they had — and now keep it on an empty or
   single-page listing too. Sits on top of `.archive`'s own 80px. */
.archive__inner { max-width: var(--bp-container); margin-inline: auto; padding: 0 var(--bp-side) 60px; }
.archive__header { padding: 40px 0; }
@media (min-width: 1200px) {
	.archive__inner { padding-bottom: 100px; }
}
.archive__title { font-size: var(--bp-fs-h2); margin: 0; color: var(--bs-dark); }
.archive__description { margin-top: 14px; color: var(--bs-muted); }
.archive__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 60px; }
.archive__empty { color: var(--bs-muted); }

.error-404 { padding: 96px 24px 120px; text-align: center; }
.error-404__inner { max-width: 560px; margin-inline: auto; display: flex; flex-direction: column; align-items: center; gap: 18px; }
.error-404__code { font-family: var(--bs-heading); font-size: 96px; font-weight: 800; color: var(--bs-secondary); margin: 0; line-height: 1; }
.error-404__title { font-size: 34px; margin: 0; color: var(--bs-dark); }
.error-404__text { color: var(--bs-muted); margin: 0; }
.error-404__btn { padding: 14px 28px; background: var(--bs-primary); color: #fff; border-radius: 999px; text-decoration: none; font-weight: 600; }

/* =========================================================================
   Responsive
   ========================================================================= */
/*
 * The starter's chrome rules that used to live here are gone. They targeted the
 * previous markup — `.site-header__login` (removed), `.site-footer__inner` as a
 * CSS grid (it is flex now) and `.site-footer__bar` (now `__bar-top`) — and being
 * further down the file they beat the chrome section above it. The burger bar was
 * the live casualty: a hardcoded `width: 24px` here overrode the responsive
 * --bp-burger-bar-w and made the mobile hamburger render at the tablet size.
 * Header and footer now own their own responsive behaviour in the chrome section.
 */
@media (max-width: 1024px) {
	.archive__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 860px) {
	.archive__grid { grid-template-columns: 1fr; }
}
