/*
Theme Name: Goldvion
Theme URI: https://github.com/abdullahharithworks/goldvion-research
Description: Goldvion storefront theme. Child of Twenty Twenty-Five. Design tokens live in theme.json so the client can adjust them in the Site Editor rather than needing us.
Author: Goldvion build team
Template: twentytwentyfive
Version: 0.1.0
Requires at least: 6.9
Requires PHP: 8.3
Text Domain: goldvion
*/

/*
 * Keep this file thin. Anything expressible in theme.json belongs there —
 * it stays editable by the client in the Site Editor, which is the point.
 * CSS here is only for what theme.json cannot express.
 */

/* --- Primitives (T-002) ---
   The reference's composited greys, as custom properties derived from the
   theme.json presets so theme.json stays the single source of the base
   colours. For our own component CSS only — anything set through a block
   attribute (textColor, backgroundColor) must use a preset from theme.json
   instead; .has-*-color wins with !important, so a --gv-* variable used that
   way would silently lose. See state/sprints/S-002/notes/T-001-primitives.md
   §4.2 for what each percentage is used for. */
:root {
	--gv-fg-60: color-mix(in srgb, var(--wp--preset--color--charcoal) 60%, transparent);
	--gv-fg-50: color-mix(in srgb, var(--wp--preset--color--charcoal) 50%, transparent);
	--gv-fg-45: color-mix(in srgb, var(--wp--preset--color--charcoal) 45%, transparent);
	--gv-fg-40: color-mix(in srgb, var(--wp--preset--color--charcoal) 40%, transparent);
	--gv-fg-30: color-mix(in srgb, var(--wp--preset--color--charcoal) 30%, transparent);
	--gv-fg-25: color-mix(in srgb, var(--wp--preset--color--charcoal) 25%, transparent);
	--gv-fg-20: color-mix(in srgb, var(--wp--preset--color--charcoal) 20%, transparent);
	--gv-fg-10: color-mix(in srgb, var(--wp--preset--color--charcoal) 10%, transparent);
	--gv-ivory-90: color-mix(in srgb, var(--wp--preset--color--ivory) 90%, transparent);
	--gv-ivory-80: color-mix(in srgb, var(--wp--preset--color--ivory) 80%, transparent);
	--gv-ivory-70: color-mix(in srgb, var(--wp--preset--color--ivory) 70%, transparent);
	--gv-ivory-60: color-mix(in srgb, var(--wp--preset--color--ivory) 60%, transparent);
	--gv-ivory-50: color-mix(in srgb, var(--wp--preset--color--ivory) 50%, transparent);
	--gv-ivory-40: color-mix(in srgb, var(--wp--preset--color--ivory) 40%, transparent);
	--gv-ivory-20: color-mix(in srgb, var(--wp--preset--color--ivory) 20%, transparent);
	--gv-ivory-18: color-mix(in srgb, var(--wp--preset--color--ivory) 18%, transparent);
	--gv-gold-50: color-mix(in srgb, var(--wp--preset--color--gold) 50%, transparent);
	--gv-gold-40: color-mix(in srgb, var(--wp--preset--color--gold) 40%, transparent);
	--gv-gold-30: color-mix(in srgb, var(--wp--preset--color--gold) 30%, transparent);
	--gv-gold-20: color-mix(in srgb, var(--wp--preset--color--gold) 20%, transparent);
	--gv-gold-15: color-mix(in srgb, var(--wp--preset--color--gold) 15%, transparent);
	--gv-gold-35: color-mix(in srgb, var(--wp--preset--color--gold) 35%, transparent);
	/* T-004: border-palegold/10 (Checkout.jsx:108 review-table rows) has no existing
	   primitive; same color-mix form as its neighbours above. */
	--gv-gold-10: color-mix(in srgb, var(--wp--preset--color--gold) 10%, transparent);

	/* S0-1 (T-009): the export's section vertical rhythm is a step —
	   `py-24 md:py-32` (96px -> 128px at >=768px) — not the continuous
	   `clamp(4rem,9vw,8rem)` every pattern's own block attribute currently
	   emits inline. Patterns reference this variable in their padding
	   attribute instead of the literal clamp() so the step can be applied
	   here, in one place, without fighting inline-style specificity —
	   redefining a custom property a `var()` points to always wins,
	   regardless of what property consumes it.

	   All seven content sections now resolve their padding from this one
	   variable, by two different routes: experience, how-it-works, tiers,
	   craftsmanship and faq carry it inline from their pattern's padding
	   attribute, while testimonials (.gv-testimonials) and instagram
	   (.gv-ig) pick it up from their own class rules further down this
	   file. Different delivery, same lever — changing it here reaches all
	   seven. The hero is the deliberate exception: its vertical space
	   comes from .gv-hero__slide's own padding, not from this step.

	   S-008: halved from the export's 96/128px step to 44/64px on the
	   client's instruction. The gap between two sections is this value
	   twice plus core's 1.2rem block gap, so the export's step read as
	   275px of empty screen before every section title at >=768px; it is
	   now 147px. The export's own figure is recorded as a delta in
	   design-reference/export-reconstruction-spec.md — this is a
	   deliberate departure from it, not drift. */
	--gv-section-pad-y: 2.75rem; /* 44px */

	/* S-008: the height of the sticky site header.

	   THIS EXISTS BECAUSE OUR HEADER IS NOT THE EXPORT'S HEADER. The
	   client's export puts a `fixed` header OVER the hero
	   (design-reference/.../components/Header.jsx), so `h-screen` there is
	   exactly the viewport. Ours is `position: sticky` (see
	   .wp-block-template-part:has(> .gv-header-root) below), which sits in
	   normal flow and pushes everything after it down by its own height —
	   so anything sized to the viewport must subtract this or it overflows
	   the fold by exactly one header. That is what put the hero's slide
	   dots off-screen until S-008.

	   The header has no explicit height; this is the sum of what makes it:
	   .gv-icon-btn's 2.5rem row + 2 x .gv-site-header__bar's 1.15rem
	   padding + the 1px hairline = 77.8px. Measured 78px at both 390px and
	   1592px wide, and no media query touches the header, so it is stable
	   across breakpoints. If either of those two rules changes, change
	   this with it. */
	--gv-header-h: 4.875rem; /* 78px */

	/* S-008: the band at the bottom of the viewport owned by
	   .gv-shipping-pill, which is position: fixed. 41px pill + its 1rem
	   inset; larger below 600px, where the pill goes full-bleed and wraps
	   to two lines.

	   This is NOT reserved out of the hero's height. It was, briefly, and
	   that was wrong: the pill is `fixed`, so it floats over content and
	   never needed space carved out for it — carving it out just left a
	   strip of bare ivory under the hero. The hero fills the screen; this
	   value is used only to lift the slide dots clear of the pill on the
	   widths where the two actually collide. See .gv-hero__dots. */
	--gv-hero-bottom-clear: 3.5rem; /* 56px */

	/* S0-3/X-12 (T-009): redefines the theme.json `section-title` preset's
	   own custom property rather than editing theme.json directly — the
	   export's h2 is a step (text-4xl -> text-5xl at >=768px), not the
	   continuous clamp() theme.json emits, and theme.json cannot express a
	   media-query step. Redefining the variable a `var()` points to works
	   regardless of whether `.has-section-title-font-size` carries
	   `!important` (T-002 spec §12.4 flagged this as unverified) — the
	   property still resolves to whatever this variable computes to.
	   Verified live: see T-009's task note. Affects all eight section h2s
	   (S0-3); TwoExperiences is the only one built by this task. */
	--wp--preset--font-size--section-title: 2.25rem; /* 36px */
}

@media (min-width: 768px) {
	:root {
		--gv-section-pad-y: 4rem; /* 64px */
		--wp--preset--font-size--section-title: 3rem; /* 48px */
	}
}

/* Focus ring and text-selection colour (T-004, source: src/index.css's
   `*:focus-visible` / `::selection`). Global default so any element without
   its own focus treatment gets the corrected pale gold ring; more specific
   per-component rules elsewhere in this file (e.g. .gv-inside__close on a
   dark backdrop, the WooCommerce form inputs) have higher specificity and
   keep winning on purpose — those are earlier deliberate contrast choices,
   not something this task reverts. */
:focus-visible {
	outline: 2px solid var(--wp--preset--color--gold);
	outline-offset: 4px;
}

::selection {
	background: var(--gv-gold-35);
	color: var(--wp--preset--color--charcoal);
}

/* Custom scrollbar (B-037), source: src/index.css:89-91 in the export — a
   bare selector, so it reaches every scrollable surface site-wide, not just
   the "What's Inside?" modal where the human found the gap. An 8px bar, an
   ivory track and a 35%-alpha gold thumb at 4px radius; both colours already
   have palette tokens (--wp--preset--color--ivory, --gv-gold-35 — the same
   composited token ::selection above uses), so this reads them rather than
   the export's literal hex/rgba. scrollbar-width/scrollbar-color are the
   Firefox/standards pair so the bar isn't Chrome-only. This does NOT touch
   .gv-ig__scroller's own scrollbar-width: none further down — that rule is
   also from the export and deliberately hides the Instagram strip's bar
   (B-029), and a more specific selector always wins the standards property
   regardless of source order. */
* {
	scrollbar-width: thin;
	scrollbar-color: var(--gv-gold-35) var(--wp--preset--color--ivory);
}

::-webkit-scrollbar {
	width: 8px;
	height: 8px;
}

::-webkit-scrollbar-track {
	background: var(--wp--preset--color--ivory);
}

::-webkit-scrollbar-thumb {
	background: var(--gv-gold-35);
	border-radius: 4px;
}

/* Eyebrow: uppercase label above a section or card heading. Carries its own
   colour and letter-spacing so patterns apply it as a class rather than
   copying inline typography attributes per instance — an eyebrow paragraph
   must carry no textColor attribute, or .has-*-color beats this rule.

   Margin (S0-2, T-009): the export's seven non-hero eyebrows are all
   `mb-3` (12px) — only the hero's is `mb-4` (16px). 1rem used to be the
   shared default for all eight; it is now the hero's own override
   (`.gv-hero__inner .gv-eyebrow`, below) and this default drops to 12px so
   the other seven sections read correctly as their section tasks land. */
.gv-eyebrow {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--eyebrow);
	font-weight: 400;
	line-height: 1;
	text-transform: uppercase;
	letter-spacing: 0.32em;
	color: var(--wp--preset--color--gold);
	margin: 0 0 0.75rem;
}

.gv-eyebrow--on-dark {
	color: var(--gv-ivory-80);
}

/* Buttons: the filled default lives in theme.json (styles.elements.button)
   so it is the WooCommerce/builder default too. The outline and card
   variants below are selected per-instance via className on core/button —
   which puts the class on the WRAPPER (`div.wp-block-button`), not the
   anchor: core's save output is
   `<div class="wp-block-button {className}"><a class="wp-block-button__link">`.
   Every rule here therefore targets the child anchor explicitly (`>
   .wp-block-button__link`), the same shape core's own block styles use.
   Getting this wrong is silent, not an error: it styles the wrapper and
   leaves the filled theme.json button showing through. Colours must never
   be set through block attributes either — same has-*-color trap as the
   eyebrow. Fixed T-005 finding 3. */

/* Outline: styles core's own `is-style-outline` block-style variation
   rather than adding a second, parallel mechanism — four shipped patterns
   (home-hero.php, home-experience.php x2, home-tiers.php) already carry
   is-style-outline, so this needed no pattern rewrite, and it retires the
   duplicate `.gv-btn--outline` class T-005 finding 11 flagged. Overrides
   core's own outline defaults (2px currentColor border, 0.667em/1.333em
   padding) with the reference's values. Radius and font-size are not set
   here — the anchor already carries them via styles.elements.button. */
.wp-block-button.is-style-outline > .wp-block-button__link {
	background: transparent;
	color: var(--wp--preset--color--charcoal);
	border: 1px solid var(--gv-fg-30);
	padding: 14px 40px;
}

.wp-block-button.is-style-outline > .wp-block-button__link:hover {
	background: var(--wp--preset--color--charcoal);
	color: var(--wp--preset--color--ivory);
}

.gv-btn--card > .wp-block-button__link {
	background: var(--wp--preset--color--ivory);
	color: var(--wp--preset--color--charcoal);
	border: none;
	padding: 12px 32px;
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	/* X-10 (T-009): Tailwind's shadow-lg — a neutral black shadow, not the
	   charcoal-tinted two-part shadow this used to carry. */
	box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}

/* Hover is triggered by the card, not the button. The card (.gv-xcard) is
   a plain container with no tabindex, so :focus-visible on it can never
   match — a keyboard user focuses the CTA *inside* it, which makes the
   card :focus-within, not :focus-visible. Fixed T-005 finding 6/8. */
.gv-xcard:hover .gv-btn--card > .wp-block-button__link,
.gv-xcard:focus-within .gv-btn--card > .wp-block-button__link {
	background: var(--wp--preset--color--gold);
}

/* A drawer link to "/#tiers" or "/#faq" (parts/header.html) scrolls the
   browser straight to the anchored element's own top edge. The header is
   sticky at ~78px tall (T-011), so without this every anchor lands with
   its heading tucked underneath it — present and "correct" by the
   attribute, wrong on screen. Scoped to sections that actually carry an
   anchor id; harmless on the ones that don't. */
.gv-section[id] {
	scroll-margin-top: 6rem;
}

.gv-section-head {
	text-align: center;
	margin-bottom: clamp(2.5rem, 5vw, 4rem);
}

.gv-lede {
	color: var(--wp--preset--color--muted-soft);
	max-width: 34rem;
	margin: 1rem auto 0;
}

.gv-lede--sm {
	font-size: var(--wp--preset--font-size--small);
	color: var(--gv-fg-50);
}

/* Charm tiles in the builder. Emoji placeholders until real photography
   exists — see open question 4.12. Sized so swapping in square images
   later needs no layout change. */
.gv-charm__icon {
	display: flex;
	align-items: center;
	justify-content: center;
	aspect-ratio: 1;
	font-size: 2rem;
	line-height: 1;
}

.gv-charm__icon img {
	width: 100%;
	height: 100%;
	object-fit: contain;
}

/* An unavailable charm must read as unavailable without relying on colour
   alone — it carries aria-disabled and visible text as well. */
.gv-charm[aria-disabled="true"] {
	opacity: 0.4;
	cursor: not-allowed;
}

/* --- Home: hero (T-006) ---
   Full-bleed autoplaying carousel matching home-reference.html's `.hero`.
   Per the primitives contract §3.2's measure table, the hero is the one
   full-bleed exception — no inner measure, no gv-section padding/gutter
   treatment, it fills the viewport edge to edge.

   patterns/home-hero.php emits this as raw markup (one wp:html block, not
   nested core blocks) with the `alignfull` class written by hand rather
   than via a block's own align attribute. That still works: WordPress's
   constrained-layout CSS is a plain child-combinator selector keyed to the
   containing block's generated `.wp-container-content-N` id
   (`.wp-container-content-N > .alignfull{...}`) and matches on the class of
   whatever HTML ends up there, regardless of whether the block parser
   recognises the child as a real block — verified in-browser, not assumed.

   Progressive enhancement (ADR-004): the rules immediately below (no class
   guard) render every slide visible and stacked in normal document flow,
   arrows and dots hidden — a script that fails to load degrades to "three
   stacked sections", never to a hidden slide or a hidden compliance
   placeholder. assets/js/hero.js (enqueued, defer) adds `gv-hero--js` to
   #hero once it has run; only the rules scoped under that class turn the
   stack into an actual crossfading, autoplaying carousel. Verified with
   JavaScript disabled in the browser. */
.gv-hero {
	position: relative;
	isolation: isolate;
	/* Contract table (§3): `overflow-hidden` is part of the export's own
	   hero section class, not previously carried here. Surfaced by browser
	   verification (T-009): the H-1 media scale(1.05) on inactive slides
	   was pushing document.documentElement.scrollWidth past the viewport
	   at every width — a transformed box's visual bounds count toward an
	   ancestor's scrollable overflow even at opacity:0/pointer-events:none.
	   Clipping it here matches the export and removes the phantom
	   horizontal scroll. */
	overflow: hidden;
}

.gv-hero__slide {
	position: relative;
	padding: clamp(4rem, 9vw, 8rem) var(--wp--preset--spacing--gutter);
}

/* S-004/T-018 rework: the box geometry and the legibility scrim were living
   only on the .gv-image-placeholder compound below, so both vanished the
   moment a real photo landed and Goldvion_Block_Hero stopped emitting the
   placeholder class — the media layer stopped being full-bleed and the
   copy stopped overlaying it. Moved here, to the base selector, so a real
   photo gets the same box and the same scrim the placeholder always had.
   .gv-inside__media (below, "Home: sections 1-4") is the shape this
   follows — its ratio already sat on the base selector, not the compound. */
.gv-hero__media {
	position: absolute;
	inset: 0;
	z-index: -1;
}

/* H-16 (T-009): the export's ivory gradient scrim (top to bottom, 40% ->
   20% -> 60%) is in scope even though the photography behind it is not
   (ADR-011 §10 item 7) — moved to the base selector with the geometry
   above so the copy's contrast target holds over a real photo too, not
   just over the placeholder it was written against. */
.gv-hero__media::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(to bottom, var(--gv-ivory-40) 0%, var(--gv-ivory-20) 50%, var(--gv-ivory-60) 100%);
}

/* Compound selector, not just .gv-hero__media: the shared .gv-image-
   placeholder rule (below, "Home: sections 1-4") sets align-items:center
   and comes later in the file, so a bare .gv-hero__media rule here would
   lose that cascade fight and the placeholder text would silently recentre
   instead of sitting flush to the top like every other reference detail
   that follows this repo's dashed-border placeholder convention. Only the
   placeholder-specific alignment stays here now; the geometry and scrim
   above apply to a real photo as well. */
.gv-hero__media.gv-image-placeholder {
	align-items: flex-start;
	padding-top: 2rem;
}

/* S-004/T-018: object-fit: cover does nothing without a definite box to
   cover, and .gv-hero__media above is that box (position:absolute;
   inset:0) — fill it edge to edge, same shape as .gv-ig__tile img. */
.gv-hero__media img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* One modifier per palette slug a slide can use — driven entirely by the
   `media_bg` value in $gv_hero_slides, so a new slide needs no new CSS
   unless it introduces a background token not in the palette yet.
   Compound selector (repeated class), same shape and same reason as
   T-007's .gv-xcard__media.gv-xcard__media--blush: .gv-image-placeholder's
   own background:surface-muted (below, "Home: sections 1-4") is a single
   class, the same specificity as a bare .gv-hero__media--<slug> rule, and
   sits later in the file, so it silently won that cascade fight and every
   slide rendered surface-muted regardless of its modifier — the exact bug
   T-007 hit and fixed in its own card, flagged forward here and now closed.
   Re-verified by asserting each slide's computed background against its
   token, not by eye, per state/repos/goldvion.md's verification note. */
.gv-hero__media.gv-hero__media--surface-muted {
	background: var(--wp--preset--color--surface-muted);
}

.gv-hero__media.gv-hero__media--blush {
	background: var(--wp--preset--color--blush);
}

.gv-hero__media.gv-hero__media--butter {
	background: var(--wp--preset--color--butter);
}

.gv-hero__inner {
	position: relative;
	text-align: center;
	max-width: 42rem; /* H-7: export's max-w-2xl, was 44rem */
	margin-inline: auto;
}

/* H-6/H-8 (T-009): the export's h1 carries no margin at all — the space
   below it is the lede's own mt-5 (20px), not a heading margin. The lede
   also drops the shared `.gv-lede`'s 34rem cap (the hero's own 42rem
   measure on `.gv-hero__inner` already does that job) and steps to 18px
   at >=768px — the one surviving half of H-8, since T-004 already made the
   16px base site-wide (S0-4, closed). */
.gv-hero__inner .gv-lede {
	margin: 1.25rem auto 0;
	max-width: none;
}

@media (min-width: 768px) {
	.gv-hero__inner .gv-lede {
		font-size: 1.125rem; /* 18px */
	}
}

/* Corrected against the export (H-3/V-3, T-009): the hero eyebrow is
   charcoal at 60%, not the `muted` preset's 70% — that 70% figure came
   from home-reference.html, which T-002's spec supersedes for this value.
   T-006 confirmed live (`.gv-hero__inner .gv-eyebrow` computed
   rgba(38,36,31,0.7)) that no `.has-*-color` attribute was involved, so a
   plain colour swap is sufficient here, not a theme.json preset.

   Margin (S0-2/H-4): the hero is the one section whose eyebrow keeps the
   16px (`mb-4`) margin the shared `.gv-eyebrow` default used to carry
   site-wide — restated here now that the shared default has dropped to
   12px for the other seven sections. */
.gv-hero__inner .gv-eyebrow {
	color: var(--gv-fg-60);
	margin-bottom: 1rem;
}

/* .gv-hero__heading (T-012): the same visual style as the real h1, used on
   the hero's non-active slides so exactly one <h1> exists on the page — see
   the docblock in home-hero.php for why. It is a <p>, not a heading, so it
   does not pick up theme.json's styles.elements.heading (font, weight,
   line-height) the way an actual <h1>-<h6> does — restated explicitly here
   rather than relying on the element default. */
.gv-hero__inner h1,
.gv-hero__inner .gv-hero__heading {
	font-size: var(--wp--preset--font-size--hero-title);
	margin: 0; /* H-6: the export's h1 carries no margin; the lede's own mt-5 does the spacing */
}

.gv-hero__inner .gv-hero__heading {
	font-family: var(--wp--preset--font-family--display);
	font-weight: 500;
	line-height: 1.1;
}

.gv-hero__cta {
	margin-top: 2rem;
	display: flex;
	gap: 0.75rem;
	justify-content: center;
	flex-wrap: wrap;
}

/* H-9: the hero CTA sits over an image (once photography exists) or the
   flat placeholder ground; the export blurs whatever is behind it
   (backdrop-blur-sm = 4px). Scoped to the hero's own outline button, not
   the shared is-style-outline rule above — no other section's CTA carries
   this in the export. */
.gv-hero__cta .wp-block-button.is-style-outline > .wp-block-button__link {
	backdrop-filter: blur(4px);
	-webkit-backdrop-filter: blur(4px);
}

/* H-10 (T-009): the reference's text glyphs (&#8249;/&#8250; in a 3rem box,
   charcoal-70, 0.5rem inset) are replaced with the export's actual chevron
   — a 24px lucide ChevronLeft/ChevronRight at strokeWidth 1 (not the 1.5
   the survey's icon table claims site-wide; the architect's T-002
   correction found the hero specifically ships at 1, distinct from the
   testimonial carousel's 1.5 — see home-hero.php's inline SVGs), charcoal
   at 40%, inset 16px (`left-4`/`right-4`). No hit-area padding is added
   around the icon: the export positions the bare 24px glyph at the inset,
   nothing more. */
.gv-hero__arrow {
	position: absolute;
	top: 50%;
	transform: translateY(-50%);
	z-index: 2;
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
	color: var(--gv-fg-40);
	width: 24px;
	height: 24px;
	line-height: 0;
	display: none;
}

.gv-hero__arrow svg {
	width: 24px;
	height: 24px;
	display: block;
}

.gv-hero__arrow:hover {
	color: var(--wp--preset--color--charcoal);
}

.gv-hero__arrow--prev {
	left: 1rem;
}

.gv-hero__arrow--next {
	right: 1rem;
}

.gv-hero__dots {
	position: absolute;
	bottom: 2rem; /* H-12: export's bottom-8 (32px), was 1.75rem */
	left: 50%;
	transform: translateX(-50%);
	z-index: 2;
	display: none;
	gap: 0.5rem;
}

/* S-008: lift the dots clear of the fixed shipping pill, but only on the
   widths where the pill can actually reach them.

   The dots are centred; the pill is pinned bottom-left and is a fixed
   ~434px wide (its text does not wrap above 600px). So they collide when
   half the viewport, less half the dots rail, is inside the pill's right
   edge — measured as overlapping by 35px at 900px wide, and clearing by
   only 15px at 1000px. Above this breakpoint the dots keep the export's
   32px (delta H-12) and sit level with, but well to the right of, the
   pill, exactly as the export has them.

   1040px rather than the ~992px where they technically stop touching: the
   pill's text is client-editable copy, so its width is not ours to rely
   on. The extra tolerance is for a longer sentence, not for geometry.

   Below it they would be rendered behind the pill, so they move up by its
   band. --gv-hero-bottom-clear grows below 600px, where the pill goes
   full-bleed and wraps to two lines, and this rule picks that up free. */
@media (max-width: 1040px) {
	.gv-hero__dots {
		bottom: calc(2rem + var(--gv-hero-bottom-clear));
	}
}

/* H-13 (T-009): dot colours corrected against the export — inactive
   charcoal 25% (was --gv-fg-30/30%), active charcoal 60% (was solid
   charcoal). Geometry (6px -> 32px over 500ms) already matched (H-14,
   confirmed by T-006) and is untouched. */
.gv-hero__dots button {
	width: 6px;
	height: 6px;
	padding: 0;
	border: 0;
	cursor: pointer;
	border-radius: var(--wp--custom--radius--pill);
	background: var(--gv-fg-25);
	transition: width 0.5s, background 0.5s;
}

.gv-hero__dots button[aria-pressed="true"] {
	width: 32px;
	background: var(--gv-fg-60);
}

/* Carousel mode — added by assets/js/hero.js once it has run. Everything
   above this rule is the no-JS baseline and stays true with the class
   present too; this only layers the fixed-height stack, the crossfade and
   the two controls on top of it. */
.gv-hero--js {
	/* S-008: fit the viewport, so the slide dots are visible without
	   scrolling. Was `height: 100vh`, which overflowed the fold by the
	   sticky header's height plus the fixed shipping pill's band — see
	   --gv-header-h's comment for why our header makes 100vh wrong here.

	   Two declarations on purpose: `svh` is the SMALL viewport height, the
	   one that excludes mobile browser chrome. Plain `vh` on a phone is the
	   LARGE viewport — measured as if the address bar were hidden — which
	   is precisely why the dots fell below the fold worst on mobile. The
	   first line is the fallback for anything that does not know `svh`. */
	height: calc(100vh - var(--gv-header-h));
	height: calc(100svh - var(--gv-header-h));

	/* Was 600px, the export's min-h-[600px]. That floor re-creates the very
	   bug this rule fixes: on any window shorter than ~734px it wins over
	   the calc above and pushes the dots back off-screen. 480px still holds
	   the headline, lede and button comfortably. */
	min-height: 30rem; /* 480px */
	display: grid;
}

/* H-1 (T-009): crossfade corrected to the export's 1.2s (was 0.7s), and
   the media layer now also scales 1.05 -> 1 as a slide becomes active
   (framer-motion's `initial{opacity:0,scale:1.05} -> animate{opacity:1,
   scale:1}`), giving the media its own transition track distinct from the
   copy block's (H-2, below). */
.gv-hero--js .gv-hero__slide {
	grid-area: 1 / 1;
	display: grid;
	place-items: center;
	opacity: 0;
	transition: opacity 1.2s ease;
	pointer-events: none;
}

.gv-hero--js .gv-hero__slide[data-active="true"] {
	opacity: 1;
	pointer-events: auto;
}

.gv-hero--js .gv-hero__media {
	transform: scale(1.05);
	transition: transform 1.2s ease;
}

.gv-hero--js .gv-hero__slide[data-active="true"] .gv-hero__media {
	transform: scale(1);
}

/* H-2 (T-009): the copy block gets its own, slower, delayed entrance
   (framer-motion's `initial{opacity:0,y:30} -> animate{opacity:1,y:0}`,
   duration 0.8s, delay 0.2s) layered on top of the slide's own 1.2s
   opacity fade above — this repo ships no animation timeline library, so
   the two tracks are separate CSS transitions on the same `data-active`
   toggle rather than a single choreographed sequence; both are
   independently verifiable via getComputedStyle's transition-duration/
   -delay. */
.gv-hero--js .gv-hero__inner {
	opacity: 0;
	transform: translateY(30px);
	transition: opacity 0.8s ease 0.2s, transform 0.8s ease 0.2s;
}

.gv-hero--js .gv-hero__slide[data-active="true"] .gv-hero__inner {
	opacity: 1;
	transform: translateY(0);
}

.gv-hero--js .gv-hero__arrow,
.gv-hero--js .gv-hero__dots {
	display: flex;
}

/* T-005: the export's HeroSlideshow shows its arrows at every width (H-11)
   — there is no client breakpoint that hides them, so the 600px rule that
   used to hide them below that width was a phantom breakpoint, not a
   client value, and is removed rather than reassigned to 768px. */

/* Shipping pill + Instagram FAB: pinned to the viewport corners independent
   of any one section (home-reference.html places them after every section,
   not inside the hero) — patterns/home-hero.php emits them because T-006
   owns them. Not one of Home.jsx's eight imported sections and not present
   in the export at all, so there is no client breakpoint to reconcile
   against here — the 600px value below is ours by necessity, not a
   competing system (T-005). */
.gv-shipping-pill {
	position: fixed;
	left: 1rem;
	bottom: 1rem;
	z-index: 40;
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	background: var(--wp--preset--color--ivory);
	border: 1px solid var(--wp--preset--color--hairline);
	border-radius: var(--wp--custom--radius--pill);
	padding: 0.6rem 1.1rem;
	font-size: var(--wp--preset--font-size--x-small);
	text-transform: uppercase;
	letter-spacing: 0.14em;
	color: var(--wp--preset--color--muted);
	margin: 0;
}

.gv-ig-fab {
	position: fixed;
	right: 1rem;
	bottom: 1rem;
	z-index: 40;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	border-radius: var(--wp--custom--radius--pill);
	background: var(--wp--preset--color--ivory);
	border: 1px solid var(--wp--preset--color--hairline);
	color: var(--wp--preset--color--charcoal);
	text-decoration: none;
}

.gv-ig-fab:hover {
	background: var(--wp--preset--color--surface-muted);
}

@media (max-width: 600px) {
	/* S-008: the pill below goes full-bleed here and its text wraps to two
	   lines, so it owns a taller band than it does on desktop — measured
	   60px + its 0.5rem inset at 390px wide. The hero reserves this out of
	   its own height so the slide dots clear it. */
	:root {
		--gv-hero-bottom-clear: 4.5rem; /* 72px */
	}

	.gv-shipping-pill {
		left: 0.5rem;
		right: 0.5rem;
		bottom: 0.5rem;
		justify-content: center;
	}

	.gv-ig-fab {
		display: none;
	}
}

/* --- Home: shared scroll-reveal utility (S0-7, T-009) ---
   Seven of the eight home sections animate their own content in on scroll
   in the export, each with its own duration/stagger (S0-7) — TwoExperiences
   0.8s/0.15, TierSection 0.7s/0.1, MaterialSection 0.6s/0.1,
   InstagramSection 0.4s/0.05 (scale, not y). This is the shared mechanism;
   TwoExperiences is the only section wired to it by this task. Later
   section tasks apply the same `gv-reveal` class + data attributes rather
   than reinventing the observer.

   Progressive enhancement (ADR-004): the base rule below renders every
   `.gv-reveal` element fully visible with no transform — a page whose
   assets/js/reveal.js fails to load shows all content immediately, never a
   permanently-hidden section. reveal.js's only job is to add
   `gv-reveal--armed` (switches on the pre-reveal hidden state) and then,
   once the element crosses the viewport, `gv-reveal--in` (transitions it
   to rest) — once only, never reversed, matching every `viewport:{once:
   true}` in the export. Respects prefers-reduced-motion (reveal.js exits
   early and never arms anything when it matches).

   Duration/delay are read from `--gv-reveal-duration`/`--gv-reveal-delay`,
   set per section below via className + nth-child rather than a data
   attribute reveal.js would have to add itself — see reveal.js's own
   docblock for why (block-validation risk on real core blocks). */
.gv-reveal {
	opacity: 1;
	transform: none;
}

.gv-reveal.gv-reveal--armed {
	opacity: 0;
	/* --gv-reveal-distance (T-012/M-2): most sections rise 40px (y:40 in the
	   export); MaterialSection's own export value is y:30 — see its
	   per-section override below rather than changing this 40px default,
	   which the other sections still rely on.
	   --gv-reveal-scale (S-003/T-013, I-3): InstagramSection is the export's
	   only *scale*-based reveal (`scale:0.95 -> 1`, no y-offset at all) —
	   extending this one shared rule with a second custom property, rather
	   than forking a second reveal mechanism, keeps every section on the
	   same observer/class pair. Instagram sets --gv-reveal-distance:0px so
	   only the scale term moves; every other section leaves this at its
	   default of 1 (no-op) and is unaffected. */
	transform: translateY(var(--gv-reveal-distance, 40px)) scale(var(--gv-reveal-scale, 1));
	transition: opacity var(--gv-reveal-duration, 0.8s) ease, transform var(--gv-reveal-duration, 0.8s) ease;
	transition-delay: var(--gv-reveal-delay, 0s);
}

/* T-011 (S-005): resting transform is `none`, not `translateY(0) scale(1)`.
   Visually identical (both are the identity transform), but a non-`none`
   transform value on ANY ancestor -- even an identity matrix -- creates a
   containing block for `position: fixed` descendants (CSS Transforms
   Level 1). `.gv-tier` carries this class, and `.gv-inside__panel` sits
   fixed inside it: once a tier's reveal animation finishes (i.e. as soon
   as a visitor has scrolled it into view, which "What's Inside?" requires
   anyway), the panel silently stopped positioning against the viewport
   and positioned against `.gv-tier` instead -- found while re-measuring
   this panel for the `height: fit-content` fix above; a second, unrelated
   bug hiding behind the first. */
.gv-reveal.gv-reveal--armed.gv-reveal--in {
	opacity: 1;
	transform: none;
}

/* TwoExperiences (X-1/S0-7): 0.8s duration, 0.15s per-card stagger — the
   first card reveals at 0s, the second at 0.15s. */
.gv-experience .wp-block-column:nth-child(1) .gv-xcard.gv-reveal {
	--gv-reveal-duration: 0.8s;
	--gv-reveal-delay: 0s;
}

.gv-experience .wp-block-column:nth-child(2) .gv-xcard.gv-reveal {
	--gv-reveal-duration: 0.8s;
	--gv-reveal-delay: 0.15s;
}

/* T-1 (T-011): TierSection cards are 0.7s duration, i*0.1s stagger — a
   different pair from TwoExperiences' 0.8s/0.15s above, per S0-7 ("not one
   shared value"). Three tiers, three columns, nth-child 1/2/3. */
.gv-tiers .wp-block-column:nth-child(1) .gv-tier.gv-reveal {
	--gv-reveal-duration: 0.7s;
	--gv-reveal-delay: 0s;
}

.gv-tiers .wp-block-column:nth-child(2) .gv-tier.gv-reveal {
	--gv-reveal-duration: 0.7s;
	--gv-reveal-delay: 0.1s;
}

.gv-tiers .wp-block-column:nth-child(3) .gv-tier.gv-reveal {
	--gv-reveal-duration: 0.7s;
	--gv-reveal-delay: 0.2s;
}

/* M-2 (T-012): MaterialSection's four claim cards are 0.6s duration,
   i*0.1s stagger, y:30 (not the shared 40px default — see
   --gv-reveal-distance on the base rule above). Four columns, nth-child
   1-4. */
.gv-claims .wp-block-column:nth-child(1) .gv-claim.gv-reveal {
	--gv-reveal-duration: 0.6s;
	--gv-reveal-delay: 0s;
	--gv-reveal-distance: 30px;
}

.gv-claims .wp-block-column:nth-child(2) .gv-claim.gv-reveal {
	--gv-reveal-duration: 0.6s;
	--gv-reveal-delay: 0.1s;
	--gv-reveal-distance: 30px;
}

.gv-claims .wp-block-column:nth-child(3) .gv-claim.gv-reveal {
	--gv-reveal-duration: 0.6s;
	--gv-reveal-delay: 0.2s;
	--gv-reveal-distance: 30px;
}

.gv-claims .wp-block-column:nth-child(4) .gv-claim.gv-reveal {
	--gv-reveal-duration: 0.6s;
	--gv-reveal-delay: 0.3s;
	--gv-reveal-distance: 30px;
}

/* I-3 (T-013): InstagramSection's tiles are the export's only scale-based
   reveal — 0.4s duration, scale 0.95 -> 1, no y-offset
   (--gv-reveal-distance:0px overrides the shared 40px default). Tiles are
   plain siblings inside .gv-ig__scroller (a flex row, not wp:columns).
   T-015 (ADR-020): the eight per-index nth-child rules above are gone — the
   tile count is no longer eight by construction now that the strip is a
   dynamic block (goldvion/instagram-tiles) reading a CPT a client can add
   to. The per-tile stagger delay (0s, 0.05s, … capped at 0.35s, same as
   before) is now set inline per tile as --gv-reveal-delay by the block's
   render_callback, so a ninth tile gets the same treatment as its
   neighbours instead of falling through to the base .gv-reveal defaults. */
.gv-ig__scroller .gv-ig__tile.gv-reveal {
	--gv-reveal-duration: 0.4s;
	--gv-reveal-distance: 0px;
	--gv-reveal-scale: 0.95;
}

/* --- Home: choose your experience (T-007) ---
   Two overlay cards matching home-reference.html's .xcard/.experience. The
   reference's own card is a single <a> wrapping the whole tile; core has no
   block that renders as an anchor with arbitrary block content inside it
   (Group's own "HTML element" picker offers div/header/main/section/
   article/aside/footer, never a), so .gv-xcard here is a wp:group (a plain
   div) and the CTA carries the real link instead — the only structural
   deviation from the reference markup, kept so patterns/home-experience.php
   stays real, click-to-edit core blocks per T-006's precedent for sections
   2-11. Hover/focus-within still work identically since neither depends on
   the outer element being a link. */
.gv-experience {
	/* Hook for the two card columns; core's own wp:columns handles the
	   50/50 split and the single-column mobile stack, so no grid override
	   is needed here — see the "Home: responsive pass" note below. */
}

/* X-2/X-3 (T-009): radius corrected to the export's rounded-lg (4px, the
   `base` token) — scoped to `.gv-xcard` only, not a change to the shared
   `--radius--card` token (S0-5 names five call sites in total; the other
   four belong to sections outside this task and get the same one-line fix
   from their own section tasks). Sizing switched from a fixed
   `min-height` clamp to the export's own aspect ratio (4/5 base, 3/4 at
   >=768px, media query below) — a different shape at every width, not a
   height that happens to look similar at one. */
.gv-xcard {
	position: relative;
	display: flex;
	flex-direction: column;
	justify-content: flex-end;
	aspect-ratio: 4 / 5;
	padding: 32px 32px 48px;
	border-radius: var(--wp--custom--radius--base);
	overflow: hidden;
	isolation: isolate;
}

@media (min-width: 768px) {
	.gv-xcard {
		aspect-ratio: 3 / 4;
	}
}

.gv-xcard__media {
	position: absolute;
	inset: 0;
	z-index: -2;
}

/* Compound selector for the same cascade-order reason as
   .gv-hero__media.gv-image-placeholder above: the shared .gv-image-
   placeholder rule sets align-items:center and comes later in this file, so
   a bare .gv-xcard__media rule here would lose that fight and the
   placeholder caption would recentre instead of sitting flush to the media
   edge like the reference's .xcard__media .img-ph. */
.gv-xcard__media.gv-image-placeholder {
	width: 100%;
	height: 100%;
	border: 0;
	border-radius: 0;
	align-items: flex-start;
}

/* Compound selector, same reason as .gv-xcard__media.gv-image-placeholder
   just above: .gv-image-placeholder's own background:surface-muted is the
   same specificity (one class) and sits later in the file, so a bare
   .gv-xcard__media--blush rule loses the cascade and silently renders
   surface-muted instead — caught here by checking the computed background
   in the browser, not by reading the CSS. The pre-existing
   .gv-hero__media--blush/--butter modifiers (T-006) share this exact shape
   and were found to have the same bug when checked collaterally; flagged
   forward rather than fixed here since T-006's file is out of this task's
   scope. */
.gv-xcard__media.gv-xcard__media--blush {
	background: var(--wp--preset--color--blush);
}

/* S-004/T-018: object-fit: cover does nothing without a definite box to
   cover — .gv-xcard__media above already is one (position:absolute;
   inset:0), unlike .gv-hero__media and .gv-tier__media, whose box lived
   only on the placeholder compound. This rule was simply missing. The real
   photo is emitted by `goldvion/xcard-media` (finding 4's fix) via a plain
   wp_get_attachment_image() call with no <figure> wrapper — same shape as
   .gv-hero__media/.gv-tier__media's img, no extra wrapper rule needed. */
.gv-xcard__media img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Legibility scrim: the design overlays card copy on the photograph — this
   is what keeps that text readable once real photography replaces the
   placeholder (open question 4.12). */
.gv-xcard::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background: linear-gradient(to top, var(--gv-fg-60) 0%, var(--gv-fg-10) 50%, transparent 100%);
}

/* X-6 (T-009): the export sets spacing per element (eyebrow mb-3, h3 none,
   copy mt-4, CTA mt-6), not a uniform flex gap — the gap below is dropped
   to 0 and each child's own margin restated to match. */
.gv-xcard__body {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	color: var(--wp--preset--color--ivory);
}

.gv-xcard__body .gv-eyebrow {
	margin: 0 0 0.75rem; /* X-6: mb-3, 12px */
}

/* X-7: the card eyebrow steps 11px -> 12px at >=768px (was fixed 11px). */
@media (min-width: 768px) {
	.gv-xcard__body .gv-eyebrow--on-dark {
		font-size: 0.75rem;
	}
}

/* X-8 (T-009): h3 steps 30px -> 48px at >=768px. The pattern used to carry
   `fontSize:"xx-large"` (`has-xx-large-font-size`), which set the correct
   30px base but blocked this rule's 768px step: WordPress's global-styles
   stylesheet emits `.has-xx-large-font-size{font-size:var(...) !important}`
   (confirmed live — a plain class-selector override here computed 30px at
   any width, T-009 task note), the same family of trap this repo's own
   notes document for `.has-*-color`, just for font-size classes too.
   Rather than fight it (or redefine the shared `--wp--preset--font-size--
   xx-large` token, which the tier modal's price also uses and this task
   does not own), the pattern's heading block now carries no `fontSize`
   attribute at all, so no `has-*-font-size` class exists to out-rank this
   selector. */
.gv-xcard__body .wp-block-heading {
	margin: 0; /* X-6 */
	font-size: 1.875rem; /* 30px base */
}

@media (min-width: 768px) {
	.gv-xcard__body .wp-block-heading {
		font-size: 3rem; /* 48px */
	}
}

/* :not(.gv-eyebrow) (T-009 fix, found in browser verification): the card
   eyebrow is also a <p> inside .gv-xcard__body, and a bare `.gv-xcard__body
   p` selector's specificity (0,1,1) beats `.gv-eyebrow` (0,1,0), so without
   this exclusion the eyebrow's font-size was silently hijacked to this
   rule's 14px below 768px, where nothing more specific was around to win
   it back (`.gv-xcard__body .gv-eyebrow--on-dark` only exists at >=768px,
   X-7). Caught by getComputedStyle, not by eye — same lesson as the
   .gv-hero__media--blush cascade trap this repo's notes already document. */
.gv-xcard__body p:not(.gv-eyebrow) {
	color: var(--gv-ivory-70);
	font-size: var(--wp--preset--font-size--small); /* X-9: 14px base, was fixed 16px */
	max-width: 26rem;
	margin: 1rem 0 0; /* X-6: mt-4, 16px */
}

@media (min-width: 768px) {
	.gv-xcard__body p:not(.gv-eyebrow) {
		font-size: var(--wp--preset--font-size--medium); /* X-9: 16px at >=768px */
	}
}

.gv-xcard__body .wp-block-buttons {
	margin-top: 1.5rem; /* X-6: mt-6, 24px */
}

.gv-btn--card svg {
	width: 0.875rem;
	height: 0.875rem;
	flex: none;
}

/* --- Home: craftsmanship (T-012, reconstructed against the base44 export) ---
   Four fixed material-claim badges matching MaterialSection.jsx. See
   patterns/home-craftsmanship.php's docblock for why all four titles/bodies
   stay marked placeholders (design-content-extract.md §6.3) — only the
   ring and the section's own lede are real content here. M-1 (ground now
   plain ivory, not `band`) is a pattern-only fix — see that file. */
.gv-claims {
	margin-top: 3.5rem;
}

/* M-9: base gap is the export's literal 24px (pattern's own blockGap
   attribute, T-012); this steps it to 32px at >=768px.

   Review fix (T-014 finding 4): same wrong premise as W-12's gap half
   above, and the same fix — no inline style exists on this element
   (`home-craftsmanship.php` emits a bare `<div class="wp-block-columns
   gv-claims">`); core's gap comes from a generated container class
   (`.wp-container-core-columns-is-layout-*`, 0,1,0) printed in `<head>`
   before this stylesheet loads. `.gv-claims` ties that class on
   specificity (both 0,1,0), so it comes down to source order — and this
   stylesheet's `<link>` loads after the generated class's inline
   `<style>`, so `.gv-claims` already wins without help. Verified
   in-browser the same way as W-12's: `!important` removed live via the
   CSSOM, `getComputedStyle` at 768px+ unchanged at 32px. */
@media (min-width: 768px) {
	.gv-claims {
		column-gap: 2rem;
		row-gap: 2rem;
	}
}

/* M-7: this section's own lede drops the shared `.gv-lede`'s 34rem cap and
   1rem (16px) top margin — the export has no measure cap here and its
   `mt-3` is 12px, not the hero's/shared default's 16px. Scoped to this
   section only; other sections' `.gv-lede` usage is unaffected. */
.gv-craftsmanship .gv-lede {
	max-width: none;
	margin-top: 0.75rem;
}

.gv-claim {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
}

/* 56px ring, pale-gold (the `gold` preset, T-001 §1.1) at 30% border. */
.gv-claim__icon {
	display: flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: 3.5rem;
	height: 3.5rem;
	margin-bottom: 1rem;
	border-radius: var(--wp--custom--radius--pill);
	border: 1px solid var(--gv-gold-30);
	color: var(--wp--preset--color--gold);
}

.gv-claim__icon svg {
	width: 1.25rem;
	height: 1.25rem;
}

/* Craft-claim kept typography, applied S-007/T-004 (T-006 review, F2):
   once the craft claims carry the client's own copy rather than a
   `.gv-placeholder` paragraph, the kept state's typography has to be set
   explicitly — `.gv-placeholder`'s own italic/small styling is gone along
   with the marker, and a bare `wp-block-paragraph` renders both title and
   body at identical default type with no distinction between them.
   Recorded once here rather than re-derived: title is Cormorant (the
   `display` font family) at 18px charcoal (`font-size--body` is exactly
   1.125rem = 18px); body is 12px (`font-size--x-small` = 0.75rem)
   leading-relaxed (1.625) at charcoal 50% (`muted-medium`). */
.gv-claim__title {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--body);
	color: var(--wp--preset--color--charcoal);
}

.gv-claim .gv-claim__body {
	margin-top: 0.5rem;
	font-size: var(--wp--preset--font-size--x-small);
	line-height: 1.625;
	color: var(--wp--preset--color--muted-medium);
}

/* --- Home: a simple, thoughtful process (T-008) ---
   Charm/Mystery toggle over patterns/home-how-it-works.php's two step
   panels. Progressive enhancement (ADR-004), additive-class shape matching
   T-006's hero and T-003's drawer: the base state (no class on
   .gv-how-it-works) renders BOTH panels, each under its own visible mode
   label, and the toggle itself stays hidden — a control that does nothing
   without JS is worse than no control. assets/js/how-it-works.js is the
   only thing that adds `gv-how-it-works--js`, at which point the toggle
   appears, the mode labels (now redundant with the toggle) hide, and the
   script picks one panel to show. A script that fails to load never hides
   either panel's content — both stay reachable and visible.

   W-1 (T-011): ground is the export's blush/10 over ivory, computed by hand
   against ivory (spec §12.3) since color-mix can't be asked "composite
   against whatever is under this element" — a plain classname, not a
   theme.json backgroundColor attribute/token, so it never has to fight
   .has-*-color's !important. */
.gv-how-it-works {
	background: color-mix(in srgb, var(--wp--preset--color--blush) 10%, var(--wp--preset--color--ivory));
}

/* W-2/W-3 (T-011): mt-8 (32px) below the h2, no gap between the two
   buttons (they abut inside the p-1 rail) — was a bottom-margin-only rail
   with a 4px inter-button gap ported from home-reference.html. The other
   half of W-2 (mt-14/56px between the rail and the grid) lives on
   .gv-how-it-works__panel below, since that is the element actually
   adjacent to the rail once JS hides the other panel. */
.gv-how-it-works__toggle {
	display: none;
	justify-content: center;
	gap: 0;
	margin-top: 2rem;
	margin-bottom: 0;
	width: max-content;
	margin-inline: auto;
	background: transparent;
	border: 1px solid var(--gv-gold-30);
	border-radius: var(--wp--custom--radius--pill);
	padding: 0.25rem;
}

.gv-how-it-works--js .gv-how-it-works__toggle {
	display: flex;
}

.gv-how-it-works__toggle button {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--micro);
	text-transform: uppercase;
	letter-spacing: 0.32em;
	padding: 8px 24px;
	border: 0;
	border-radius: var(--wp--custom--radius--pill);
	background: none;
	color: var(--gv-fg-50);
	cursor: pointer;
	transition: background 0.25s, color 0.25s;
}

.gv-how-it-works__toggle button[aria-pressed="true"] {
	background: var(--wp--preset--color--charcoal);
	color: var(--wp--preset--color--ivory);
}

/* The no-JS substitute for the toggle: each panel keeps its own labelled
   heading so "which mode is this" is never ambiguous with the control that
   would normally say so hidden. Redundant, and hidden, once JS is running
   the real toggle. */
.gv-how-it-works__mode-label {
	margin-bottom: 1.5rem;
}

.gv-how-it-works--js .gv-how-it-works__mode-label {
	display: none;
}

/* W-2 (T-011), second half: mt-14 (56px) between the rail and the grid.
   Both panels carry this — in the no-JS baseline both are visible and
   stacked, so both read the same rhythm; once JS hides one, the visible
   panel is the one directly under the toggle and this is the space that
   applies.

   W-13 (T-011): opacity-only crossfade, `mode="wait"` (exit fully before
   entering), driven by assets/js/how-it-works.js toggling these two
   classes rather than a third state — a script that never runs leaves
   `opacity` at its own base value of 1 and both panels simply show/hide
   via the `hidden` attribute exactly as before, so nothing here is
   load-bearing for ADR-004. Respects prefers-reduced-motion: the script
   skips the fade (and the 400ms stagger) entirely when it matches, toggling
   `hidden` immediately instead. */
.gv-how-it-works__panel {
	margin-top: 3.5rem;
	opacity: 1;
	transition: opacity 0.4s ease;
}

.gv-how-it-works__panel--fade-out {
	opacity: 0;
}

.gv-how-it-works__panel--fade-in {
	opacity: 0;
	transition: none;
}

.gv-how-it-works__step {
	text-align: center;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0;
}

/* W-7 (T-011): the export's spacing is NOT a uniform gap — number-to-title
   is mt-3 (12px), title-to-body is mt-2 (8px). Explicit margins replace
   the old uniform flex `gap` above (now 0) so the two distances can differ;
   flexbox doesn't collapse margins between its own children, so these are
   the full story, not additive with anything else. */
.gv-how-it-works__step .wp-block-heading {
	margin-top: 0.75rem;
	margin-bottom: 0;
}

.gv-how-it-works__step p:not(.gv-how-it-works__step-num) {
	margin-top: 0.5rem;
	margin-bottom: 0;
}

/* No block attribute expresses this: gold at 50% has no palette slug of its
   own (§4.2 of the primitives contract — it exists as a composited custom
   property precisely because the design uses it nowhere else), so the
   numeral's face, size and colour are all component CSS rather than a mix
   of attribute + override. */
.gv-how-it-works__step-num {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--display-lg);
	color: var(--gv-gold-50);
	line-height: 1;
	margin: 0;
}

/* Step copy's colour comes from the paragraph's own textColor attribute
   (muted-medium, W-8); this only adds what a block attribute cannot
   express — the 320px measure and the W-7 top margin above. Widening the
   max-width is the specific, already-hit regression: the reference's own
   comment reads "18px was wrapping these", and T-001 §1.2/§2.2 retuned
   `small` to 14px for exactly this paragraph. Losing either the 14px
   font-size or this max-width reintroduces the wrap. */
.gv-how-it-works__step p:not(.gv-how-it-works__step-num) {
	max-width: 20rem;
}

/* A connector BETWEEN steps only — 64px, pale gold at 30%, never after the
   last step in a panel. Each .gv-how-it-works__step is the lone child of
   its own wp:column, so it is always :last-child *of that column* — the
   :last-child has to target the column, not the step, or every connector
   in every column matches "last" and none of them render. Scoped to
   .gv-how-it-works so it doesn't reach an unrelated wp:columns row
   elsewhere on the page. */
.gv-how-it-works__step-rule {
	display: block;
	width: 4rem;
	height: 1px;
	margin-top: 1.5rem;
	background: var(--gv-gold-30);
}

.gv-how-it-works .wp-block-column:last-child .gv-how-it-works__step-rule {
	display: none;
}

@media (max-width: 767px) {
	/* .gv-how-it-works__step-rule's own display:none lives in the "Home:
	   mid-width pass" block below, at the same 767px step (T-005/W-10) —
	   nothing to duplicate here.

	   The toggle rail going full-width below the grid's own collapse point
	   is not modelled in the export at any width — HowItWorks carries no
	   `sm:`/responsive class on the toggle at all (T-002 spec §5). It is
	   ours, kept as a tap-target affordance, and consolidated onto the
	   client's one breakpoint (was 600px) so it does not read as a second
	   competing breakpoint system (T-005). */

	.gv-how-it-works__toggle {
		width: 100%;
		max-width: 100%;
	}

	.gv-how-it-works__toggle button {
		flex: 1;
		padding: 0.7rem 0.5rem;
		letter-spacing: 0.1em;
		font-size: 0.68rem;
	}
}

/* W-12 (T-011), gap half: 40px base steps to 32px at 768px — the column
   count already steps at the same breakpoint via the shared "Home:
   mid-width pass" block below.

   Review fix (T-014 finding 4): the `!important` this carried rested on a
   wrong premise — core's columns block does NOT write blockGap as an
   inline style on this markup (patterns/home-how-it-works.php emits a
   bare `<div class="wp-block-columns">`, no `style` attribute at all). It
   writes the gap through a generated container class instead
   (`.wp-container-core-columns-is-layout-*`, specificity 0,1,0), printed
   in an inline `<style id="core-block-supports-inline-css">` in `<head>`
   *before* this stylesheet's own `<link>` loads. `.gv-how-it-works
   .wp-block-columns` is 0,2,0 - already more specific - so it was always
   going to win on the cascade without help. Verified in-browser: removed
   `!important` from both declarations live via the CSSOM and re-read
   `getComputedStyle` at 768px+ - `column-gap`/`row-gap` stayed 32px,
   unchanged. */
@media (min-width: 768px) {
	.gv-how-it-works .wp-block-columns {
		column-gap: 2rem;
		row-gap: 2rem;
	}
}

/* T-011 QA fix (T-015): TierSection's (.gv-tiers) and TwoExperiences'
   (.gv-experience) grids were both declaring blockGap as
   clamp(1rem,2.5vw,2rem) — a single fluid value meant to read as 32px below
   768px and 24px at/above it. Confirmed live via document.styleSheets that
   this is not a specificity loss like W-12/M-9 above: WordPress's gap-support
   code does not honour an arbitrary clamp() string at all here and silently
   falls back to the nearest preset spacing token
   (--wp--preset--spacing--50 = 24px) instead of ever emitting the declared
   clamp() literal, so both grids rendered a flat 24px at every width and
   never stepped. patterns/home-tiers.php and patterns/home-experience.php
   now declare a literal 32px (2rem) base blockGap instead of the clamp(),
   and this steps both down to 24px at 768px+, the same base+explicit-step
   shape already proven correct for W-12/M-9 just above. Verified in-browser
   via getComputedStyle in a same-origin iframe probe at fixed widths: 767px
   reads 32px on both selectors, 768px reads 24px on both, and no other
   section's gap moved. */
@media (min-width: 768px) {
	.gv-tiers,
	.gv-experience {
		column-gap: 1.5rem;
		row-gap: 1.5rem;
	}
}

/* T-033 (B-019 item 1): the >=768px step above was already pinned
   deliberately by T-011/T-015. The BASE (<768px) gap was not — T-023's
   review traced that `.gv-tiers` only reads 32px below 768px because
   `class-goldvion-block-tier-cards.php` reproduces the literal string
   `wp-container-core-columns-is-layout-cca67494`, and that hash-bearing
   rule (`gap:2rem 2rem`) is actually emitted by core's layout-support for
   home-experience.php's real, still-parsed `wp:columns` block
   (`.gv-experience`) — the two only share it because both currently carry
   identical blockGap attrs. Change either section's attrs and the hash
   changes, and `.gv-tiers` silently loses its base gap with nothing to
   say so. `.gv-tiers` is no longer a parsed block at all (T-029 moved its
   markup into a render_callback with no attrs to hash), so it can never
   legitimately earn that generated rule on its own — pin the base value
   here instead, the same base+explicit-step shape already proven for
   W-12/M-9/the >=768px rule above, so `.gv-tiers`'s gap no longer depends
   on what `.gv-experience` happens to declare. Same specificity (0,1,0) as
   the generated container rule; this stylesheet's `<link>` still loads
   after that inline `<style>`, so source order wins exactly as it does for
   M-9. Verified: forcing `.gv-experience`'s blockGap to a different value
   (which changes its generated hash and detaches `.gv-tiers` from it
   entirely) leaves `.gv-tiers`'s computed gap at 32px below 768px,
   unchanged.

   Scoped to `(max-width: 767px)` rather than left unconditional: an
   unconditional same-specificity rule placed after the >=768px step above
   would win at every width by source order and cancel that step for
   `.gv-tiers` — caught by re-testing 768px after adding this. */
@media (max-width: 767px) {
	.gv-tiers {
		column-gap: 2rem;
		row-gap: 2rem;
	}
}

/* --- Home: sections 1-4 ---
   Owned by T-008. T-009 appends its own delimited block below rather than
   editing this one — see state/sprints/S-001/notes/T-007-design.md §3. */

/* Every image slot on the home page ships without photography — see
   T-007-design.md §1. A flat tinted block at the design's aspect ratio,
   never a grey box that could be mistaken for a broken <img>. */
.gv-image-placeholder {
	display: flex;
	align-items: center;
	justify-content: center;
	text-align: center;
	padding: 1.5rem;
	border: 1px dashed var(--gv-fg-30);
	background: var(--wp--preset--color--surface-muted);
}

.gv-image-placeholder--card {
	aspect-ratio: 4 / 5;
}

.gv-image-placeholder--square {
	aspect-ratio: 1;
}

/* Compliance placeholders (requirements.md "Interpretation"). Must never be
   mistaken for approved copy going live by accident — hairline border,
   italic, and a background that reads as "unfinished" rather than styled.
   Text colour is pinned rather than inherited: found during the T-015 review
   fix-up that the footer group carries has-ivory-color, which put the
   Newsletter placeholder at 1.28:1 (ivory on blush) — invisible, and
   site-wide because the footer is a template part. Charcoal-on-blush is
   11.29:1, so pinning it here makes the class safe in any surrounding
   colour context, which is the property it exists for. */
.gv-placeholder {
	background-color: var(--wp--preset--color--blush);
	color: var(--wp--preset--color--charcoal);
	border: 1px dashed var(--wp--preset--color--charcoal);
	padding: 1rem 1.25rem;
	font-style: italic;
	font-size: var(--wp--preset--font-size--small);
}

/* S-007/T-007: the two FAQ rows this used to serve (mystery-gifts, tarnish)
   are restored, approved copy now and no longer wear a placeholder-family
   class -- documentation/compliance-register.md's launch check greps for
   the placeholder family and expects to find only image-pending and
   testimonial-quote, so a class named gv-placeholder--cut surviving on
   approved rows would make that check fail even though nothing is visually
   wrong (T-008 QA, S-007). The spacing itself is real (a kept .gv-faq__a
   answer immediately followed by the restored second sentence, also
   .gv-faq__a, needs a top margin between them) so it moves to a structural
   selector instead of a named class: two .gv-faq__a paragraphs in a row is
   exactly the "kept answer, then a restored clause" shape and occurs
   nowhere else in this pattern. */
.gv-faq__a + .gv-faq__a {
	margin-top: 0.75rem;
}

/* Tier cards and the "What's Inside" modal (T-009) have their own
   delimited block below, between this one and "sections 5-9" — the
   S-001-era .gv-tiers__badge rule that used to live here is retired there
   (B-004: it let the badge grow to fit whatever placeholder string it was
   given, which is the bug). */

/* --- Home: tiers + "What's Inside" modal (T-009) ---
   patterns/home-tiers.php's three tier cards, matching home-reference.html's
   .tiers/.tier, plus its "What's Inside?" modal (the reference's .inside).
   Price and inclusions are read from WooCommerce/Goldvion_Tiers at render
   time — see that pattern's own docblock; nothing here is a data source.

   Cards are real core blocks (columns/group/heading/paragraph/list/
   buttons); wp:columns' own equal-width flex split plus its default
   align-items:stretch reproduces the reference's
   grid-template-columns:repeat(3,1fr) with no override needed, same
   reasoning as .gv-experience above.

   B-004: the Signature badge used to render the full ~100-character
   compliance placeholder sentence inside a pill sized for two words. The
   pill itself never grows (white-space:nowrap, fixed padding) so the
   *string* clipped against its own edges — but the card underneath still
   reserved space for the overflow, which is what threw Signature out of
   vertical alignment with Essential and Royal. Two independent fixes,
   because they're two independent causes: .gv-tier__foot's margin-top:auto
   (also below) holds every card's foot on one baseline regardless of how
   much a card's badge/tagline state pushes its body height around, which
   covers taglines of different lengths as well as the badge and is
   unaffected by anything below; the badge's own overflow risk is now
   closed by a `maxlength` on the admin field instead (T-020 review,
   findings 2/4 — see .gv-tier__badge's own comment below). This note used
   to close the badge half by asserting .gv-tier__badge only ever renders a
   short bracketed marker (patterns/home-tiers.php), which stopped being
   true the moment T-014 made that field admin-editable, and the reasoning
   this note used to reject the alternative on ("MOST POPULAR is two words
   and will fit the pill's real dimensions once approved") only ever held
   while the string was hardcoded — recorded here as history, not as the
   current close. */
/* T-2 (T-011): radius corrected to the export's rounded-lg (4px, the
   `base` token) — scoped to `.gv-tier` only, same shape as X-2/X-3's fix
   on `.gv-xcard`, not a change to the shared `--radius--card` token
   (S0-5 names five call sites total; the others belong to sections
   outside this task). */
.gv-tier {
	position: relative;
	display: flex;
	flex-direction: column;
	height: 100%;
	background: var(--wp--preset--color--ivory);
	border: 1px solid var(--gv-gold-20);
	border-radius: var(--wp--custom--radius--base);
	overflow: hidden;
}

.gv-tier--featured {
	border-color: var(--wp--preset--color--gold);
	background: color-mix(in srgb, var(--wp--preset--color--blush) 10%, var(--wp--preset--color--ivory));
}

/* S-004/T-018 rework: aspect-ratio: 4/3 lived only on the compound below,
   so Essential and Signature lost their box the moment
   Goldvion_Block_Tier_Cards stopped emitting the placeholder class for
   them (Royal keeps it under ADR-021 and is unaffected). Moved to the base
   selector, same shape as .gv-inside__media, so a real photo gets the same
   box the placeholder always had — the three cards take the same media
   height again regardless of source.

   T-020 QA (sent back to T-018 a second time): the ratio alone was not
   enough. .gv-tier is a column flexbox, and .gv-tier__media is a flex
   item with no explicit width — the aspect-ratio trap this repo's own
   notes already document ("aspect-ratio is a preference, not a
   constraint"). Essential's photo happened to have a WP-generated
   488x488 intermediate at the tested viewport, and that intrinsic size
   won over the flex stretch, producing a 488x366 box against a 366px
   card — 33% oversized, clipped invisibly by .gv-tier's own
   overflow:hidden so no rendering glitch gave it away. width: 100% pins
   the box to its container the same way .gv-hero__media/.gv-xcard__media
   already get for free from position:absolute;inset:0; overflow: hidden
   is the belt-and-braces half of the same documented fix, in case a
   future photo's intrinsic size is even larger than the box. */
.gv-tier__media {
	aspect-ratio: 4 / 3;
	width: 100%;
	overflow: hidden;
}

/* T-015: found while measuring the featured badge against its own card and
   its siblings (B-045). WP core's block-gap CSS zeroes a constrained
   layout's gap only on the DOM's actual :first-child
   (":root :where(.is-layout-constrained) > :first-child { margin-block-start: 0 }",
   specificity 0,2,0) and gives every other child 1.2rem
   (":root :where(.is-layout-constrained) > * { margin-block: 1.2rem 0 }",
   0,1,0) — the same block-gap mechanism "blockGap silently ignores
   clamp()" above documents, in a new shape. .gv-tier__badge renders before
   .gv-tier__media in markup so its own card reads correctly on top of the
   photo, but that makes .gv-tier__media the *second* child whenever a
   badge is present, so it lost the :first-child exemption and picked up
   the generic 1.2rem margin-block-start instead — 19.2px measured on the
   featured card at both 390px and 1440px, present on no other tier card.
   Invisible at the badge's own element (it is position:absolute and never
   moves), and invisible in a tight zoom on the photo; visible only as a
   blank strip of card background above the photo on whichever tier is
   featured, and as that card's box (and everything below it) sitting
   19.2px lower than its own aspect-ratio box alone would predict.

   Fix: pin the margin to 0 unconditionally — .gv-tier__media should always
   sit flush at the top of its card regardless of whether a badge precedes
   it in the DOM; the badge is a self-positioned overlay and was never
   meant to reserve flow space. Compound selector (0,2,0) to outrank WP's
   generic 0,1,0 rule outright — no source-order tie-break needed here,
   this selector wins on specificity alone.

   T-020 review, finding 6: this used to cite ".gv-tier .gv-tier__foot's
   margin-top:auto fix above" as the precedent for this shape, and no such
   compound selector exists — the actual foot fix is the bare
   `.gv-tier__foot { margin-top: auto }` below (0,1,0), which wins its own
   tie against WP's generic rule by sitting later in source order, not by
   outranking it on specificity. This rule genuinely is 0,2,0 and genuinely
   wins outright regardless of order, so the fix itself was always right;
   only the citation was wrong, worth correcting because the next reader
   could otherwise go looking for a compound selector that isn't there, or
   copy "wins like the foot" into a place where the source-order dependency
   actually does matter. Confirmed by removing the badge from
   the featured tier and by moving it to a different tier: the fix holds
   the media box flush in every case, not just the one it was found on. */
.gv-tier .gv-tier__media {
	margin-block-start: 0;
}

/* Compound selector, same cascade-order reason as
   .gv-xcard__media.gv-image-placeholder above: .gv-image-placeholder's own
   border/background rule is the same specificity and sits earlier in this
   file, so a bare .gv-tier__media rule would lose the fight for border. */
/* T-5 (T-011): the export has no divider under the card image. */
.gv-tier__media.gv-image-placeholder {
	border: 0;
	border-radius: 0;
}

/* S-004/T-018: object-fit: cover does nothing without a definite box to
   cover — .gv-tier__media above is that box now (aspect-ratio: 4/3). */
.gv-tier__media img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.gv-tier__body {
	padding: 2rem;
	display: flex;
	flex-direction: column;
	flex: 1;
}

.gv-tier__spec {
	list-style: none;
	margin: 1.25rem 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	color: var(--gv-fg-60);
	font-size: var(--wp--preset--font-size--x-small);
}

.gv-tier__spec li {
	display: flex;
	align-items: flex-start;
	gap: 0.5rem;
}

/* T-7 (T-011): tick offset corrected to the export's mt-0.5 (2px). */
.gv-tier__spec svg {
	width: 0.75rem;
	height: 0.75rem;
	flex: none;
	margin-top: 0.125rem;
	color: var(--wp--preset--color--gold);
}

/* Pushes price/CTA/"What's Inside?" to the bottom of the card regardless of
   how long a given tier's badge/tagline/spec list runs, so all three
   cards' feet land on one baseline — the acceptance criterion this section
   has to hold ("three cards are vertically aligned"), and the other half
   of the B-004 fix above. The reference's own .tier__foot uses a fixed
   margin-top:1.5rem instead, because its cards never differ enough in
   content length to need it; ours can, so auto replaces the fixed value
   deliberately. T-9 (T-011): the export's foot carries no padding-top at
   all — ours used to add 1.5rem on top of the auto margin, which is a
   real, closeable visual delta distinct from the auto-vs-fixed choice
   above (that choice is out of scope, this padding never was). */
.gv-tier__foot {
	margin-top: auto;
	padding-top: 0;
	display: flex;
	flex-direction: column;
	gap: 0.75rem;
}

.gv-tier__foot .wp-block-button__link {
	display: block;
	width: 100%;
	text-align: center;
}

.gv-tier__inside {
	display: inline-block;
	background: none;
	border: 0;
	cursor: pointer;
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--micro);
	text-transform: uppercase;
	letter-spacing: 0.32em;
	color: var(--wp--preset--color--muted-faint);
	text-underline-offset: 4px;
	text-align: left;
}

.gv-tier__inside:hover {
	color: var(--wp--preset--color--gold);
	text-decoration: underline;
}

/* Focus lands on the checkbox, not the (tabindex="-1", aria-hidden)
   trigger label — same redirect trick as .gv-drawer-toggle:focus-visible
   above, and for the same reason: the checkbox is the thing a keyboard
   user actually tabs to, so its visible focus ring belongs on the label
   that reads as the control, not on the 1px clipped input itself. */
.gv-inside-toggle:focus-visible ~ .gv-tier__inside {
	outline: 2px solid var(--wp--preset--color--charcoal);
	outline-offset: 2px;
}

/* T-020 review, findings 2 and 4 — both about this rule, closed together
   because the naive fix for either one reintroduces the other.

   THIS USED TO SAY the badge only ever renders a short bracketed marker
   (patterns/home-tiers.php), so there was no long string to overflow in
   the first place. That stopped being true the moment T-014 made the
   badge text an admin-editable field (_gv_featured_badge_text) instead of
   a hardcoded marker — an admin can now type a string of any length in,
   and this pill (fixed padding, no wrap) does not grow to fit it. That is
   B-004 again, through the door T-014 opened: B-004 was exactly this
   shape, and the rejected alternative to the B-004 fix ("MOST POPULAR is
   two words and will fit the pill's real dimensions once approved") only
   held while the string was hardcoded. The actual close for the
   overflow risk is
   class-goldvion-tiers.php's `maxlength` on that field, sized to what
   this pill can hold (see that file's own comment on the number) — not
   anything in this rule, which cannot know the string's length at all.

   THIS RULE ALSO USED TO BE THE COMPOUND SELECTOR
   `.gv-tier__badge.gv-placeholder`, with every one of the pill's styles —
   including position: absolute — living only on that compound and no
   `.gv-tier__badge` rule existing at all. That was fine while the badge
   could only ever hold placeholder text, so the two classes were never
   apart. T-014 made the text real some of the time, and
   class-goldvion-block-tier-cards.php now applies `gv-placeholder` only
   when the text is still the compliance default — same conditional
   pattern as `$gv_has_real_tagline` beside it. A compound selector would
   have meant the pill (including position: absolute) simply disappearing
   the day the client's approved copy goes in, dropping the badge into
   flow at the top of the card — a different bug wearing the same fix.
   Moved to the bare `.gv-tier__badge` below so the geometry applies
   regardless of which text it holds; `gv-placeholder`'s own generic look
   (dashed border, italic — see that rule above) does not need to survive
   on top of it, since this pill has never used that look for either kind
   of text, before or after this change. */
/* T-4 (T-011): top/pad/size corrected to the export's top-3/4px-16px/8px.
   No preset font-size exists at 8px (`micro` is the smallest, at 10px), so
   this is a literal value — safe here since it is set through a plain
   classname rule, never a block attribute, so there is no .has-*-color-
   style trap to fall into. */
.gv-tier__badge {
	position: absolute;
	top: 0.75rem;
	left: 50%;
	transform: translateX(-50%);
	z-index: 2;
	display: inline-block;
	background-color: var(--wp--preset--color--charcoal);
	color: var(--wp--preset--color--ivory);
	border: 0;
	border-radius: var(--wp--custom--radius--pill);
	padding: 0.25rem 1rem;
	font-size: 0.5rem;
	font-style: normal;
	text-transform: uppercase;
	letter-spacing: 0.32em;
	white-space: nowrap;
}

/* "What's Inside" modal — home-reference.html's .inside, one self-contained
   instance per tier rather than the reference's single shared overlay.
   No-JS contract, same shape as the header drawer below: the only real
   control is #gv-inside-toggle-<slug>, a visually-hidden checkbox (added to
   the shared clip rule on .gv-drawer-toggle below) living inside its own
   tier's .gv-tier__foot. Its trigger ("What's Inside?"), backdrop and close
   glyph are all decorative <label for="...">s pointing at that checkbox, so
   a mouse click on any of them — or focusing the checkbox directly and
   pressing Space — opens or closes the panel with zero script, verified
   with JavaScript disabled. Deliberately nested as the checkbox's own
   sibling rather than collected in one place at the end of the pattern:
   this is what puts an opened panel's real content (the "Explore <Tier>"
   link) in keyboard tab order immediately after the checkbox that revealed
   it, instead of somewhere unrelated later in the document.
   assets/js/tiers.js is additive on top of that baseline — Escape closes
   whichever panel is open, Tab is trapped inside the open panel, and focus
   returns to the checkbox (the trigger's own position) on close — none of
   which the reference itself implements and all of which are acceptance
   criteria here. A script load failure degrades to "no Escape, no trap, no
   focus return", never to "the panel's content is unreachable" — every
   compliance placeholder inside it (the tagline reuse, see the pattern's
   PHP) renders exactly the same with or without the script. */
.gv-inside__scrim {
	position: fixed;
	inset: 0;
	z-index: 65;
	display: block;
	background: var(--gv-fg-50);
	backdrop-filter: blur(4px);
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
	transition: opacity 0.3s, visibility 0s linear 0.3s;
}

.gv-inside-toggle:checked ~ .gv-inside__scrim {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transition: opacity 0.3s, visibility 0s linear 0s;
}

/* Centred via inset:0 + margin:auto rather than a flex wrapper, so the
   panel can stay a flat sibling of its own checkbox/scrim/trigger instead
   of needing an extra centring element around it.
   T-11 (T-011): radius corrected to the export's rounded-lg (4px, `base`),
   same scoped-not-shared-token reasoning as .gv-tier above.
   T-14 (T-011): shadow alpha corrected to the export's shadow-2xl (0.25,
   ours was 0.35) — the offsets/blur already matched.
   T-011 (S-005): same latent bug as `.gv-charm-modal` in builder.css --
   `inset: 0` + `height: auto` solves to the viewport, not the content,
   until `max-height: 88vh` clamps it. Didn't show before because this
   panel's content (752px) was close to the 792px clamp; a shorter tier
   inclusions list would have exposed it. `height: fit-content` added.
   T-011 (S-005), second bug found while re-measuring the first: selector
   scoped to `.gv-tier .gv-inside__panel` (0,2,0) rather than the bare
   class (0,1,0) -- WordPress's generated
   `.is-layout-constrained > :where(...)` rule is also 0,1,0 and declares
   its own `max-width` (the theme's content-size, 1024px here), and it
   loses the tie only by source order; global-styles is printed late and
   was winning, so this panel was rendering at 1024px wide instead of the
   32rem (512px) declared here -- same family as the `:where()` block-gap
   trap and the `margin-top: auto` trap already recorded in
   state/repos/goldvion.md. The width miss was hiding behind the height
   bug: at the wrong width, `.gv-inside__media`'s `aspect-ratio: 3/2`
   derived a much taller box, which is what the content-height numbers
   above were measuring before this was caught. */
.gv-tier .gv-inside__panel {
	position: fixed;
	inset: 0;
	z-index: 66;
	margin: auto;
	width: calc(100% - 2rem);
	max-width: 32rem;
	height: fit-content;
	max-height: 88vh;
	overflow-y: auto;
	overflow-x: hidden;
	background: var(--wp--preset--color--ivory);
	border-radius: var(--wp--custom--radius--base);
	box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
	visibility: hidden;
	opacity: 0;
	transform: translateY(20px) scale(0.95);
	transition: opacity 0.3s, transform 0.3s, visibility 0s linear 0.3s;
}

.gv-inside-toggle:checked ~ .gv-inside__panel {
	visibility: visible;
	opacity: 1;
	transform: none;
	transition: opacity 0.3s, transform 0.3s, visibility 0s linear 0s;
}

.gv-inside__media {
	position: relative;
	aspect-ratio: 3 / 2;
}

.gv-inside__media.gv-image-placeholder {
	border: 0;
	border-radius: 0;
}

/* S-004/T-018: object-fit: cover does nothing without a definite box to
   cover — .gv-inside__media above already is one (aspect-ratio: 3/2 on
   the base selector, the shape findings 1 and 2 needed copying). This
   rule was simply missing. */
.gv-inside__media img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* T-13 (T-011): the export has no filled circle behind the close glyph —
   a bare X at ivory/80, hover ivory. The svg itself was bumped from 14px
   to the export's 20px (h-5 w-5) in the pattern markup.

   Review fix (T-014 finding 7): the box was sized to the glyph itself
   (1.25rem = 20px), under the WCAG 2.5.8 24px minimum pointer target —
   and this label is the only pointer affordance for closing the modal
   besides the scrim. Grown to 1.5rem (24px) via padding around the same
   20px glyph, not by enlarging the glyph; top/right shifted by the same
   0.125rem so the glyph's own visual centre — and its position relative
   to the modal corner — is unchanged. */
.gv-inside__close {
	position: absolute;
	top: 0.875rem;
	right: 0.875rem;
	width: 1.5rem;
	height: 1.5rem;
	padding: 0;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	background: none;
	color: var(--gv-ivory-80);
	cursor: pointer;
}

.gv-inside__close:hover {
	color: var(--wp--preset--color--ivory);
}

/* Only reachable once assets/js/tiers.js upgrades the close glyph from its
   static tabindex="-1" to a real tab stop — see that file's own comment. */
.gv-inside__close:focus-visible {
	outline: 2px solid var(--wp--preset--color--ivory);
	outline-offset: 2px;
}

/* T-12 (T-011): 32px base steps to 40px at the export's own 768px (p-8
   md:p-10) — the one place this modal's own contract carries a
   responsive step at all. */
.gv-inside__body {
	padding: 2rem;
}

@media (min-width: 768px) {
	.gv-inside__body {
		padding: 2.5rem;
	}
}

.gv-inside__body h3 {
	font-size: var(--wp--preset--font-size--xx-large);
}

/* Placeholder state (Signature) is a compound selector for the same
   cascade-order reason as .gv-tier__media.gv-image-placeholder above —
   plain .gv-inside__desc sits later in this same file than .gv-placeholder,
   so without the compound it would win the tie and silently override the
   placeholder's own colour/padding/background instead of the other way
   round. (T-020 review, finding 6 — this used to cite
   .gv-tier__badge.gv-placeholder for the same reasoning; that rule is no
   longer a compound selector, see its own comment above, so the citation
   moved to a precedent that still exists.) */
.gv-inside__desc {
	margin-top: 0.75rem;
}

.gv-inside__desc:not(.gv-placeholder) {
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.7;
	color: var(--gv-fg-60);
}

.gv-inside__label {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--micro);
	text-transform: uppercase;
	letter-spacing: 0.32em;
	color: var(--wp--preset--color--muted-faint);
	margin: 1.5rem 0 0.75rem;
	font-weight: 400;
}

.gv-inside__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.625rem;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--muted);
}

.gv-inside__list li {
	display: flex;
	align-items: center;
	gap: 0.75rem;
}

.gv-inside__dot {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: 1.25rem;
	height: 1.25rem;
	border-radius: var(--wp--custom--radius--pill);
	background: var(--gv-gold-20);
	color: var(--wp--preset--color--gold);
}

.gv-inside__dot svg {
	width: 0.75rem;
	height: 0.75rem;
}

.gv-inside__pack {
	font-size: var(--wp--preset--font-size--small);
	color: var(--gv-fg-60);
}

.gv-inside__foot {
	margin-top: 2rem;
	padding-top: 1.5rem;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	flex-wrap: wrap;
	border-top: 1px solid var(--gv-gold-15);
}

.gv-inside__price {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--xx-large);
	color: var(--wp--preset--color--charcoal);
}

.gv-inside__charms {
	margin-left: 0.5rem;
	font-size: var(--wp--preset--font-size--micro);
	text-transform: uppercase;
	letter-spacing: 0.32em;
	color: var(--wp--preset--color--muted-faint);
}

/* --- Home: sections 5-9 ---
   Owned by T-009. Appended below T-008's block per
   state/sprints/S-001/notes/T-007-design.md §3 — that block is untouched. */

/* --- Home: testimonials + Instagram (T-010) ---
   patterns/home-testimonials.php and patterns/home-instagram.php are raw
   wp:html, array-driven repeating units, same shape and same reasoning as
   T-006's hero — see both patterns' own docblocks for why (carousel/
   scroll-snap behaviour core blocks have no primitive for). Sections 5 and
   9 (either side of this pair) stay real core blocks; this pair's own
   docblocks record that as a stated handover limitation, not an oversight.
   Retires the old .gv-testimonial-card / .gv-instagram-strip rules below,
   written for wp:columns-based markup this task replaces. */

/* Testimonials: one card at a time inside the 768px measure, arrows
   outside the card on desktop, one dot per card beneath. Progressive
   enhancement (ADR-004), same additive-class shape as .gv-hero above: the
   rules immediately below (no class guard) stack every card in normal
   document flow with a hairline divider between them, arrows and dots
   hidden — a script that fails to load degrades to "every testimonial
   readable, one after another", never to a hidden compliance placeholder.
   assets/js/testimonials.js (enqueued, defer) adds `gv-testimonials--js`
   to #testimonials once it has run; only rules scoped under that class
   turn the stack into the reference's actual one-at-a-time carousel.
   Verified with JavaScript disabled in the browser. */
/* S0-1 (T-012): padding now reads the shared `--gv-section-pad-y` step
   (96px -> 128px at >=768px) instead of the old continuous clamp(). */
.gv-testimonials {
	padding-block: var(--gv-section-pad-y);
	padding-inline: var(--wp--preset--spacing--gutter);
}

.gv-testimonials__inner {
	max-width: 48rem;
	margin-inline: auto;
}

/* R-11 (T-012): this section has no separate `.gv-section-head` bottom
   margin in the export — just the eyebrow's own `mb-3` (already the shared
   12px default) and a flat `mb-12` (48px) on the h2 itself. The shared
   `.gv-section-head { margin-bottom: clamp(2.5rem,5vw,4rem) }` (40-64px)
   is scoped out here so it doesn't stack with this section's own value. */
.gv-testimonials .gv-section-head {
	margin-bottom: 0;
}

.gv-section-head__title {
	font-size: var(--wp--preset--font-size--section-title);
}

.gv-testimonials .gv-section-head__title {
	margin-bottom: 3rem;
}

.gv-testimonials__track {
	position: relative;
}

.gv-testimonial {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	padding-block: 2.5rem;
}

.gv-testimonial:first-child {
	padding-top: 0;
}

.gv-testimonial:last-child {
	padding-bottom: 0;
}

.gv-testimonial + .gv-testimonial {
	border-top: 1px solid var(--wp--preset--color--hairline);
}

/* R-6/S0-5 (T-012): radius corrected to the export's rounded-lg (4px, the
   `base` token) — one of S0-5's five call sites, scoped to this component
   only, same shape as X-2/T-2's fixes on .gv-xcard/.gv-tier. */
.gv-testimonial__photo {
	width: 100%;
	max-width: 20rem;
	height: 10rem;
	margin-bottom: 1.25rem;
	border-radius: var(--wp--custom--radius--base);
	overflow: hidden;
}

/* T-032 / B-018: a `verified` row with a real photo renders an actual
   <img> here (see class-goldvion-block-testimonials.php) rather than the
   URL-as-text this container used to receive unconditionally — same
   edge-to-edge fit as .gv-ig__tile img above. */
.gv-testimonial__photo img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Rating: no star fills gold by default, unlike the reference. Rows without
   verified provenance (ADR-016) make no claim about anyone, so their five
   stars stay uniformly greyed here, plus aria-label="Rating pending" for
   anyone not seeing the colour at all. See home-testimonials.php's docblock
   and ADR-016.
   T-032 / B-018: a `verified` row carries a real `_gv_t_rating`, and
   `editable-content-model.md` promises "gold stars at META_RATING" for it —
   `.gv-testimonial__stars--rated` (applied only on that branch, in
   class-goldvion-block-testimonials.php) and `svg.is-filled` below are the
   two hooks that turn a real rating gold without touching the default,
   still-grey styling every row without verified provenance keeps. */
.gv-testimonial__stars {
	display: flex;
	gap: 0.25rem;
	margin-bottom: 1rem;
	color: var(--gv-fg-30);
}

.gv-testimonial__stars svg {
	width: 1rem;
	height: 1rem;
	fill: currentColor;
}

.gv-testimonial__stars--rated svg.is-filled {
	color: var(--wp--preset--color--gold);
}

/* R-8 (T-012): .gv-placeholder already sets the compliance-placeholder
   colour, border and background (see "Home: sections 1-4" above) — colour
   is deliberately NOT overridden to the export's charcoal 70% here
   (ADR-011 §10 item 1, same reasoning as M-5/M-6: the placeholder's own
   contrast treatment stays until real copy lands). What this layers is
   type only: Cormorant italic, the export's 24px -> 30px step at >=768px
   (was a continuous clamp() that never actually reached either exact
   value), leading-relaxed (1.625, was 1.5), and no measure cap (the
   48rem `.gv-testimonials__inner` is already the cap the export relies on
   — the old 36rem was ours, narrower than the export's own layout). */
.gv-testimonial__text.gv-placeholder {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large); /* 24px base */
	line-height: 1.625;
}

@media (min-width: 768px) {
	.gv-testimonial__text.gv-placeholder {
		font-size: var(--wp--preset--font-size--xx-large); /* 30px at >=768px */
	}
}

.gv-testimonial__who {
	margin-top: 1.5rem;
}

.gv-testimonial__who-line {
	display: inline-block;
}

/* R-10: mt-1 (4px), was 0.5rem (8px). */
.gv-testimonial__tier {
	margin: 0.25rem 0 0;
	font-size: var(--wp--preset--font-size--micro);
	text-transform: uppercase;
	letter-spacing: 0.22em;
	color: var(--wp--preset--color--gold);
}

/* R-5 (T-012): text-glyph arrows replaced with lucide ChevronLeft/Right at
   20px, strokeWidth 1.5 — the font-size/line-height glyph sizing is gone,
   replaced by an explicit svg size, same shape as .gv-hero__arrow. */
.gv-testimonials__nav {
	display: none;
	position: absolute;
	top: 50%;
	transform: translateY(-50%);
	background: none;
	border: 0;
	padding: 0.25rem;
	cursor: pointer;
	color: var(--gv-fg-30);
	line-height: 0;
	transition: color 0.2s;
}

.gv-testimonials__nav svg {
	width: 20px;
	height: 20px;
	display: block;
}

.gv-testimonials__nav:hover {
	color: var(--wp--preset--color--charcoal);
}

.gv-testimonials__nav--prev {
	left: 0;
}

.gv-testimonials__nav--next {
	right: 0;
}

@media (min-width: 768px) {
	.gv-testimonials__nav--prev {
		left: -2rem;
	}

	.gv-testimonials__nav--next {
		right: -2rem;
	}
}

.gv-testimonials__dots {
	display: none;
	justify-content: center;
	gap: 0.5rem;
	margin-top: 2rem;
}

/* R-3 (T-012): geometry now 6px -> 24px over 500ms (was a fixed 6px
   square with no width transition at all), inactive palegold 30% (was
   charcoal 20%) — same `width`/`background` transition-property pair as
   the hero's own dots (H-14), just the export's own narrower 6px->24px
   range and palegold instead of charcoal throughout. */
.gv-testimonials__dots button {
	width: 0.375rem;
	height: 0.375rem;
	padding: 0;
	border: 0;
	border-radius: var(--wp--custom--radius--pill);
	cursor: pointer;
	background: var(--gv-gold-30);
	transition: width 0.5s, background 0.5s;
}

.gv-testimonials__dots button[aria-pressed="true"] {
	width: 1.5rem;
	background: var(--wp--preset--color--gold);
}

/* Carousel mode — added by assets/js/testimonials.js once it has run.
   Everything above this rule is the no-JS baseline and stays true with the
   class present too; this only layers one-card-visible plus the two
   controls on top of it. */
.gv-testimonials--js .gv-testimonial {
	display: none;
	padding-block: 0;
	border-top: 0;
}

/* R-1 (T-012): the export crossfades the swap with a rise/fall
   (`y:20 -> 0` in, `y:-20` out) over 0.4s, `mode="wait"` — sequential, not
   simultaneous, same reasoning as W-13's how-it-works panel fade. A
   testimonial that is `[data-active="true"]` and carries neither modifier
   class is simply at rest, visible, opacity 1, translateY(0) — exactly
   what happens if this script fails to load (progressive enhancement,
   matching the file's other scripts). Skipped under prefers-reduced-motion
   by the script itself.

   Review fix (T-014 finding 1): this used to be two separate
   `[data-active="true"]` rules (now merged below) at (0,3,0), with the
   `--exiting`/`--entering` modifiers as bare classes at (0,2,0) — an
   attribute selector counts at class level, so the resting-state rule
   always outranked the modifiers outright and the crossfade never ran.
   testimonials.js deliberately keeps `data-active="true"` on the element
   through both the exit phase (still-active outgoing card) and the enter
   phase (`setActive()` runs before the `--entering` class is added), so
   the fix qualifies both modifiers with `[data-active="true"]` too,
   putting them at (0,4,0) — one level above the resting rule they need to
   override, regardless of source order. The dead `display: flex` that
   used to sit on `--exiting` is dropped: the element already has
   `display: flex` from the (still true) `[data-active="true"]` resting
   rule, and the combination `--exiting` without `[data-active="true"]`
   never occurs (the JS always removes `--exiting` before flipping
   `data-active`). Verified with a real double-click, instrumented via
   `getComputedStyle` polled every ~50ms across the 400ms window (the
   automated tab throttles timers and reports `document.hidden`, so a
   captured visual frame can't be trusted here) — see testimonials.js's
   own note and the task's closing artefact for the readings. */
.gv-testimonials--js .gv-testimonial[data-active="true"] {
	display: flex;
	opacity: 1;
	transform: translateY(0);
	transition: opacity 0.4s ease, transform 0.4s ease;
}

.gv-testimonials--js .gv-testimonial[data-active="true"].gv-testimonial--exiting {
	opacity: 0;
	transform: translateY(-20px);
}

.gv-testimonials--js .gv-testimonial[data-active="true"].gv-testimonial--entering {
	opacity: 0;
	transform: translateY(20px);
	transition: none;
}

.gv-testimonials--js .gv-testimonials__nav,
.gv-testimonials--js .gv-testimonials__dots {
	display: flex;
}

/* Instagram: a horizontal snap-scroller, not a grid — three square tiles
   across on desktop, one and a bit on a phone, scrollbar suppressed. No
   script dependency (native CSS scroll-snap), so there is no --js
   modifier here the way the hero, how-it-works and testimonials have.
   I-1 (T-013): ground is ivory, not band — this section sits directly on
   the page ground in the export, unlike HowItWorks/MaterialSection either
   side of it. S0-1 (T-013): padding now reads the shared
   --gv-section-pad-y step (96px -> 128px at >=768px), same swap T-009/
   T-012 made for the other sections, instead of the old continuous
   clamp(). */
/* T-019: `.gv-ig__inner`'s content-width constraint (64rem, centered, no
   padding of its own) is dropped as a hand-written rule and replaced by
   giving the outer wp:group a real `layout:{"type":"constrained",
   "contentSize":"64rem"}` (home-instagram.php) — the exact native mechanism
   `home-faq.php` and every `.gv-section-head` group in this codebase already
   use for the same job. Core's layout-support styling then clamps and
   centers each of this section's direct children (the section-head, the
   scroller, the buttons) to 64rem independently, which renders identically
   to one shared 64rem wrapper around all three, because all three clamp
   against the same parent width. No extra wrapper element needed, and none
   of `.gv-ig`'s own full-bleed background/padding is affected — a
   `constrained` layout styles a group's CHILDREN, never the group itself. */
.gv-ig {
	background: var(--wp--preset--color--ivory);
	padding-block: var(--gv-section-pad-y);
	padding-inline: var(--wp--preset--spacing--gutter);
}

.gv-ig__scroller {
	display: flex;
	gap: 1rem;
	overflow-x: auto;
	padding-bottom: 1rem;
	scroll-snap-type: x mandatory;
	scrollbar-width: none;
	-ms-overflow-style: none;
}

.gv-ig__scroller::-webkit-scrollbar {
	display: none;
}

/* S-008 (ADR-043): the arrows' positioning context. Carries the scroller's
   old `margin-top: 3rem` so the section's rhythm is unchanged — the wrapper
   is new, the spacing is not. */
.gv-ig__viewport {
	position: relative;
	margin-top: 3rem;
}

/* I-4/S0-5 (T-013): radius corrected to the export's rounded-lg (4px, the
   `base` token) — same one-line, per-component fix shape as X-2/T-2/R-6
   rather than the global token change S0-5 explicitly warns against. */
.gv-ig__tile {
	position: relative;
	flex: none;
	width: 70%;
	/* S-008 (ADR-043): 9:16, not the export's square. Reels are portrait, and
	   a square box letterboxes or centre-crops every one of them. This
	   supersedes ADR-020 §3's square ruling for the tile box only — the
	   `aspect-ratio` on the <img> below is still what B-019 item 4 needed, it
	   just now describes a portrait box. */
	aspect-ratio: 9 / 16;
	scroll-snap-align: start;
	border-radius: var(--wp--custom--radius--base);
	overflow: hidden;
	text-decoration: none;
}

@media (min-width: 640px) {
	.gv-ig__tile {
		width: calc(38% - 0.5rem);
	}
}

@media (min-width: 1024px) {
	/* Four across, as the reference site shows. Portrait tiles are narrower
	   than the export's squares, so three would leave the row short. */
	.gv-ig__tile {
		width: calc(25% - 0.75rem);
	}
}

/* T-019: each tile is now a real core/image, not a div carrying its own
   placeholder text — `font-size`/`padding` above (sized for that text) are
   dropped, and the <img> fills the tile edge-to-edge so the shared
   placeholder PNG (which already reads "IMAGE PENDING") is what shows,
   matching a real photo's eventual fit exactly.
   B-019 item 4 / ADR-020 §3 (T-015): `height: 100%` never resolved — S-003/
   T-024 measured the <img> rendering 411.75px tall inside a 329.3px clipped
   <figure>, so object-fit had no definite height to crop against and
   top-anchored the image instead of centring the crop. Invisible only
   because the placeholder is a flat colour; real photography is exactly
   what makes it visible, which is why it is fixed here rather than
   discovered later as T-018's photos land. The number: the tile is square
   (.gv-ig__tile already declares aspect-ratio:1 above) and the image now
   gets its own definite box the same way, so object-fit: cover has
   something to resolve against. */
.gv-ig__tile img {
	display: block;
	width: 100%;
	height: auto; /* was height: 100% — never resolved; B-019 item 4 */
	aspect-ratio: 9 / 16; /* the definite box object-fit crops against */
	object-fit: cover;
}

/* core/image always emits a <figcaption> when the block's caption attribute
   is non-empty (editable-content-model.md §4's "caption, link and alt at
   minimum"). The export has no visible per-tile caption text, so the
   editable value stays available to the client without appearing as an
   on-image label — same visually-hidden-but-reachable treatment screen
   readers already get from every other caption on this site. */
.gv-ig__tile figcaption {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* I-5 (T-013): hover-only charcoal wash + centred Instagram glyph. Moved
   from a decorative inner `<span>` (the pre-T-019 `<a>`-based tile) to a
   `::after` pseudo-element per documentation/editable-content-model.md §4 —
   core/image's save() has no slot for an extra decorative child, and a
   pseudo-element keeps it out of the accessibility tree with no `aria-hidden`
   needed. The image-zoom half of the export's hover state
   (`group-hover:scale-110`) is deferred with photography (ADR-011 §10 item
   7); this is the non-image half, unchanged from T-013. `:focus-visible`
   gets the same treatment as `:hover` so a keyboard user tabbing through the
   strip sees the same affordance a mouse user does. */
.gv-ig__tile::after {
	content: "";
	position: absolute;
	inset: 0;
	display: flex;
	background: transparent;
	opacity: 0;
	/* T-015 (human gate on T-016): this overlay sits on the <figure>, above
	   the <a> inside it, so with no pointer-events rule it silently
	   intercepted every click on a linked tile — same defect class T-008
	   found the same day in .gv-charm__state (builder.css), also absolute/
	   inset:0/no pointer-events. `none` here lets clicks fall through to the
	   anchor; the hover/:focus-visible affordance below is opacity/
	   background-color only, so it is unaffected by not receiving pointer
	   events itself. */
	pointer-events: none;
	transition: background-color 0.3s ease, opacity 0.3s ease;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23F9F7F2' stroke-width='1.5'%3E%3Crect x='2' y='2' width='20' height='20' rx='5'/%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Ccircle cx='17.5' cy='6.5' r='1.1' fill='%23F9F7F2' stroke='none'/%3E%3C/svg%3E");
	background-repeat: no-repeat;
	background-position: center;
}

/* T-033 (B-019 item 3): T-019's core/image conversion moved the focusable
   element from this .gv-ig__tile (then the <a>) to the <a> INSIDE the
   figure that now carries this class — a <figure> is not focusable, so
   `.gv-ig__tile:focus-visible` could never match and the keyboard
   affordance silently stopped firing, even though the comment above still
   claimed parity with :hover. `:has(a:focus-visible)` reaches back up from
   the inner link to this ::after's own host, restoring the same visual for
   a keyboard user tabbing through the strip. */
.gv-ig__tile:hover::after,
.gv-ig__tile:has(a:focus-visible)::after {
	background-color: var(--gv-fg-30);
	opacity: 1;
}

/* T-012 (B-047): .gv-ig__cta is a core/buttons block, which core's layout
   support always renders as `display: flex` (confirmed via getComputedStyle
   - `is-layout-flex`/`wp-block-buttons-is-layout-flex` are injected by
   WordPress at render time regardless of what's written in this pattern's
   markup, because core/buttons declares layout support with a `{"type":
   "flex"}` default even with no explicit `layout` attribute). `text-align:
   center` is a no-op on a flex container's main-axis position - only
   `justify-content` moves a flex item along it - so the single button (a
   flex item, `display: inline-block`, its own width) sat flush against the
   container's start edge instead of centred. Measured before the fix at
   1864px viewport: button centre x=581.78, container (.gv-ig__cta) centre
   x=927 - 345px off. This is a container-level cause, same family as the
   `.wp-block-button__link`'s inline-flex/align-items below, which centres
   only the button's own contents and was never the mechanism here. */
/* --- S-008 (ADR-043): arrows, reel badge and the reel player ------------ */

/* Hidden until instagram.js reveals them, the same contract as the hero's
   controls: no-JS must never be shown a button that does nothing. The block
   ships them with the `hidden` attribute; this only styles them. */
.gv-ig__arrow {
	position: absolute;
	top: calc(50% - 0.5rem);
	transform: translateY(-50%);
	z-index: 3;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	border: 1px solid var(--wp--preset--color--hairline);
	border-radius: var(--wp--custom--radius--pill);
	background: var(--gv-ivory-90);
	backdrop-filter: blur(8px);
	color: var(--wp--preset--color--charcoal);
	cursor: pointer;
	transition: background 0.3s ease, opacity 0.3s ease;
}

.gv-ig__arrow[hidden] {
	display: none;
}

.gv-ig__arrow:hover {
	background: var(--wp--preset--color--ivory);
}

.gv-ig__arrow--prev {
	left: -0.5rem;
}

.gv-ig__arrow--next {
	right: -0.5rem;
}

@media (min-width: 1024px) {
	.gv-ig__arrow--prev {
		left: -1.25rem;
	}

	.gv-ig__arrow--next {
		right: -1.25rem;
	}
}

/* The reel glyph, mirroring what the reference site puts on a video tile.
   Sits above the hover wash (::after, z-index-less but later in paint order)
   and is pointer-events:none for the same reason that wash is — the click
   has to reach the anchor or the play button underneath. */
.gv-ig__reel {
	position: absolute;
	top: 0.75rem;
	right: 0.75rem;
	z-index: 2;
	display: flex;
	color: var(--wp--preset--color--ivory);
	filter: drop-shadow(0 1px 3px rgb(0 0 0 / 35%));
	pointer-events: none;
}

/* Only exists when a reel tile has no link of its own to hang the
   interaction on — instagram.js injects it so the tile stays keyboard
   reachable rather than putting a click handler on the <figure>. */
.gv-ig__play {
	position: absolute;
	inset: 0;
	z-index: 2;
	border: 0;
	padding: 0;
	background: none;
	cursor: pointer;
}

.gv-ig--js .gv-ig__tile--reel a,
.gv-ig--js .gv-ig__play {
	cursor: pointer;
}

/* --- the player -------------------------------------------------------- */

.gv-ig__player {
	position: fixed;
	inset: 0;
	z-index: 100;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--wp--preset--spacing--gutter);
	background: rgb(0 0 0 / 78%);
}

.gv-ig__player[hidden] {
	display: none;
}

.gv-ig__player-frame {
	position: relative;
	width: min(26rem, 100%);
	max-height: 100%;
	aspect-ratio: 9 / 16;
	border-radius: var(--wp--custom--radius--base);
	overflow: hidden;
	background: #000;
}

.gv-ig__player-video {
	width: 100%;
	height: 100%;
	object-fit: contain;
	display: block;
}

.gv-ig__player-close {
	position: absolute;
	top: 0.5rem;
	right: 0.5rem;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	border: 0;
	border-radius: var(--wp--custom--radius--pill);
	background: rgb(0 0 0 / 45%);
	color: #fff;
	cursor: pointer;
}

.gv-ig__player-close:hover {
	background: rgb(0 0 0 / 70%);
}

/* Stop the page scrolling behind the player. Set on <html> rather than
   <body> because the sticky header is a body child and would otherwise
   shift when the scrollbar goes. */
.gv-ig-player-open {
	overflow: hidden;
}

.gv-ig__cta {
	margin-top: 2.5rem;
	text-align: center;
	justify-content: center;
}

/* I-7 (T-013): this is core's is-style-outline button (shared rule,
   14px/40px padding, matching the hero CTA) — the export's Instagram
   follow CTA is the same outline shape but at px-8/py-3 (32px/12px), not
   the hero's px-10/py-3.5. The selector repeats all three classes the
   wrapper div actually carries (`wp-block-button is-style-outline
   gv-ig__follow`) so this beats the shared is-style-outline rule on
   specificity (0,4,0) vs (0,3,0) — the same fix shape as .gv-hero__cta's
   own is-style-outline scoping just above it in this file, which instead
   wins via an ancestor selector. A bare `.gv-ig__follow >` (two classes)
   was tried first and silently lost to the shared rule; this is the
   corrected version. */
.wp-block-button.is-style-outline.gv-ig__follow > .wp-block-button__link {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	padding: 12px 32px;
}

/* FAQ: native core/details accordion (T-007-design.md §2 deviation 3,
   ADR-004). The theme ships no JavaScript, so the disclosure triangle and
   open/close behaviour come from the browser's native <details> element —
   this only restyles the summary row, the plus/minus glyph and the divider
   between items to match home-reference.html's .faq/.faq summary (T-011).
   No max-width or flex-column rule is needed here: the pattern's own outer
   group already selects the 48rem measure via layout.contentSize
   (T-001-primitives.md §3.2), and <details> elements already stack in
   normal block flow, which is the reference's .faq{flex-direction:column}
   for free.
   F-1 (T-013): the export puts all the padding on the summary row (py-5,
   20px top+bottom) and the answer (pb-5, 20px bottom) — the item itself
   (`.wp-block-details`) carries none, so this drops the old flat
   `1rem 0` in favour of that split. */
.wp-block-details {
	border-bottom: 1px solid var(--gv-gold-15);
}

.wp-block-details summary {
	cursor: pointer;
	list-style: none;
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 1rem;
	padding-block: 1.25rem;
	font-family: var(--wp--preset--font-family--display);
	/* F-3 (T-013): the export steps 18px -> 20px at >=768px (text-lg
	   md:text-xl); the old clamp() read continuously and never landed on
	   either exact value. */
	font-size: var(--wp--preset--font-size--body);
}

@media (min-width: 768px) {
	.wp-block-details summary {
		font-size: var(--wp--preset--font-size--large);
	}
}

/* Chrome/Safari's own disclosure triangle, on top of the ::after glyph
   below, would double up the affordance. */
.wp-block-details summary::-webkit-details-marker {
	display: none;
}

/* F-4 (T-013): a typographic `+`/en-dash in Inter at 20px does not read as
   the export's lucide Plus/Minus at 16px, strokeWidth 1.5 — different
   stroke weight, optical size and vertical centring. Deliberately still a
   CSS `::after` glyph, not an inline SVG placed inside the question text:
   core/details' `summary` attribute is `type: rich-text` (WordPress core
   `blocks/details/block.json`), and this repo has already hit the cost of
   putting content a block's own attribute schema doesn't expect into a
   rich-text field (S-002/T-013's "invalid content" finding, referenced in
   reveal.js's own docblock) — the Site Editor re-parses and can silently
   strip or flag it the next time the page is saved. A `::after` pseudo-
   element sits outside that schema entirely, so this instead redraws the
   glyph itself: two 1.5px bars in a 1rem (16px) box, closed to a plus and
   open to a minus (the vertical bar just disappears), coloured with the
   `gold` token — the closest a script-free, attribute-free CSS shape gets
   to lucide's Plus/Minus without risking exactly the failure mode already
   on record for this repo. */
.wp-block-details summary::after {
	content: "";
	display: inline-block;
	flex-shrink: 0;
	width: 1rem;
	height: 1rem;
	background-image:
		linear-gradient(var(--wp--preset--color--gold), var(--wp--preset--color--gold)),
		linear-gradient(var(--wp--preset--color--gold), var(--wp--preset--color--gold));
	background-repeat: no-repeat;
	background-position: center;
	background-size: 100% 1.5px, 1.5px 100%;
}

.wp-block-details[open] summary::after {
	background-size: 100% 1.5px, 0 100%;
}

/* The answer paragraph — home-reference.html's .faq__a.
   F-5 (T-013): line-height corrected to the export's leading-relaxed
   (1.625, was 1.7) and the 40rem measure cap dropped — the export has no
   cap on the answer text, it relies on the section's own 48rem inner
   measure (same reasoning R-8 used for the testimonial quote). */
.gv-faq__a {
	padding: 0 0 1.25rem;
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.625;
	color: var(--wp--preset--color--muted-soft);
}

/* F-6 (T-013, ADR-014): native <details> cannot animate to height:auto in
   CSS without either a JS-measured height or `interpolate-size`/
   `calc-size()` support. This progressively enhances with the CSS-only
   mechanism — zero script, so ADR-004's "never sit behind a script that
   failed to load" simply does not apply here, and the block stays real
   core/details throughout (no markup change, still client-editable).
   `@supports` gates it to browsers that implement both `interpolate-size`
   and the `::details-content` pseudo-element (shipped together); every
   other browser keeps the native instant toggle, which is a complete,
   fully keyboard-operable, screen-reader-announced disclosure on its own —
   an accepted fidelity gap there, not a broken control. See ADR-014 for
   the full decision and the two alternatives it did not take. */
@supports (interpolate-size: allow-keywords) {
	.wp-block-details {
		interpolate-size: allow-keywords;
	}

	.wp-block-details::details-content {
		height: 0;
		overflow: hidden;
		opacity: 0;
		transition: height 0.3s ease, opacity 0.3s ease, content-visibility 0.3s ease allow-discrete;
	}

	.wp-block-details[open]::details-content {
		height: auto;
		opacity: 1;
	}
}

/* .gv-faq__note removed — ADR-029 (S-006/T-027) deletes the FAQ 'note' kind
   outright rather than rewording it; see home-faq.php's docblock and
   documentation/compliance-register.md (key faq-shipping-threshold). */

/* Footer (T-004): restyled to home-reference.html's .site-footer — charcoal
   ground, four-column grid (28/20/24/28%, the block-width equivalent of the
   reference's 1.4fr/1fr/1.2fr/1.4fr), uppercase tracked column headings, and
   a base bar with a top hairline. Links inherit the theme's default link
   colour rather than the group's has-text-color, and are unreadable on the
   dark charcoal background without this — same fix shape as core's own
   admonition to check contrast against a non-default background. */
/* Links: colour from theme.json's default link colour reads unreadable
   here without an explicit override (same fix shape core itself warns
   about for a non-default background) and underline-on-hover matches the
   reference's `.site-footer a{text-decoration:none}` / `:hover{underline}`
   — theme.json's styles.elements.link sets colour only, so without this a
   footer link stays permanently underlined. The comment used to claim that
   default existed; it does not, and asserting behaviour the code doesn't
   have is worse than no comment (T-005 finding 8). */
.gv-site-footer a {
	color: var(--wp--preset--color--ivory);
	text-decoration: none;
}

.gv-site-footer a:hover {
	text-decoration: underline;
}

/* core's list reset otherwise leaves the ul with its default indent and
   bullets, which the reference does not have. */
.gv-site-footer ul {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.6rem;
}

.gv-site-footer a,
.gv-site-footer ul {
	font-size: var(--wp--preset--font-size--small);
}

/* Column headings: uppercase, tracked, and on the dark-ground muted token —
   a class rather than block attributes so nothing here can be beaten by a
   has-*-color !important (same trap as .gv-eyebrow). */
.gv-site-footer__heading {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 400;
	line-height: 1;
	text-transform: uppercase;
	letter-spacing: 0.22em;
	color: var(--wp--preset--color--muted-dark);
	margin: 0 0 1.1rem;
}

.gv-site-footer__brand {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	margin: 0 0 0.75rem;
}

/* Brand column claim (S-007/T-004, the fourth destyled surface): once
   `gv-placeholder`'s own pinned charcoal/small/italic look is gone, this
   paragraph falls back to the footer group's `has-ivory-color` (full
   ivory) at the site's default 1.125rem body size — the export's kept
   state is 14px ivory at 50% opacity with a relaxed line-height
   (Footer.jsx:34, `text-sm text-ivory/50 leading-relaxed`). */
.gv-site-footer__claim {
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.625;
	color: var(--gv-ivory-50);
}

/* Contact email: the S-001 defect was this wrapping mid-word at narrow
   widths. overflow-wrap:anywhere matches the reference's own fix, but on
   its own it does not reliably land the break at "@" or "." — verified
   in-browser at a forced narrow width, it still broke inside "goldvion" and
   inside "com" (see T-004's task note). parts/footer.html adds two <wbr> at
   those two logical boundaries so a forced wrap prefers them; anywhere
   stays as the fallback for anything narrower still. */
/* font-size is explicit because the email is a <p>, not a link or a list —
   `.gv-site-footer a, .gv-site-footer ul{font-size:small}` above doesn't
   reach it, so without this line it inherits the 1.125rem site body size
   against the reference's 0.875rem: a 28% wider string in the narrowest
   column, and much of why the wrap needed forcing at all (T-005 finding
   7). See parts/footer.html for whether the <wbr> pair is still needed
   now that the size is fixed — re-measured, not removed blind. */
.gv-footer-email {
	font-size: var(--wp--preset--font-size--small);
	overflow-wrap: anywhere;
}

/* Base bar: the sign-off line, separated from the grid by a hairline at
   18% ivory (the reference's own value — plain `hairline` is tuned for
   light grounds, see T-001 §1.3). */
.gv-site-footer__base {
	padding-top: 1.5rem;
	border-top: 1px solid var(--gv-ivory-18);
	display: flex;
	justify-content: space-between;
	flex-wrap: wrap;
	gap: 1rem;
	font-size: var(--wp--preset--font-size--x-small);
	color: var(--wp--preset--color--muted-dark);
}

/* --- Home: responsive pass (T-010) ---
   Core already stacks wp:columns to one column under 782px, so sections 1-9
   reflow correctly with no CSS of ours (verified: no horizontal scroll, no
   overlap or clipping at 390px — see T-010's task note for how the 390px
   viewport was produced). The two rules below are the only mobile-specific
   adjustments the pass needed. */

/* Instagram no longer needs a rule here (T-010): .gv-ig__tile's own media
   queries (above, "Home: testimonials + Instagram") set 85%/45%/33.333%
   directly and cover every width this file's other breakpoints do, so
   there's nothing left for a mobile-only pass to add. */

/* --- Home: mid-width pass (T-005, reconciled against the client's export) ---
   Superseded 2026-09-06. This used to be a 900px/600px pair ported from
   home-reference.html, which was itself reconstructed by eye before we had
   source. The export (state/repos/goldvion-design-export.md §3,
   state/sprints/S-003/notes/export-reconstruction-spec.md §2/§10) shows the
   client's real intent for these four sections is ONE step at 768px
   (Tailwind `md:grid-cols-*`), not two steps at 600/900 — see ADR-012.
   `.gv-experience` (TwoExperiences), `.gv-tiers` (TierSection) and
   `.gv-how-it-works .wp-block-columns` (HowItWorks) all go from a single
   column to their desktop grid at exactly 768px, with nothing in between.
   `.gv-claims` (MaterialSection) is TWO columns at every width below 768px
   and never collapses to one (M-8) — the old 600px block that forced it to
   100% was our own invention with no basis in the export and is removed.

   `.gv-site-footer` is NOT one of the eight home sections the export covers
   (T-002 spec §1/§10 excludes Footer.jsx) and keeps its own 900px/600px
   pair below, unchanged — see ADR-012 for why.

   !important, against this repo's usual rule (style.css's own note on the
   colour trap: "do not reach for !important"): core's own columns/style.css
   sets `flex-wrap: nowrap !important` unconditionally above 782px and
   `flex-basis: 100% !important` below it (verified by reading the shipped
   core CSS, not assumed). A compound selector can't outrank !important —
   only another !important, with source order breaking the tie, can — so
   this is the same forced exception as the .has-*-color trap, not a
   reach for convenience. Kept to the narrowest possible declarations
   (flex-wrap and flex-basis only) rather than a blanket rule. */
@media (max-width: 767px) {
	.gv-experience,
	.gv-tiers,
	.gv-claims,
	.gv-how-it-works .wp-block-columns {
		flex-wrap: wrap !important;
	}

	.gv-experience .wp-block-column,
	.gv-tiers .wp-block-column,
	.gv-how-it-works .wp-block-columns .wp-block-column {
		flex-basis: 100% !important;
	}

	/* A single stacked card at desktop width reads as an error; capping the
	   row keeps it reading as a deliberate one-column layout. The export
	   itself has no width cap here (T-17) — this stays ours. */
	.gv-tiers {
		max-width: 26rem;
		margin-inline: auto;
	}

	/* 50% minus half of the section's own row gap (blockGap, set in the
	   pattern markup) — flexbox, unlike the export's CSS grid, does not
	   shrink flex-basis to make room for `gap` on its own, so a bare 50%
	   here plus the gap overflows the row by the gap's width and wraps to
	   one column per row instead of two. Verified in-browser: without the
	   subtraction the claim columns landed on their own row despite
	   flex-basis:50% computing correctly. Subtracting half of the gap
	   keeps the two columns from ever overflowing. Two columns all the way
	   down to 390px (M-8) — there is no further collapse to one column
	   below this.

	   Review fix (T-014 finding 6): this used to subtract half of the
	   gap's old clamp() max (2rem), i.e. 1rem. T-012 changed this pattern's
	   own blockGap to a flat 1.5rem (home-craftsmanship.php), so the
	   correct subtraction is half of *that* — 0.75rem — not the stale 1rem.
	   It did not visibly break: core's own flex-grow:1 on
	   `.wp-block-column` absorbed the leftover 0.5rem of slack and the row
	   still filled edge-to-edge at the 24px gap. Re-verified in-browser at
	   390px with the corrected value below.

	   Specificity, not just !important, matters here (T-005 finding, this
	   file's earlier note that "a compound selector can't outrank
	   !important" is incomplete): core's own mobile-stack rule is
	   `.wp-block-columns:not(.is-not-stacked-on-mobile) > .wp-block-column`
	   at `(max-width:781px)`, which is three class-level selectors of
	   specificity — a plain two-class `.gv-claims .wp-block-column` ties
	   neither on specificity nor (reliably) on source order and was
	   silently losing to core below 782px, which is exactly the range this
	   rule exists to control. `.gv-claims.wp-block-columns` repeats the
	   class already on that element to match core's specificity on the
	   ancestor, and `.wp-block-column.wp-block-column` does the same on the
	   descendant, putting this selector one level above core's regardless
	   of stylesheet order. Verified in-browser at 390px: computed
	   `flex-basis` is `calc(50% - 0.75rem)`, not core's `100%`. */
	.gv-claims.wp-block-columns .wp-block-column.wp-block-column {
		flex-basis: calc(50% - 0.75rem) !important;
	}

	/* Connector hidden below the same 768px step the grid collapses at
	   (W-10) — each step column is only wide enough for a hairline between
	   them to look intentional once the row is the desktop 3-across grid. */
	.gv-how-it-works__step-rule {
		display: none;
	}
}

/* T-005: reclaim the 768–781px gap core's own mobile-stack breakpoint
   leaves inside the export's 768px step. WordPress's own
   `.wp-block-columns:not(.is-not-stacked-on-mobile) > .wp-block-column`
   rule forces `flex-basis: 100% !important` everywhere *below 782px*, not
   768px — core's threshold is wider than the client's. Left alone, the
   four sections above would still render as one stacked column from
   768–781px even though the export's `md:` step says they should already
   be the desktop grid at exactly 768px (verified in-browser: at 768px
   without this block, tier/experience/how-it-works columns still stacked
   and .gv-claims read flex-basis:100%). `flex-basis: 0` plus the
   `.wp-block-column { flex-grow: 1 }` base rule (unconditional, already
   shipped by core) is what core itself uses at its own ≥782px desktop
   breakpoint to divide a flex row into N equal columns — restating it here
   at ≥768px, at core's own specificity (see the 767px block above for why
   specificity, not just !important, is required to beat this selector),
   is what actually makes the grid appear at the export's own step instead
   of core's. */
@media (min-width: 768px) {
	.gv-experience.wp-block-columns .wp-block-column.wp-block-column,
	.gv-tiers.wp-block-columns .wp-block-column.wp-block-column,
	.gv-how-it-works .wp-block-columns.wp-block-columns .wp-block-column.wp-block-column,
	.gv-claims.wp-block-columns .wp-block-column.wp-block-column {
		flex-basis: 0 !important;
	}
}

/* Footer is shared chrome across every page, including /build-gift and
   /curated-edit, which the export does not cover — ADR-012 retains this
   pair rather than adopting 768px for it. */
@media (max-width: 900px) {
	.gv-site-footer .wp-block-columns {
		flex-wrap: wrap !important;
	}

	.gv-site-footer .wp-block-columns .wp-block-column {
		flex-basis: calc(50% - 1.5rem) !important;
	}
}

@media (max-width: 600px) {
	.gv-site-footer .wp-block-columns .wp-block-column {
		flex-basis: 100% !important;
	}
}

/* --- Global chrome: header + drawer (T-003) ---
   parts/header.html is one wp:html block: the block system has no primitive
   for a sticky three-column bar or an off-canvas nav, so this is entirely
   component CSS per §5 of the primitives contract.

   No-JS contract: the ONLY real control is #gv-drawer-toggle, a visually-
   hidden (not display:none, so it stays focusable) checkbox. Every visible
   affordance — the hamburger, the close glyph, the scrim — is a decorative
   <label for="gv-drawer-toggle">, aria-hidden and tabindex="-1", so mouse
   clicks on any of them toggle the one checkbox via the browser's native
   label behaviour with zero script. A keyboard user without JS tabs to the
   checkbox itself and toggles it open or closed with Space. The checkbox's
   OWN native checked/unchecked state is what a screen reader announces
   with no script at all — its static aria-label is the neutral "Menu", not
   "Open menu", so it never lies about a state it cannot update; the drawer
   panel's visibility:hidden (below) is what actually removes the closed
   drawer from the tab order and the accessibility tree without JS (T-005
   finding 2 — transform alone only hides it visually). assets/js/header.js
   (enqueued, defer) is additive on top of that native baseline: it upgrades
   the checkbox's aria-label to "Open menu"/"Close menu" and adds
   aria-expanded once it can keep both truthful, moves focus into the
   drawer on open and back out on close, closes on Escape, closes the
   drawer after a same-page anchor link inside it is followed (T-005
   finding 5 — a checkbox cannot react to a hash change on its own), and
   marks the drawer inert while closed as a second, redundant mechanism
   alongside visibility. None of it is required for the drawer to open or
   close. Verified with JavaScript disabled in the browser. */
.gv-header-root {
	position: relative;
}

/* .gv-inside-toggle (T-009's per-tier "What's Inside" modal control) shares
   this clip technique for the same reason: a real, focusable checkbox that
   is never visually present. */
.gv-drawer-toggle,
.gv-inside-toggle {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* The checkbox itself is 1px and clipped, so its own focus ring would be
   invisible — suppressed here, and redirected to the hamburger glyph (or,
   for .gv-inside-toggle, the "What's Inside?" label — see T-009's own block
   above) it controls via the sibling+descendant combinator below. */
.gv-drawer-toggle:focus-visible,
.gv-inside-toggle:focus-visible {
	outline: none;
}

.gv-drawer-toggle:focus-visible ~ .gv-site-header .gv-site-header__left {
	outline: 2px solid var(--wp--preset--color--charcoal);
	outline-offset: 2px;
}

/* Sticky lives on WP's own template-part wrapper (`<header
   class="wp-block-template-part">`), not on .gv-site-header. A sticky
   element's stick range is bounded by its immediate parent's content box;
   .gv-header-root only ever contains the header's own in-flow height (the
   checkbox is absolute, the drawer and scrim are fixed), so a sticky rule
   placed there has zero room to move and the header scrolls away like a
   static element. The auto-generated wrapper one level up is a plain flow
   child of .wp-site-blocks, which spans the full page — the same role
   <body> plays for the reference's .site-header, a direct child of it.
   Fixed T-005 finding 1. No backdrop-filter here: filter/backdrop-filter
   creates a new containing block for position:fixed descendants, and the
   drawer + scrim are both fixed and both render inside this wrapper — that
   filter would silently pin them to this box instead of the viewport. */
.wp-block-template-part:has(> .gv-header-root) {
	position: sticky;
	top: 0;
	z-index: 50;
}

.gv-site-header {
	background: var(--gv-ivory-90);
	backdrop-filter: blur(8px);
	border-bottom: 1px solid var(--wp--preset--color--hairline);
}

.gv-site-header__bar {
	display: grid;
	grid-template-columns: 1fr auto 1fr;
	align-items: center;
	gap: 1rem;
	padding: 1.15rem var(--wp--preset--spacing--gutter);
}

.gv-icon-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	padding: 0;
	background: none;
	border: 0;
	color: var(--wp--preset--color--charcoal);
	cursor: pointer;
	border-radius: var(--wp--custom--radius--pill);
	text-decoration: none;
}

.gv-icon-btn:hover {
	background: var(--wp--preset--color--surface-muted);
}

.gv-site-header__left {
	justify-self: start;
}

.gv-site-header__right {
	justify-self: end;
}

.gv-wordmark {
	font-family: var(--wp--preset--font-family--display);
	font-size: clamp(var(--wp--preset--font-size--large), 2.4vw, var(--wp--preset--font-size--x-large));
	letter-spacing: 0.025em;
	text-decoration: none;
	color: var(--wp--preset--color--charcoal);
	white-space: nowrap;
}

/* slide-in nav — the hamburger's panel.
   Closed state must be genuinely inert without JS, not just off-screen:
   transform alone leaves the four links focusable and in the a11y tree —
   a no-JS keyboard user tabs into four invisible off-screen links on every
   page, and a screen reader announces the "Main" landmark and all four
   links whether the drawer is open or not. visibility:hidden removes an
   element (and its subtree) from both the tab order and the accessibility
   tree in every browser, no script required — header.js's `inert` on the
   open drawer does the same job when JS is present and is left in place as
   the redundant belt-and-braces case. The visibility transition is delayed
   on close (and instant on open) so the slide animation still plays before
   the panel drops out of the tab order. Fixed T-005 finding 2. */
.gv-drawer {
	position: fixed;
	inset: 0 auto 0 0;
	width: min(23rem, 86vw);
	z-index: 60;
	background: var(--wp--preset--color--ivory);
	border-right: 1px solid var(--wp--preset--color--hairline);
	padding: 1.15rem var(--wp--preset--spacing--gutter);
	transform: translateX(-100%);
	visibility: hidden;
	transition: transform 0.32s ease, visibility 0s linear 0.32s;
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
}

.gv-drawer-toggle:checked ~ .gv-drawer {
	transform: translateX(0);
	visibility: visible;
	transition: transform 0.32s ease, visibility 0s linear 0s;
}

/* T-015 addition, not in the reference (no client mobile design exists —
   see task note): the scrim already blocks pointer clicks on the page
   behind the open drawer, but not a scroll wheel or touch-drag over it, so
   the home page keeps scrolling under an open drawer — confirmed with a
   real mouse wheel over the scrim, checkbox checked, window.scrollY moving.
   :has() is already used elsewhere in this file (the sticky-header rule
   above), so reaching for it here to lock the body without JS is
   consistent rather than a new dependency. No visual change while closed.
   QA (T-017) found the body rule alone has no effect: this document's real
   scrolling element is <html>, not <body> (documentElement.scrollTop moves,
   body.scrollTop stays 0). Target html as well as body — the actual
   scroller varies by browser/doctype and both selectors are cheap. */
html:has(.gv-drawer-toggle:checked),
body:has(.gv-drawer-toggle:checked) {
	overflow: hidden;
}

.gv-drawer__top {
	display: flex;
	justify-content: flex-end;
	margin-bottom: 1.5rem;
}

.gv-drawer a {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	text-decoration: none;
	color: var(--wp--preset--color--charcoal);
	padding: 0.55rem 0;
	border-bottom: 1px solid var(--wp--preset--color--hairline);
}

.gv-drawer a:hover {
	color: var(--wp--preset--color--muted);
}

.gv-scrim {
	position: fixed;
	inset: 0;
	z-index: 55;
	display: block;
	background: var(--gv-fg-45);
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.32s;
}

.gv-drawer-toggle:checked ~ .gv-scrim {
	opacity: 1;
	pointer-events: auto;
}

/* --- WooCommerce + builder: shared surfaces (T-016) ---
   Cart, checkout, my-account and both builder pages already inherit the
   header/footer template parts and theme.json's global styles (colour,
   button, heading) without any CSS from here — verified in-browser, e.g.
   the checkout Place Order button (`.wp-element-button`) lands on-spec with
   zero rules of ours. What's below is only the surfaces that DON'T reach a
   `.wp-element-button`/heading-element selector on their own. */

/* Checkout's own header (WooCommerce's namespaced `checkout-header` part —
   see state/sprints/S-002/notes/T-016-*.md; not overridden, per B-011) uses
   core's `core/site-title` block, which the real header never does (it's a
   hand-built `.gv-wordmark` anchor in parts/header.html). `.wp-block-
   site-title` is therefore unique to checkout on this build, safe to style
   globally, and worth it: with no rule at all it fell through to the theme's
   plain body font at 18px, reading as a different, unbranded site at the
   exact moment a customer is entering card details. The site's Settings ->
   General title itself is still "Goldvion", not "Goldvion Jewellery" like
   the real wordmark — a content mismatch outside this task's CSS-only
   remit, noted for the PM rather than changed here. */
.wp-block-site-title {
	font-family: var(--wp--preset--font-family--display);
	font-size: clamp(var(--wp--preset--font-size--large), 2.4vw, var(--wp--preset--font-size--x-large));
	font-weight: 500;
	letter-spacing: 0.025em;
	color: var(--wp--preset--color--charcoal);
}

.wp-block-site-title a {
	color: inherit;
	text-decoration: none;
}

/* Form controls: checkout billing, the coupon field, my-account login, and
   the builder's own preferences textarea all resolve to a plain <input>/
   <select>/<textarea> underneath whatever wrapper WooCommerce or the
   builder puts around it. Border colour and radius only — padding and
   background are left alone deliberately, because WooCommerce's floating-
   label inputs (checkout) position their label off that exact padding, and
   fighting it would misalign the label rather than restyle it. */
/* The bare type selectors are the fallback (classic forms — my-account
   login, the builder's .gv-field textarea). The `.wc-block-components-
   text-input input`/`.woocommerce-input-wrapper input` pairs exist because
   WooCommerce's own stylesheets ship a same-specificity, later-loaded rule
   for the checkout/cart block inputs that would otherwise win by source
   order — same family of trap as the .has-*-color one in T-001 §5, just
   border-color instead of a block attribute. */
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="password"],
input[type="number"],
input[type="url"],
input[type="search"],
select,
textarea,
.woocommerce-input-wrapper input,
.wc-block-components-combobox input,
.woocommerce form .form-row .input-text,
.woocommerce form .form-row select,
.wc-block-components-form .wc-block-components-text-input input[type="text"],
.wc-block-components-form .wc-block-components-text-input input[type="email"],
.wc-block-components-form .wc-block-components-text-input input[type="tel"],
.wc-block-components-form .wc-block-components-text-input input[type="password"],
.wc-block-components-form .wc-block-components-text-input input[type="number"],
.wc-block-components-form .wc-block-components-text-input input[type="url"] {
	border-color: var(--wp--preset--color--hairline);
	border-radius: var(--wp--custom--radius--base, 0.25rem);
}

input[type="text"]:focus-visible,
input[type="email"]:focus-visible,
input[type="tel"]:focus-visible,
input[type="password"]:focus-visible,
input[type="number"]:focus-visible,
input[type="url"]:focus-visible,
input[type="search"]:focus-visible,
select:focus-visible,
textarea:focus-visible,
.woocommerce-input-wrapper input:focus-visible,
.wc-block-components-text-input input:focus-visible,
.woocommerce form .form-row .input-text:focus-visible,
.woocommerce form .form-row select:focus-visible {
	outline: 2px solid var(--wp--preset--color--charcoal);
	outline-offset: 1px;
}

/* Builder title centring (T-013, B-036) ---------------------------------- */

/* The page's own <h1> -- since T-022/Q-003, `builder.php`'s own
   `.gv-builder__title`, rendered inside `.gv-builder` after the eyebrow --
   must share one centre line with `.gv-builder__eyebrow` and `.gv-steps`
   (builder.css), matching the export's BuildGift.jsx:82-85, which wraps its
   own eyebrow and h1 in a single `text-center` div.

   Scoped to `.gv-builder-page` (functions.php: detects the
   `[goldvion_builder]` shortcode in post_content) and keyed to the bare `h1`
   tag rather than to a class, on purpose: this rule was written by T-013
   before T-022 moved the h1 out of the theme's `wp-block-post-title` block
   and into `builder.php`'s own markup, and it kept matching unchanged
   through that swap for exactly this reason. The `.gv-builder-page` scope
   keeps it off every other page's title -- verified on the front page,
   which carries no such class and renders no post-title block at all. */
body.gv-builder-page h1 {
	text-align: center;
}

/* ==========================================================================
   /cart/ and /checkout/ — client design pass (T-004, S-006 chain A)
   ==========================================================================
   ADR-023 moved these two pages to WooCommerce's classic-shortcode markup so
   a scripting-off customer can build a cart and place an order at all
   (B-030). ADR-026 + state/sprints/S-006/notes/T-002-contract.md are the
   design contract this section builds to; every rule below carries the
   citation the contract gave it. One contiguous block, appended here on
   purpose (T-002-contract.md §1.3): chain B is live in the same sprint and
   this keeps any merge conflict a single textual hunk.

   CSS-only. No file under app/ other than this stylesheet was touched to
   produce it, and no `.gv-cart-*` class name is reused here — the drawer
   (cart-drawer.css) renders on /cart/ itself and colliding would move both
   surfaces at once (ADR-026 Consequences).

   theme.json already supplies body/heading typography and every
   .wp-element-button's look (background, text, radius, caps, hover) via the
   has-button-styles gate — nothing below re-declares any of that. The only
   button exceptions are §CTA width and the disabled state, both cited
   inline (T-002-contract.md §4).
*/

/* --- Cart: page container width ------------------------------------------
   Woo forces `table.shop_table { width: 70% }` on both the line-list and
   the review-order table (T-002-contract.md §4). Scaffolding reset, shared
   by every shop_table on these two pages. */
body.woocommerce-cart table.shop_table,
body.woocommerce-checkout table.shop_table {
	width: 100%;
}

/* === /cart/ — Region A: the line list (CartDrawer.jsx CartItem, 7-47) === */

body.woocommerce-cart form.woocommerce-cart-form table.shop_table.cart {
	border: 0;
}

body.woocommerce-cart form.woocommerce-cart-form table.shop_table.cart tr.woocommerce-cart-form__cart-item {
	border: 0;
	border-bottom: 1px solid var(--gv-gold-15); /* CartDrawer.jsx:9 */
}

body.woocommerce-cart form.woocommerce-cart-form table.shop_table.cart td {
	padding-top: 1.25rem; /* CartDrawer.jsx:9, py-5 */
	padding-bottom: 1.25rem;
	border: 0;
}

body.woocommerce-cart td.product-name {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--body);
	color: var(--wp--preset--color--charcoal);
} /* CartDrawer.jsx:11 */

/* T-017 finding 3: `display: flex; flex-direction: column` neutralised
   woocommerce.css's own `dt { float: left; clear: both }` — float has no
   effect on a flex item — so every dt and every dd landed on its own
   line instead of pairing up, rendering the seven-row brief as fourteen
   lines against CartDrawer.jsx:16-31's one line per item. Removed the
   flex declaration and let the float mechanism do the pairing it was
   always meant to; the stacked-column look this rule wants is the
   margin-bottom below, on the pair itself (clear:both starts a new line
   per dt), not a flex column on the dl. */
body.woocommerce-cart td.product-name dl.variation {
	margin-top: 0.5rem;
	font-size: var(--wp--preset--font-size--x-small);
	color: var(--wp--preset--color--muted-soft); /* CartDrawer.jsx:16-31, charcoal/55 */
}

body.woocommerce-cart td.product-name dl.variation dt,
body.woocommerce-cart td.product-name dl.variation dd {
	margin-bottom: 0.125rem;
}

body.woocommerce-cart td.product-name dl.variation dd p {
	margin: 0;
}

body.woocommerce-cart td.product-price,
body.woocommerce-cart td.product-subtotal {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--body);
	color: var(--wp--preset--color--charcoal);
} /* CartDrawer.jsx:43 */

/* The one !important in this diff, authorised by T-002-contract.md §3:
   `.woocommerce a.remove { color: var(--wc-red)!important }` and its
   `:hover` counterpart cannot be beaten at any specificity by a
   non-!important rule. Size/weight are ordinary and need no !important. */
body.woocommerce-cart .woocommerce-cart-form td.product-remove a.remove {
	font-size: 1.125rem;
	font-weight: 400;
	color: var(--gv-fg-30) !important; /* CartDrawer.jsx:12-14 */
}

body.woocommerce-cart .woocommerce-cart-form td.product-remove a.remove:hover {
	background: transparent; /* kill Woo's red :hover fill */
	color: var(--wp--preset--color--charcoal) !important;
}

/* Product thumbnail: ours, kept deliberately (ADR-026 "what is ours" #2).
   Retokenised off Woo's raw 117px/120px onto the theme's own radius token
   and a size judged against the theme's rem scale rather than measured
   from the export, which draws none — ASSUMED, flag if a reviewer disagrees. */
body.woocommerce-cart td.product-thumbnail img {
	width: 5.5rem;
	height: 5.5rem;
	object-fit: cover;
	border-radius: var(--wp--custom--radius--base);
}

/* div.quantity > input.qty: stays Woo's number input. The generic
   `input[type="number"]` border/radius rule at the top of this file
   already reaches it — no new rule needed here (T-002-contract.md §6). */

/* thead: ours, no counterpart in the export (ADR-026 "what is ours" #3). */
body.woocommerce-cart table.shop_table.cart thead th {
	font-size: var(--wp--preset--font-size--micro);
	font-weight: 400;
	text-transform: uppercase;
	letter-spacing: 0.22em;
	color: var(--gv-fg-40);
	border: 0;
}

/* === /cart/ — Region B: coupon and actions (CartDrawer.jsx:104-116) ===
   Shared, identical, with /checkout/'s own coupon field further down —
   T-002-contract.md §7.3 asks for one rule, both pages, both cite the same
   two CartDrawer.jsx lines. */

body.woocommerce-cart td.actions div.coupon,
body.woocommerce-checkout #woocommerce-checkout-form-coupon {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

body.woocommerce-cart #coupon_code.input-text,
body.woocommerce-checkout #coupon_code.input-text {
	border: 0;
	border-bottom: 1px solid var(--gv-gold-20);
	background: transparent;
	padding: 0.375rem 0;
	font-size: var(--wp--preset--font-size--x-small);
	color: var(--wp--preset--color--charcoal);
} /* CartDrawer.jsx:108 */

body.woocommerce-cart #coupon_code.input-text:focus,
body.woocommerce-checkout #coupon_code.input-text:focus {
	border-bottom-color: var(--wp--preset--color--gold);
	outline: none;
}

body.woocommerce-cart #coupon_code.input-text::placeholder,
body.woocommerce-checkout #coupon_code.input-text::placeholder {
	color: var(--gv-fg-30);
}

/* The one .wp-element-button on either page that must be un-buttoned —
   text-only, not a pill (T-002-contract.md §6 Region B, CartDrawer.jsx:112). */
body.woocommerce-cart button[name="apply_coupon"],
body.woocommerce-checkout button[name="apply_coupon"] {
	background: transparent;
	border: 0;
	padding: 0;
	color: var(--wp--preset--color--gold);
	font-size: var(--wp--preset--font-size--micro);
	letter-spacing: 0.22em;
	text-transform: uppercase;
}

/* button[name="update_cart"]: ours, no counterpart — left as theme.json's
   pill, no rule (ADR-026 "what is ours" #3). */

/* Unspecified in the contract's capture (single-line cart, no coupon
   applied) — styled rather than deferred, per the team lead's ruling that a
   validation surface should not ship unstyled this sprint. Restrained: the
   discount row reads like any other totals row in the palegold accent
   CartDrawer.jsx already uses for a successful promo (CartDrawer.jsx:123-126,
   `text-palegold`); the two error/invalid surfaces borrow the `.required`
   red already in use elsewhere on these pages rather than inventing a new
   colour with no citation. */
body.woocommerce-cart tr.cart-discount th,
body.woocommerce-cart tr.cart-discount td,
body.woocommerce-checkout tfoot tr.cart-discount th,
body.woocommerce-checkout tfoot tr.cart-discount td {
	font-size: var(--wp--preset--font-size--small);
	font-weight: 400;
	color: var(--wp--preset--color--gold);
}

/* T-017 finding 4: on /cart/, tr.cart-subtotal and tr.order-total below
   are flexed (justify-content: space-between) to keep label and amount
   on one line; tr.cart-discount was left out of that treatment, so once
   its siblings left table layout it was the only row still placed by the
   table column algorithm — an applied coupon's amount stops lining up
   with Subtotal and Total above and below it. Checkout's own
   tr.cart-discount is deliberately untouched: none of #order_review's
   totals rows are flexed there, so it already matches its siblings. */
body.woocommerce-cart .cart_totals tr.cart-discount {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
}

body.woocommerce-cart .woocommerce-error,
body.woocommerce-checkout .woocommerce-error {
	border: 1px solid var(--wc-red);
	border-radius: var(--wp--custom--radius--base);
	background: transparent;
	color: var(--wp--preset--color--charcoal);
	font-size: var(--wp--preset--font-size--small);
	padding: 1rem 1.25rem;
}

body.woocommerce-checkout p.form-row.woocommerce-invalid .input-text,
body.woocommerce-checkout p.form-row.woocommerce-invalid select {
	border-bottom-color: var(--wc-red);
}

/* === /cart/ — Region C: the totals (Checkout.jsx Order Summary, 104-141) === */

body.woocommerce-cart .cart-collaterals .cart_totals {
	box-sizing: border-box; /* this element's default computed content-box, confirmed
		in-browser, let border+padding add onto its 100%-width column and
		overflow it by 31px at 390px — the site carries no global border-box
		reset to fall back on */
	border: 1px solid var(--gv-gold-20);
	border-radius: var(--wp--custom--radius--base);
	padding: 1.5rem;
} /* Checkout.jsx:106 */

body.woocommerce-cart .cart_totals > h2 {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	color: var(--wp--preset--color--charcoal);
	margin-bottom: 1.5rem;
} /* Checkout.jsx:105, overrides Woo's `medium` */

body.woocommerce-cart .cart_totals table.shop_table {
	border: 0;
}

body.woocommerce-cart .cart_totals table.shop_table td,
body.woocommerce-cart .cart_totals table.shop_table th {
	border: 0;
	padding: 0.375rem 0;
}

/* "justify apart" is a row-level layout, not a per-cell one: putting
   `display: flex` on the <th>/<td> themselves (tried first) turns each into
   its own single-item flex box, which the table-layout algorithm then
   wraps in an anonymous table-cell and stacks — verified in-browser, the
   value fell to a second line under the label instead of sitting beside
   it. Flexing the <tr> itself keeps label and value on one line. */
body.woocommerce-cart .cart_totals tr.cart-subtotal,
body.woocommerce-cart .cart_totals tr.order-total {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
}

body.woocommerce-cart .cart_totals tr.cart-subtotal th,
body.woocommerce-cart .cart_totals tr.cart-subtotal td {
	font-size: var(--wp--preset--font-size--small);
	font-weight: 400;
	color: var(--wp--preset--color--muted-soft);
} /* Checkout.jsx:118-120 */

body.woocommerce-cart .cart_totals tr.order-total {
	border-top: 1px solid var(--gv-gold-15);
	padding-top: 0.5rem;
}

body.woocommerce-cart .cart_totals tr.order-total th,
body.woocommerce-cart .cart_totals tr.order-total td {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--large);
	font-weight: 400;
	color: var(--wp--preset--color--charcoal);
} /* Checkout.jsx:130-132 */

body.woocommerce-cart .wc-proceed-to-checkout a.checkout-button {
	width: 100%;
	text-align: center;
} /* Checkout.jsx:134-140 — everything else already lands from theme.json */

/* T-019: tr.shipping now renders (T-018 seeded a real Malaysia zone,
   flat rate and free-shipping threshold) — given the same label/value
   treatment as tr.cart-subtotal above, but as its own standalone rule
   rather than folded into that selector list, so it inherits nothing
   from the earlier work and can change independently of it. */
body.woocommerce-cart .cart_totals tr.shipping {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
}

body.woocommerce-cart .cart_totals tr.shipping th,
body.woocommerce-cart .cart_totals tr.shipping td {
	font-size: var(--wp--preset--font-size--small);
	font-weight: 400;
	color: var(--wp--preset--color--muted-soft);
} /* Checkout.jsx:121-124 */

/* Checkout.jsx:125-129 also draws a second line under a priced shipping
   row — a palegold "Free shipping above RM199" note with a truck icon,
   shown only while shipping is not yet free. It exists nowhere in this
   codebase (checked theme templates, patterns, and both cart/checkout
   PHP) and is deliberately NOT added here: (1) this task is scoped as
   styling only — tr.shipping already renders correctly from T-018, and
   this second line requires new PHP (a woocommerce_cart_totals hook or
   equivalent to detect "not yet free" and emit the copy), not CSS; (2)
   RM199 is still an UNCONFIRMED placeholder (H-004) and this line would
   put that exact number into new customer-facing copy, which is what
   H-004 says not to do. Flagging as a finding for the backlog rather
   than silently adding or silently dropping it. */

/* === /cart/ — the empty state (T-007) ===
   Out of scope for T-002/ADR-026 entirely (T-002-contract.md §9): neither
   region split covers it, and CartDrawer.jsx's own empty state
   (CartDrawer.jsx:79-90) is the 448px drawer PANEL's, not a full page's.
   Checked instead against Checkout.jsx's own page-level empty state
   (Checkout.jsx:63-67, `py-20 text-center` / `font-display text-xl
   text-charcoal/40` / the Build My Gift link) — functions.php's three
   `wc_empty_cart_message` / `woocommerce_return_to_shop_*` filters bring
   the copy and target in line with it; this is the layout half. */

body.woocommerce-cart .wc-empty-cart-message {
	padding: 5rem 0;
	text-align: center;
} /* Checkout.jsx:64 */

/* Strip the notice-banner's blue chrome — same instance of the family
   already handled at body.woocommerce-checkout .wc-block-components-
   notice-banner.is-info above, not a second invented treatment.
   T-017 finding 7: these three rules were unscoped, so they caught
   EVERY info-type notice on /cart/, not just the empty-cart message —
   `wc_add_notice(..., 'notice')` fires this same markup for "X has been
   removed from your cart because it has since been modified" and "Your
   order was cancelled", both reachable on a populated cart, both then
   rendering with their icon gone and the empty-state's large muted
   heading treatment. `wc_empty_cart_message()` prints straight into the
   `.wc-empty-cart-message` wrapper (confirmed in WooCommerce's own
   source); the general notices above render outside it, in
   `.woocommerce-notices-wrapper` — so scoping to that wrapper, matching
   the sibling padding rule above, narrows this to the empty state only. */
body.woocommerce-cart .wc-empty-cart-message .wc-block-components-notice-banner.is-info {
	background: transparent;
	border: 0;
	padding: 0;
	display: block;
}

/* The banner's own svg carries a hardcoded solid circle (`fill:#fff;
   background-color:#2f2f2f;border-radius:50%` — wc-blocks.css), the "solid
   black circle" this task exists to remove. Hidden rather than recoloured,
   matching the precedent already set for this exact icon family at
   body.woocommerce-checkout's own .is-info rule below — nothing in ADR-026's
   "ours" list invents an icon treatment. */
body.woocommerce-cart .wc-empty-cart-message .wc-block-components-notice-banner.is-info svg {
	display: none;
}

body.woocommerce-cart .wc-empty-cart-message .wc-block-components-notice-banner__content {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--large);
	color: var(--gv-fg-40);
	padding-right: 0;
} /* Checkout.jsx:65, `font-display text-xl text-charcoal/40` */

body.woocommerce-cart p.return-to-shop {
	margin: 1rem 0 0;
}

/* Un-buttoned exactly like button[name="apply_coupon"] above — a bare text
   link, not a pill. Checkout.jsx:66's Build My Gift link carries no CTA
   styling of its own (`mt-4 inline-block text-[10px] tracking-luxe uppercase
   text-palegold`), so the theme.json button treatment this anchor would
   otherwise inherit through .wp-element-button is wrong here. */
body.woocommerce-cart p.return-to-shop a.button {
	background: transparent;
	border: 0;
	border-radius: 0;
	padding: 0;
	display: inline-block;
	color: var(--wp--preset--color--gold);
	font-size: var(--wp--preset--font-size--micro);
	letter-spacing: 0.32em;
	text-transform: uppercase;
} /* Checkout.jsx:66 */

/* ==========================================================================
   /checkout/ — Checkout.jsx throughout, cited by line
   ========================================================================== */

/* --- 7.2 Customer details ------------------------------------------------ */

body.woocommerce-checkout .woocommerce-billing-fields > h3,
body.woocommerce-checkout .woocommerce-additional-fields > h3,
body.woocommerce-checkout #order_review_heading {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	color: var(--wp--preset--color--charcoal);
	margin-bottom: 1.5rem;
} /* Checkout.jsx:71, 105 */

body.woocommerce-checkout .woocommerce-billing-fields__field-wrapper {
	display: grid;
	grid-template-columns: 1fr;
	gap: 1.25rem;
} /* Checkout.jsx:72 */

@media (min-width: 768px) {
	body.woocommerce-checkout .woocommerce-billing-fields__field-wrapper {
		grid-template-columns: 1fr 1fr;
	}
}

body.woocommerce-checkout p.form-row {
	padding: 0;
	margin: 0 0 6px;
} /* the grid owns the spacing, Checkout.jsx:72 */

body.woocommerce-checkout p.form-row > label {
	display: block;
	margin-bottom: 0.5rem;
	line-height: normal;
	font-size: var(--wp--preset--font-size--micro);
	letter-spacing: 0.32em;
	text-transform: uppercase;
	color: var(--gv-fg-40);
} /* Checkout.jsx:12 */

body.woocommerce-checkout p.form-row .input-text,
body.woocommerce-checkout p.form-row select,
body.woocommerce-checkout p.form-row textarea {
	width: 100%;
	border: 0;
	border-bottom: 1px solid var(--gv-gold-20);
	background: transparent;
	padding: 0.5rem 0;
	color: var(--wp--preset--color--charcoal);
} /* Checkout.jsx:17, replaces Woo's white box */

body.woocommerce-checkout p.form-row .input-text:focus,
body.woocommerce-checkout p.form-row select:focus,
body.woocommerce-checkout p.form-row textarea:focus {
	border-bottom-color: var(--wp--preset--color--gold);
	/* the existing :focus-visible outline at this file's WooCommerce +
	   builder section stays — accessibility, the theme already made this
	   call (T-002-contract.md §7.2). No outline: none here. */
}

/* The State/County select is enhanced by Select2, which hides the real
   <select> (T-002-contract.md's `p.form-row select` rule above never
   reaches it — confirmed in-browser: the boxed white control that survived
   is `.select2-selection`, a sibling span Select2 inserts, not our
   underline field). Not in the contract's table; added to carry the same
   underline treatment onto the one field Select2 intercepts, rather than
   leave a single boxed control breaking the row's own look. */
body.woocommerce-checkout p.form-row .select2-container--default .select2-selection--single {
	border: 0;
	border-bottom: 1px solid var(--gv-gold-20);
	border-radius: 0;
	background: transparent;
	height: auto;
	padding: 0.5rem 0;
}

body.woocommerce-checkout p.form-row .select2-container--default .select2-selection--single .select2-selection__rendered {
	padding: 0;
	line-height: normal;
	color: var(--wp--preset--color--charcoal);
}

body.woocommerce-checkout p.form-row .select2-container--default .select2-selection--single .select2-selection__arrow {
	top: 50%;
	right: 0;
	transform: translateY(-50%);
}

/* p.form-row .required: leave Woo's red — no source for a different one. */

/* .woocommerce-shipping-fields renders empty on this stack — no rule
   (T-002-contract.md §7.2). */

/* --- 7.3 The coupon toggle: ours, not hers (ADR-026 addition #6) ------- */

body.woocommerce-checkout a.showcoupon {
	font-size: var(--wp--preset--font-size--micro);
	letter-spacing: 0.22em;
	text-transform: uppercase;
	color: var(--wp--preset--color--gold);
} /* CartDrawer.jsx:112 */

body.woocommerce-checkout .wc-block-components-notice-banner.is-info {
	background: transparent;
	border: 0;
	border-top: 1px solid var(--gv-gold-15);
	border-radius: 0;
	padding: 0.75rem 0;
	color: var(--wp--preset--color--charcoal);
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

/* The banner's own svg carries a hardcoded blue circle background —
   confirmed in-browser (getComputedStyle showed `background: rgb(0, 124,
   186)` set directly on the <svg>, not on an ancestor this rule already
   reaches). Not in T-002-contract.md's table; added after seeing the blue
   chrome survive the rule above. Hidden rather than recoloured — a plain
   hairline row reads more restrained than a re-tinted icon, and nothing in
   ADR-026's "ours" list invents an icon treatment. */
body.woocommerce-checkout .wc-block-components-notice-banner.is-info svg {
	display: none;
}

/* --- 7.4 Order review ---------------------------------------------------- */

body.woocommerce-checkout #order_review {
	box-sizing: border-box; /* same content-box overflow as .cart_totals above */
	border: 1px solid var(--gv-gold-20);
	border-radius: var(--wp--custom--radius--base);
	padding: 1.5rem;
} /* Checkout.jsx:106 */

body.woocommerce-checkout #order_review table.shop_table {
	border: 0;
}

body.woocommerce-checkout #order_review table.shop_table thead th.product-name,
body.woocommerce-checkout #order_review table.shop_table thead th.product-total {
	font-size: var(--wp--preset--font-size--micro);
	font-weight: 400;
	text-transform: uppercase;
	letter-spacing: 0.22em;
	color: var(--gv-fg-40);
	border: 0;
} /* ours — the export's summary has no header row, ADR-026 addition #7 */

body.woocommerce-checkout #order_review table.shop_table tbody tr.cart_item {
	border: 0;
	border-bottom: 1px solid var(--gv-gold-10);
}

/* T-017 finding 8: padding does not apply to table-row boxes, so the
   `padding: 1rem 0` above did nothing and this region sat tighter than
   Checkout.jsx:108's `py-4` at every viewport — a dead declaration and a
   real divergence. The /cart/ region above got this right by padding
   the `td`s themselves; mirrored here. */
body.woocommerce-checkout #order_review table.shop_table tbody tr.cart_item td {
	padding-top: 1rem;
	padding-bottom: 1rem;
} /* Checkout.jsx:108 */

body.woocommerce-checkout #order_review table.shop_table tbody tr.cart_item:last-child {
	border-bottom: 0;
}

body.woocommerce-checkout #order_review td.product-name {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--body);
	color: var(--wp--preset--color--charcoal);
} /* Checkout.jsx:109 */

body.woocommerce-checkout #order_review td.product-name strong.product-quantity {
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 400;
	color: var(--gv-fg-50);
} /* Checkout.jsx:110 */

/* T-017 finding 3, same fix as the /cart/ rule above: no flex column on
   the dl, so woocommerce.css's dt float still pairs each label with its
   value on one line. */
body.woocommerce-checkout #order_review td.product-name dl.variation {
	margin-top: 0.25rem;
	font-size: var(--wp--preset--font-size--x-small);
	color: var(--gv-fg-50);
} /* Checkout.jsx:110-114, charcoal/50 not muted-soft's /55 */

body.woocommerce-checkout #order_review td.product-name dl.variation dt,
body.woocommerce-checkout #order_review td.product-name dl.variation dd {
	margin-bottom: 0.125rem;
}

body.woocommerce-checkout #order_review td.product-name dl.variation dd p {
	margin: 0;
}

body.woocommerce-checkout #order_review td.product-total {
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--charcoal);
} /* Checkout.jsx:115 */

body.woocommerce-checkout #order_review tfoot tr.cart-subtotal th,
body.woocommerce-checkout #order_review tfoot tr.cart-subtotal td {
	font-size: var(--wp--preset--font-size--small);
	font-weight: 400;
	color: var(--wp--preset--color--muted-soft);
} /* Checkout.jsx:118-120 */

/* T-019: tr.shipping now renders here too. No flex needed — unlike
   /cart/, none of #order_review's totals rows are flexed (T-017
   finding 3), so plain table layout already keeps label and value on
   one line; matched to tr.cart-subtotal's own rule above, standalone
   rather than appended to its selector list. */
body.woocommerce-checkout #order_review tfoot tr.shipping th,
body.woocommerce-checkout #order_review tfoot tr.shipping td {
	font-size: var(--wp--preset--font-size--small);
	font-weight: 400;
	color: var(--wp--preset--color--muted-soft);
} /* Checkout.jsx:121-124 — the "Free shipping above RM199" note at
     125-129 is not added; see the /cart/ region's rule above for why. */

body.woocommerce-checkout #order_review tfoot tr.order-total th,
body.woocommerce-checkout #order_review tfoot tr.order-total td {
	border-top: 1px solid var(--gv-gold-15);
	padding-top: 0.5rem;
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--large);
	color: var(--wp--preset--color--charcoal);
} /* Checkout.jsx:130-132 */

body.woocommerce-checkout #order_review tfoot tr.order-total td strong {
	font-weight: 400;
}

/* --- 7.5 Payment and Place Order: ours, not hers (ADR-026 addition #5) - */

/* Woo's own `#payment` container carries a translucent purple tint of its
   own (confirmed in-browser: `rgba(129, 110, 153, 0.14)`, not in the
   contract's table since it belongs to the callout chrome the contract
   already asks to strip, §7.5's "restrained ... nothing invented"). Left in
   place it reads as an unrelated component dropped onto an otherwise ivory
   page. */
body.woocommerce-checkout #payment {
	background: transparent;
}

body.woocommerce-checkout ul.wc_payment_methods.payment_methods.methods {
	list-style: none;
	margin: 1.5rem 0 0;
	padding: 1rem 0 0;
	border-top: 1px solid var(--gv-gold-15);
}

body.woocommerce-checkout li.wc_payment_method > label {
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--charcoal);
}

body.woocommerce-checkout input.input-radio[name="payment_method"] {
	accent-color: var(--wp--preset--color--gold);
} /* Checkout.jsx:97, accent-palegold — the one place it transfers cleanly */

/* Woo's own `.woocommerce-checkout #payment div.payment_box` carries the
   grey callout background and outranks a plain class selector here — its
   `#payment` ID beats any number of classes regardless of load order.
   Confirmed in-browser: the class-only version below computed a transparent
   background per its own rule, yet the grey box still rendered, because
   Woo's ID-bearing selector was the one actually winning. Re-using
   `#payment` in the selector matches its specificity family and clears it
   on the extra classes.

   B-069: this was originally scoped to `.payment_method_cheque` only —
   `cheque` is the local "Test payment (local only)" stand-in that must
   never reach production, so the one gateway that got styled was the one
   nobody will ever see live, and `bacs` (the gateway that actually ships)
   fell through to Woo's raw lavender callout with the pointer triangle
   still visible. Re-scoped to `div.payment_box` generally — every enabled
   gateway's box, not an enumerated list of slugs — so the next gateway
   (HitPay/FPX per the roadmap) lands inside this rule automatically
   instead of re-opening the same trap. */
body.woocommerce-checkout #payment div.payment_box {
	background: transparent;
	font-size: var(--wp--preset--font-size--x-small);
	color: var(--wp--preset--color--muted-soft);
}

/* Its `::before` triangle pointed at the callout box that no longer exists
   — left in place it is a stray grey wedge with nothing to point to.
   Hidden rather than recoloured, same restraint as the coupon icon above. */
body.woocommerce-checkout #payment div.payment_box::before {
	display: none;
}

body.woocommerce-checkout div.form-row.place-order {
	padding: 0;
	margin-top: 1.5rem;
}

body.woocommerce-checkout .woocommerce-terms-and-conditions-wrapper,
body.woocommerce-checkout .woocommerce-privacy-policy-text {
	font-size: var(--wp--preset--font-size--x-small);
	line-height: 1.6;
	color: var(--wp--preset--color--muted-soft);
} /* nearest confirmed counterpart to Checkout.jsx:98-100 */

/* B-051 / T-013: the terms-and-conditions checkbox itself
   (`.woocommerce-terms-and-conditions-wrapper p.form-row`, WooCommerce's own
   markup — `templates/checkout/terms.php`). Cited directly against
   Checkout.jsx:96-101, the export's own checkbox: `mt-6 flex items-start
   gap-3 cursor-pointer` on the label, `mt-1 h-4 w-4 accent-palegold` on the
   input, `text-xs leading-relaxed text-charcoal/55` on the sentence. */
body.woocommerce-checkout .woocommerce-terms-and-conditions-wrapper p.form-row {
	margin: 1.5rem 0 0;
} /* Checkout.jsx:96, mt-6 */

/* `body.woocommerce-checkout p.form-row > label` (above, Checkout.jsx:12) is
   a DIRECT-CHILD selector and this label IS a direct child of `p.form-row`
   -- the exact "generic rule beats what you meant" shape state/repos/
   goldvion.md warns about, not a hypothetical: without the resets below,
   this checkbox's own sentence inherited that rule's uppercase micro-label
   treatment (confirmed in-browser via getComputedStyle before this fix --
   `text-transform: uppercase`, `letter-spacing: 0.32em`, `font-size` at the
   micro token -- on what must read as the export's plain sentence, not a
   field label). `text-transform`/`letter-spacing` are inherited properties,
   so the reset has to land on both the label and the span: the label rule
   below deliberately does not re-declare `display: block` etc. because it
   already wins on specificity (3 classes vs. the generic rule's 1), but an
   UN-declared property is not a win -- it is inherited from whichever
   ancestor rule still applies. */
body.woocommerce-checkout .woocommerce-terms-and-conditions-wrapper label.woocommerce-form__label-for-checkbox {
	display: flex;
	align-items: flex-start;
	gap: 0.75rem;
	cursor: pointer;
	color: inherit;
	text-transform: none;
	letter-spacing: normal;
} /* Checkout.jsx:96, flex items-start gap-3 cursor-pointer */

body.woocommerce-checkout .woocommerce-terms-and-conditions-wrapper input.woocommerce-form__input-checkbox {
	flex-shrink: 0;
	margin: 0.125rem 0 0;
	width: 1rem;
	height: 1rem;
	accent-color: var(--wp--preset--color--gold);
} /* Checkout.jsx:97, mt-1 h-4 w-4 accent-palegold */

body.woocommerce-checkout .woocommerce-terms-and-conditions-wrapper .woocommerce-terms-and-conditions-checkbox-text {
	font-size: var(--wp--preset--font-size--x-small);
	line-height: 1.6;
	color: var(--wp--preset--color--muted-soft);
	text-transform: none;
	letter-spacing: normal;
	font-weight: 400;
} /* Checkout.jsx:98-100, text-xs leading-relaxed text-charcoal/55 */

/* The "Read the full Terms & Conditions" link added beside the checkbox
   (Goldvion_Policy_Settings::render_terms_link). Matched to the privacy
   sentence directly above it, because the two are the same kind of thing:
   small print the customer may want and must not be made to hunt for. */
body.woocommerce-checkout .gv-terms-link {
	font-size: var(--wp--preset--font-size--x-small);
	line-height: 1.6;
	margin: 0 0 0.75rem;
	text-transform: none;
	letter-spacing: normal;
}

body.woocommerce-checkout .gv-terms-link a {
	color: var(--wp--preset--color--muted);
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

body.woocommerce-checkout .gv-terms-link a:hover,
body.woocommerce-checkout .gv-terms-link a:focus-visible {
	color: var(--wp--preset--color--charcoal);
}

/* The required-field asterisk has no counterpart in the export — she never
   shows one, because her mock has no other optional field on this form to
   distinguish it from. Kept (removing it would understate a field the
   server actually enforces) but muted to the same tone as the sentence
   rather than styled as an alarm. */
body.woocommerce-checkout .woocommerce-terms-and-conditions-wrapper abbr.required {
	color: var(--wp--preset--color--muted-soft);
	text-decoration: none;
}

/* button#place_order: width only, plus disabled state. Everything else
   lands from theme.json — Checkout.jsx:137, w-full + disabled:opacity-40. */
body.woocommerce-checkout button#place_order {
	width: 100%;
}

body.woocommerce-checkout button#place_order:disabled,
body.woocommerce-checkout button#place_order.disabled {
	opacity: 0.4;
	cursor: not-allowed;
}

/* ==========================================================================
   Policy pages — ADR-049
   ==========================================================================

   The client's nine-policy legal pack, published as four pages. This is the
   only long-form prose on the site: ~9,000 words of numbered clauses, nested
   lists and reference tables, none of which the home page's section rhythm was
   built for.

   Scoped on `body.gv-policy-page`, set by goldvion_policy_body_class() from the
   `_gv_policy_page` meta seed.php writes. Scoping on the body rather than on a
   wrapper block is deliberate: the page content stays plain prose blocks in the
   editor, so the client edits paragraphs and tables directly instead of having
   to find her way inside a group she did not create.

   The child theme has no page.html — these render through Twenty Twenty-Five's,
   exactly as /cart/ and /checkout/ already do. Everything below is a narrowing
   of that, never a replacement for it. */

/* Legal text is read, not scanned. 64rem is the site content width and far too
   wide for continuous prose — this pulls the measure back to roughly 70
   characters without touching the layout the rest of the site inherits. */
body.gv-policy-page .entry-content > *,
body.gv-policy-page .wp-block-post-title {
	max-width: 42rem;
	margin-inline: auto;
}

/* The post title block is a sibling of .entry-content, not a child, so it does
   not inherit the measure above -- without naming it explicitly the page title
   sat hard against the gutter while every paragraph beneath it started 150px
   further in. Verified in the browser, not assumed. */
body.gv-policy-page .wp-block-post-title {
	margin-bottom: 2rem;
}

/* Tables carry the ETT-2024 disclosure details (entity, SSM number, address)
   and the cookie inventory. They need the full measure, and their own scroll
   container on a phone — the page body must never scroll horizontally. */
body.gv-policy-page .entry-content > .wp-block-table {
	max-width: 48rem;
	overflow-x: auto;
}

body.gv-policy-page .gv-policy__table table {
	border-collapse: collapse;
	width: 100%;
	font-size: var(--wp--preset--font-size--small);
}

body.gv-policy-page .gv-policy__table th,
body.gv-policy-page .gv-policy__table td {
	border: 1px solid var(--wp--preset--color--hairline);
	padding: 0.625rem 0.875rem;
	text-align: left;
	vertical-align: top;
}

body.gv-policy-page .gv-policy__table th {
	background: var(--wp--preset--color--surface-muted);
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
}

/* A policy title — "3. REFUND & RETURNS POLICY". Set in the display face and
   given real air above it, because on the three merged pages this is the seam
   between two separate legal documents. */
body.gv-policy-page .gv-policy__title {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	font-weight: 400;
	letter-spacing: 0.01em;
	line-height: 1.2;
	margin-top: 3rem;
	margin-bottom: 0.25rem;
	text-transform: none;
}

body.gv-policy-page .entry-content > .gv-policy__title:first-child {
	margin-top: 0;
}

/* "Effective Date: … | Last Updated: … | Version: 1.2" — hers, and worth
   keeping legible rather than hiding: it is how anyone tells which revision of
   a policy they are reading. */
body.gv-policy-page .gv-policy__meta {
	/* `muted`, NOT `muted-dark`: muted-dark is #F9F7F2A6, ivory at 65%, and
	   exists for the charcoal footer. On this ivory page it renders the
	   effective-date line invisible -- which it did, until the browser check
	   caught it. */
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--x-small);
	letter-spacing: 0.02em;
	margin-bottom: 2rem;
}

/* Numbered clause headings (2.13, 4.6 …) and the lettered sub-headings inside
   the Cookie Policy and §3.7. */
body.gv-policy-page .gv-policy__section {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 600;
	letter-spacing: 0;
	line-height: 1.35;
	margin-top: 2.25rem;
	margin-bottom: 0.5rem;
}

body.gv-policy-page .gv-policy__subsection {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--body);
	font-weight: 600;
	margin-top: 1.5rem;
	margin-bottom: 0.5rem;
}

body.gv-policy-page .entry-content p,
body.gv-policy-page .entry-content li {
	line-height: 1.75;
}

body.gv-policy-page .entry-content ul {
	padding-left: 1.25rem;
}

body.gv-policy-page .entry-content li {
	margin-bottom: 0.4rem;
}

/* The seam between two policies on a merged page. */
body.gv-policy-page .gv-policy__rule {
	border: 0;
	border-top: 1px solid var(--wp--preset--color--hairline);
	margin-block: 3.5rem;
	max-width: 42rem;
	opacity: 1;
	width: 100%;
}

/* "On this page" — only rendered on the three pages that carry more than one
   policy, so the reader can get to the Warranty Policy without scrolling
   through the whole of Refund & Returns. */
body.gv-policy-page .gv-policy__toc {
	background: var(--wp--preset--color--surface-muted);
	border: 1px solid var(--wp--preset--color--hairline);
	margin-bottom: 3rem;
	padding: 1.25rem 1.5rem;
}

body.gv-policy-page .gv-policy__toc-title {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--eyebrow);
	font-weight: 600;
	letter-spacing: 0.12em;
	margin: 0 0 0.5rem;
	text-transform: uppercase;
}

body.gv-policy-page .gv-policy__toc ul {
	margin: 0;
	padding-left: 1.1rem;
}

body.gv-policy-page .gv-policy__toc li {
	margin-bottom: 0.2rem;
}

/* Every cross-reference on these pages is an in-page or cross-page anchor, and
   the header is sticky — without this, a jump lands the target heading
   underneath it. ADR-042's rule: anything positioned against the viewport has
   to subtract --gv-header-h. */
body.gv-policy-page :target,
body.gv-policy-page .gv-policy__title,
body.gv-policy-page .gv-policy__section,
body.gv-policy-page .gv-policy__subsection {
	scroll-margin-top: calc(var(--gv-header-h) + 1.5rem);
}

body.gv-policy-page .entry-content a {
	color: inherit;
	text-decoration-color: var(--wp--preset--color--gold);
	text-underline-offset: 0.2em;
}

/* Hover deepens the underline rather than changing the text colour: gold is
   #D4C5A9 on an #F9F7F2 ground, which as link text would be a new WCAG
   failure on a project that already carries three by design (B-006/ADR-006).
   The text stays charcoal at every state. */
body.gv-policy-page .entry-content a:hover,
body.gv-policy-page .entry-content a:focus-visible {
	text-decoration-color: currentColor;
	text-decoration-thickness: 2px;
}

@media (max-width: 600px) {
	body.gv-policy-page .gv-policy__title {
		font-size: var(--wp--preset--font-size--large);
	}

	body.gv-policy-page .gv-policy__table th,
	body.gv-policy-page .gv-policy__table td {
		padding: 0.5rem 0.625rem;
	}

	/* Without a floor, `width: 100%` collapses the three-column cookie tables
	   to min-content on a phone: the Purpose column gets ~40px and every row
	   grows to 300px tall. Give the table a real width and let it scroll
	   inside its own container instead -- the page body still must not scroll
	   sideways, which is what the overflow-x: auto above is for. Measured at
	   390px, not guessed. */
	body.gv-policy-page .gv-policy__table table {
		min-width: 32rem;
	}
}
