/**
 * Builder styles.
 *
 * Colours and fonts come from the theme's tokens (theme.json) rather than
 * being hardcoded here, so the client can restyle the builder in the Site
 * Editor along with everything else. Only layout and state live in this file.
 */

.gv-builder {
	/* Alphas the theme doesn't already declare in style.css's :root (see
	   T-002 spec §0's token vocabulary). Scoped here, not there --
	   style.css belongs to chain B this sprint. */
	--gv-gold-25: color-mix( in srgb, var( --wp--preset--color--gold, #d4c5a9 ) 25%, transparent );
	--gv-gold-60: color-mix( in srgb, var( --wp--preset--color--gold, #d4c5a9 ) 60%, transparent );
	--gv-blush-10: color-mix( in srgb, var( --wp--preset--color--blush, #e8d5d5 ) 10%, transparent );
	--gv-charcoal-35: color-mix( in srgb, var( --wp--preset--color--charcoal, #26241f ) 35%, transparent );
	--gv-charcoal-60: color-mix( in srgb, var( --wp--preset--color--charcoal, #26241f ) 60%, transparent );
	/* Near-opaque, not the 70% `--gv-ivory-70` the unavailable overlay uses:
	   this one sits on top of photographs and has to stay readable there. */
	--gv-ivory-92: color-mix( in srgb, var( --wp--preset--color--ivory, #f9f7f2 ) 92%, transparent );

	max-width: 64rem;
	margin-inline: auto;
	padding-inline: 1.5rem;
	/* The step slide travels 2.5rem horizontally, which is enough to push a
	   full-width panel past the viewport and flash a horizontal scrollbar for
	   half a second on every Continue. `clip` rather than `hidden`: it does
	   not create a scroll container, so it cannot turn into an accidental
	   scrollable region, and it leaves the page's own vertical scrolling
	   alone. */
	overflow-x: clip;
	/* Was 5rem both sides. The builder is a one-step-at-a-time wizard, so
	   every rem above the eyebrow is a rem the customer has to scroll past to
	   reach the only control on screen. 5rem read as "empty band" rather than
	   as breathing room, and on the charms step it pushed the grid below the
	   fold entirely. */
	padding-block: 2rem;
}

/* The theme's 72px page inset above the wizard is dropped in the two builder
   page TEMPLATES, not here -- `themes/goldvion/templates/page-build-gift.html`
   and `page-curated-edit.html`. It has to be, and the reason is worth knowing
   before anyone tries it again from this file: the parent theme emits that
   spacing as INLINE styles on the block (`style="margin-top:var(--wp--preset--
   spacing--60)"`), and an inline style beats any stylesheet rule short of
   `!important`. A perfectly specific `body.gv-builder-page main.wp-block-group`
   rule here matches, computes, and still loses -- silently, which is this
   repo's own documented trap shape one level up. */

/* No-JS baseline (ADR-019) ---------------------------------------------- */

/* Specificity 0,1,0. A <noscript><style> block in builder.php overrides this
   at 0,2,0, later in source, with no !important -- see that template for the
   full repair. */
.gv-panel--later {
	display: none;
}

/* Hidden by default: a Continue button that cannot advance anything, and a
   counter reading "0 / 0" because nothing is counting it, are both worse
   than absent. builder.js removes this the moment it can drive them. */
.gv-js-only {
	display: none;
}

/* Compound selectors: `.gv-nav` (below, Navigation section) sets its own
   `display: flex` at the same 0,1,0 specificity as `.gv-js-only` above and
   is later in source, so it would otherwise win and defeat the hide. Same
   "modifier loses to a later same-specificity rule" trap this repo has
   shipped before -- outrank it explicitly rather than relying on order. */
.gv-nav.gv-js-only {
	display: none;
}

.gv-counter.gv-js-only {
	display: none;
}

.gv-builder--js .gv-nav.gv-js-only {
	display: flex;
}

.gv-builder--js .gv-counter.gv-js-only {
	display: block;
}

/* Inside the charm browse bar the counter is a flex item on a shared row, so
   it must not be forced back to a full-width block by the rule above. */
.gv-builder--js .gv-charmbrowse__bar .gv-counter.gv-js-only {
	display: inline-block;
}

/* Same mechanism again for the charm browse controls (ADR-030). A filter that
   cannot filter and arrows that cannot turn a page are worse than absent, so
   they are hidden by default and only JavaScript reveals them. With scripting
   off nothing is ever hidden either, so the customer gets the full ungated
   grid -- exactly what shipped before pagination existed. Compound selectors
   for the same reason as `.gv-nav.gv-js-only` above: a later same-specificity
   rule would otherwise win on source order. */
.gv-charmbrowse__filter.gv-js-only,
.gv-charmbrowse__arrow.gv-js-only,
.gv-charmbrowse__status.gv-js-only {
	display: none;
}

.gv-builder--js .gv-charmbrowse__filter.gv-js-only {
	display: flex;
}

.gv-builder--js .gv-charmbrowse__arrow.gv-js-only {
	display: flex;
}

/* Only one page to show, so there is nothing to navigate. A separate class
   from `gv-js-only` on purpose -- that one is the server's statement that a
   control needs JavaScript, and it is never toggled at runtime. Specificity
   0,3,0 so it outranks the reveal rule above (0,3,0 as well, but earlier in
   source, so this wins on order -- and it is compound for the same reason
   every modifier in this file is). */
.gv-builder--js .gv-charmbrowse__arrow.gv-charmbrowse__arrow--idle {
	display: none;
}

.gv-builder--js .gv-charmbrowse__status.gv-js-only {
	display: block;
}

/* S-1: the export's eyebrow above the page's own <h1> (the theme's post
   title, untouched by this file). */
/* The theme gives every h1 a generous block margin, which is right for an
   article heading and costs 40px here, between the eyebrow and the progress
   rail that are both part of the same header cluster. */
.gv-builder__title {
	margin: 0 0 0.75rem;
}

.gv-builder__eyebrow {
	margin: 0 0 0.5rem;
	text-align: center;
	font-size: var( --wp--preset--font-size--eyebrow, 0.6875rem );
	letter-spacing: 0.32em;
	text-transform: uppercase;
	color: var( --wp--preset--color--gold, #d4c5a9 );
}

/* Progress ------------------------------------------------------------- */

.gv-steps {
	display: flex;
	flex-wrap: nowrap;
	align-items: flex-start;
	justify-content: center;
	gap: 0.25rem;
	/* 2.5rem -> 1.5rem: part of the vertical-rhythm pass that fits each step
	   on one screen. */
	margin: 0 0 1.5rem;
	padding: 0;
	list-style: none;
}

@media ( min-width: 768px ) {
	.gv-steps {
		gap: 0.75rem;
	}
}

.gv-steps__item {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.375rem;
	font-size: 0.5rem; /* literal -- below the `micro` preset (0.625rem) */
	letter-spacing: 0.32em;
	text-transform: uppercase;
	color: var( --gv-charcoal-35 );
}

@media ( min-width: 768px ) {
	.gv-steps__item {
		font-size: 0.5625rem; /* literal -- also below `micro` */
	}
}

.gv-steps__item[aria-current] {
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* The dot. "Reached" is is-done (already passed) or aria-current (here now);
   anything else is "ahead" and reads lighter. */
.gv-steps__item::before {
	content: "";
	width: 0.5rem;
	height: 0.5rem;
	border-radius: 50%;
	background: var( --gv-gold-25 );
}

@media ( prefers-reduced-motion: no-preference ) {
	.gv-steps__item::before,
	.gv-steps__item {
		transition: color 0.5s, background-color 0.5s;
	}
}

.gv-steps__item[aria-current]::before,
.gv-steps__item.is-done::before {
	background: var( --wp--preset--color--gold, #d4c5a9 );
}

/* The connector. A real in-flow list item between two steps -- not a
   pseudo-element -- so it occupies its own width in the row the way the
   export's flex child does (BuilderProgress.jsx), rather than drawing over
   the neighbouring step at zero width (B-035). `aria-hidden` on the element
   in builder.php keeps a screen reader from announcing it as a progress
   step; the active step still gets its own `aria-current`.

   T-016 finding 4: `margin-top` was `0.1875rem` (3px), carried over
   verbatim from the old absolutely-positioned rule's `top` -- our own dot
   arithmetic (half the 0.5rem dot's mount), not BuilderProgress.jsx.
   `mt-1` there is `0.25rem` (4px). One pixel, filed because it is B-035's
   own root-cause shape: a number taken from our prior artifact instead of
   the export, on the very mechanism T-012 fixed against that file. */
.gv-steps__rule {
	flex-shrink: 0; /* Otherwise the row's default flex-shrink silently
	   compresses this to fit a narrow viewport before the labels give up
	   any room -- invisible before this fix, since the old pseudo-element
	   never took part in flex sizing at all. */
	width: 1.5rem;
	height: 1px;
	margin-top: 0.25rem;
	background: var( --gv-gold-15 );
}

@media ( prefers-reduced-motion: no-preference ) {
	.gv-steps__rule {
		transition: background-color 0.5s;
	}
}

@media ( min-width: 768px ) {
	.gv-steps__rule {
		width: 3rem;
	}
}

/* Colouring follows the customer with no builder.js change: builder.js
   already toggles `is-done` on the item strictly before the current one
   (`i < index`), matching the export's own connector condition
   (`i < current`), so an adjacent-sibling selector reaches the rule that
   sits right after it. */
.gv-steps__item.is-done + .gv-steps__rule {
	background: var( --gv-gold-60 );
}

/* Panels --------------------------------------------------------------- */

.gv-panel {
	border: 0;
	margin: 0;
	padding: 0;
}

.gv-panel__title {
	display: block;
	width: 100%;
	margin-bottom: 1.25rem;
	font-family: var( --wp--preset--font-family--display, serif );
	font-size: var( --wp--preset--font-size--xx-large, 1.875rem );
	font-weight: 500; /* the site's heading default (theme.json elements.heading) — a <legend> isn't a heading element, so it doesn't inherit that automatically */
	line-height: 1.1;
	text-align: center;
}

@media ( min-width: 768px ) {
	.gv-panel__title {
		font-size: var( --wp--preset--font-size--display, 2.25rem );
	}
}

/* S-4: the charms step's subtitle ("You pick your charms.") sits directly
   under its title, so the title's usual 2rem margin-bottom is too wide when
   a subtitle follows it -- reduced via the sibling relation rather than a
   new panel-specific class, since S-3's panel-title treatment is shared
   across every step and only this one step carries a subtitle. */
.gv-panel__title:has( + .gv-panel__subtitle ) {
	margin-bottom: 0.5rem;
}

.gv-panel__subtitle {
	margin: 0 0 1.25rem;
	text-align: center;
	font-size: var( --wp--preset--font-size--small, 0.875rem );
	color: var( --wp--preset--color--muted-medium, #26241f80 );
}

.gv-panel__note {
	margin: 0 0 1.25rem;
	text-align: center;
	font-size: var( --wp--preset--font-size--small, 0.875rem );
	color: var( --wp--preset--color--muted-medium, #26241f80 );
}

/* Choice grids --------------------------------------------------------- */

.gv-grid {
	display: grid;
	gap: 1rem;
	margin-block: 1.5rem;
}

/* S-9/S-10: auto-fit/auto-fill both collapse unpredictably at intermediate
   widths, which the export never shows -- explicit column counts per step,
   same reasoning as C-10 below for the charm grid. */
.gv-grid--2 { grid-template-columns: 1fr; }
.gv-grid--3 { grid-template-columns: 1fr; }

@media ( min-width: 768px ) {
	.gv-grid--2 { grid-template-columns: repeat( 2, 1fr ); }
	.gv-grid--3 { grid-template-columns: repeat( 3, 1fr ); }
}

.gv-card {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 1rem;
	padding: 2rem;
	border: 2px solid var( --gv-gold-15 );
	border-radius: var( --wp--custom--radius--card, 0.5rem );
	text-align: center;
	cursor: pointer;
	transition: border-color 0.15s ease, background-color 0.15s ease;
}

/* T-027: `.gv-card`'s own `display: flex` above sits at the UA stylesheet's
   `[hidden] { display: none }` specificity (0,1,0 vs 0,1,0) and, being an
   author rule, always wins regardless of source order -- so
   `card.hidden = true` (builder.js, tone step) rendered nothing visibly
   different. A tone that is not offered for the chosen base must vanish
   entirely -- it is not something she sells at all -- which is a different
   claim from a sold-out pair, which stays visible, disabled and labelled
   "Currently Unavailable" via `.gv-card__state` + `:has(input:disabled)`
   below. Scoped to `.gv-card` on purpose: this project carries no global
   `[hidden] { display: none !important }` (see the box-sizing-reset
   precedent in state/repos/goldvion.md) precisely because a blanket rule is
   unsafe here -- this compound selector (0,2,0) only outranks this one
   component's own conflicting declaration. */
.gv-card[hidden] {
	display: none;
}

.gv-card:hover {
	border-color: var( --gv-gold-40 );
}

/* The native control stays in the accessibility tree but out of the way. */
.gv-card input {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset( 50% );
	white-space: nowrap;
}

.gv-card:has( input:checked ) {
	border-color: var( --wp--preset--color--gold, #d4c5a9 );
	background: var( --gv-blush-10 );
}

/* Keyboard focus must remain obvious — the input itself is visually hidden. */
.gv-card:has( input:focus-visible ) {
	outline: 2px solid var( --wp--preset--color--charcoal, #26241f );
	outline-offset: 2px;
}

/* T-002 spec §8b / B-025: an out-of-stock tier tile. `:has()` is safe here
   (unlike on `.gv-charm`) because the tier tile keeps exactly one input --
   see ADR-019's composition note. Same M-11 treatment as the charm modal's
   "Currently Unavailable" and the home tier card's `.gv-tier__state`.

   §8b calls for a whole-tile fade (unlike §8a's media-only fade on the home
   card), but fading `.gv-card__state` along with everything else composites
   group opacity with the text's own --gv-fg-30 (charcoal 30%): 0.4 x 0.3 =
   0.12 effective alpha, making the one element whose job is to say the tile
   is unavailable the least legible thing on it. Both sibling
   implementations (the home tier card's `.gv-tier__media`-only fade, the
   charm tile's sibling `.gv-charm__state`) keep their state text outside the
   faded subtree -- fade every other child here instead of the card as a
   whole, so the text is excluded but the rest of the tile still reads as
   whole-tile-faded per spec. */
.gv-card:has( input:disabled ) > *:not( .gv-card__state ) {
	opacity: 0.4;
}

.gv-card:has( input:disabled ) {
	cursor: not-allowed;
}

.gv-card__state {
	margin: 0;
	font-size: var( --wp--preset--font-size--micro, 0.625rem );
	letter-spacing: 0.32em;
	text-transform: uppercase;
	color: var( --gv-fg-30 );
}

.gv-card__title {
	font-family: var( --wp--preset--font-family--display, serif );
	font-size: var( --wp--preset--font-size--x-large, 1.5rem );
}

.gv-card__note {
	font-size: var( --wp--preset--font-size--x-small, 0.75rem );
	color: var( --wp--preset--color--muted-medium, #26241f80 );
}

/* T-015/Q-002: the base card's own image, export's `h-40 w-full overflow-
   hidden rounded` (BuildGift.jsx:110-113) — a definite pixel height, not an
   aspect-ratio, which is deliberate: .gv-card is a column flexbox with
   align-items: center, so a child gets its own intrinsic width unless told
   otherwise -- the same "flex item with no explicit width" trap
   .gv-tier__media's own history already documents (style.css), just a
   different flex property causing it. align-self: stretch is this card's
   fix for that, same job width: 100% does over there. Present only when a
   crop exists (builder.php omits the element entirely otherwise), so this
   never becomes a placeholder box.

   T-016 finding 2: radius was `var(--wp--custom--radius--card, 0.5rem)` —
   8px — where the export's `rounded` (BuildGift.jsx:110) resolves to this
   project's `base` token, 4px (this project's Tailwind `--radius` is
   `0.25rem`, so `rounded`/`rounded-lg` both land on `base`, not `card`).
   style.css has already made this exact correction three times (search
   "radius corrected to the export's rounded-lg"); this is the fourth.

   T-016 finding 5: `flex-shrink: 0` added. `.gv-card` is a column flexbox,
   so this element's main size is `height: 160px`, not its cross size --
   without this it is a fixed-main-size flex child with the default
   flex-shrink: 1, the exact shape `state/repos/goldvion.md` documents this
   sprint (T-012), rotated ninety degrees. Latent today only because
   `.gv-card` has no bounded height (both base cards share content, so the
   grid row stretches to fit); costs nothing to fix now, before anything
   ever constrains the card's height.

   T-016 finding 3: `margin-bottom: 0.5rem` added. The export puts `mb-2`
   (8px) on this element on top of `OptionCard`'s `gap-4` (16px,
   BuildGift.jsx:28/110) — 24px from photograph to title. `.gv-card`'s own
   `gap: 1rem` (16px) alone gave 16px; `.gv-card__select` already gets this
   right (`margin-top: 0.5rem` + the same `gap` = 24px, matching the
   export's `mt-2` + `gap-4`) — this brings the media box in line with it. */
.gv-card__media {
	display: block;
	align-self: stretch;
	flex-shrink: 0;
	height: 160px;
	margin-bottom: 0.5rem;
	overflow: hidden;
	border-radius: var( --wp--custom--radius--base, 0.25rem );
}

.gv-card__media img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* T-014/B-034: "Select →", export's mt-2 text-[10px] tracking-luxe uppercase
   text-palegold on the base cards (BuildGift.jsx:110-115). */
.gv-card__select {
	margin-top: 0.5rem;
	font-size: var( --wp--preset--font-size--micro, 0.625rem );
	letter-spacing: 0.32em;
	text-transform: uppercase;
	color: var( --wp--preset--color--gold, #d4c5a9 );
}

.gv-card__price {
	margin-top: 0.25rem;
	font-family: var( --wp--preset--font-family--display, serif );
	font-size: var( --wp--preset--font-size--large, 1.25rem );
}

.gv-swatch {
	width: 2.5rem;
	height: 2.5rem;
	border-radius: 50%;
}

.gv-counter {
	/* Margin lives on `.gv-charmbrowse__bar` now that the count shares a row
	   with the filter. */
	margin: 0;
	text-align: center;
	font-family: var( --wp--preset--font-family--display, serif );
	font-size: var( --wp--preset--font-size--large, 1.25rem );
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* Charm tiles ------------------------------------------------------------
   Circular, per T-002 spec §4 (C-1..C-12). Split out of the shared
   .gv-card treatment above: the export's charm cell is a circle, not a
   rounded rectangle, and the two now diverge on every value. The selection
   model is ADR-019's flat-sibling composition -- see builder.php -- so
   every rule below keys off `.gv-charm-select`, a preceding sibling of
   `.gv-charm`, rather than `:has()` (which would match either checkbox
   once T-005 adds the open one to the same cell) or DOM nesting. */

.gv-charm-select,
.gv-charm-open {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset( 50% );
	white-space: nowrap;
}

.gv-charm-cell {
	position: relative;
	list-style: none;
}

.gv-charm {
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 0.25rem;
	aspect-ratio: 1;
	padding: 0.5rem;
	border: 2px solid var( --gv-gold-15 );
	border-radius: 50%;
	/* Forces the aspect-ratio to actually hold. Without this, a tile whose
	   content (a real uploaded photo, wider than the 1.5rem glyph the type
	   scale assumes) needs more height than the ratio allows renders taller
	   than it is wide instead — aspect-ratio is a preference the box grows
	   past when overflow is visible, not a clamp. The badge and the
	   unavailable overlay live on `.gv-charm-cell` instead, precisely so
	   this doesn't clip them. */
	overflow: hidden;
	text-align: center;
	cursor: pointer;
	transition: border-color 0.15s ease, background-color 0.15s ease;
}

.gv-charm-select:not( :disabled ) ~ .gv-charm:hover {
	border-color: var( --gv-gold-40 );
}

.gv-charm-select:checked ~ .gv-charm {
	border-color: var( --wp--preset--color--gold, #d4c5a9 );
	background: color-mix( in srgb, var( --wp--preset--color--blush, #e8d5d5 ) 20%, transparent );
}

/* Keyboard focus must remain obvious — the input itself is visually hidden.
   Two separate hidden checkboxes share this tile (T-009 fix: `.gv-charm-open`
   now precedes `.gv-charm-select` in DOM/tab order, see builder.php), so
   each needs its own ring, and the two must look DIFFERENT from each other
   -- a keyboard user has to be able to tell which of the two stops they are
   on ("about to view detail" vs. "about to select"), not just that focus is
   somewhere on the tile. Replaces `.gv-charm:has(input:focus-visible)`,
   which would fire identically for either checkbox. */
.gv-charm-open:focus-visible ~ .gv-charm {
	outline: 2px dashed var( --wp--preset--color--gold, #d4c5a9 );
	outline-offset: 4px;
}

.gv-charm-select:focus-visible ~ .gv-charm {
	outline: 2px solid var( --wp--preset--color--charcoal, #26241f );
	outline-offset: 2px;
}

/* Compound selectors so these definitively outrank the two orphan builder
   rules stranded in style.css (chain B's file, not editable here) rather
   than depending only on the builder.css-after-style.css load order T-001
   measured -- this repo has shipped the "modifier loses to a same-
   specificity generic rule" trap three times already. */
.gv-charm-cell .gv-charm__icon {
	display: flex;
	align-items: center;
	justify-content: center;
	aspect-ratio: 1;
	font-size: var( --wp--preset--font-size--x-large, 1.5rem );
	line-height: 1;
}

@media ( min-width: 768px ) {
	.gv-charm-cell .gv-charm__icon {
		font-size: var( --wp--preset--font-size--xx-large, 1.875rem );
	}
}

/* A real photograph FILLS the circle, edge to edge.
   ------------------------------------------------------------------------
   Two things kept it from doing so, and both had to go:

     `object-fit: contain` letterboxed the photo inside its box, so a frame
     that is not square left bars down the sides.

     `.gv-charm` carries `padding: 0.5rem`, which is right for centring an
     emoji glyph and wrong for a photograph — it inset the image from the
     circle's edge, and because the image box was then a square sitting
     inside the circle, the tile read as a rounded SQUARE rather than as a
     round one. The clip only nipped its corners.

   Absolute positioning takes the image out of the padded flow and `inset: 0`
   fills the padding box of `.gv-charm` (its nearest positioned ancestor),
   which is the full circle inside the border. `cover` then crops rather than
   letterboxes, and `.gv-charm`'s own `border-radius: 50%` + `overflow: hidden`
   does the rounding.

   SCOPED TO `.gv-charm__photo`, NOT TO `.gv-charm__icon img`. WordPress
   rewrites every emoji on the page into `<img class="emoji">` served from
   s.w.org, so a bare `img` selector here matches the thirteen charms that are
   still emoji stand-ins too. Their width and height are pinned to 1em by
   WordPress's own `!important` rules, which this cannot override -- but
   `position` is NOT pinned, so it leaked through and parked every emoji, at
   30px, in the top-left corner of its circle. Caught by looking at the page
   rather than at the measurement, which said the photograph was filling
   correctly and was right about that. */

/* COMPOUND, at 0,2,0. `style.css:252` carries an orphan
   `.gv-charm__icon img { object-fit: contain }` at that same specificity, and
   a bare `.gv-charm__photo` (0,1,0) loses to it — silently, because the two
   rules agree on `width` and `height` and differ only on `object-fit`. The box
   filled the circle while the picture inside it stayed letterboxed, so
   measuring the element said "fills: true" and was right about the box and
   wrong about the paint. builder.css is enqueued after style.css, so at equal
   specificity this wins on source order.

   known-issues-and-gotchas.md already says builder.css must use compound
   selectors to outrank those three orphan rules. This is what it means. */
.gv-charm__icon .gv-charm__photo {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* The name straddles the circle's bottom edge rather than sitting inside it.
   ------------------------------------------------------------------------
   `.gv-charm` has `overflow: hidden` so it stays a true circle when a real
   photograph is wider than the glyph the type scale assumes — and that same
   clip removed the name entirely on the one charm that has real photography.
   Every emoji tile looked fine, which is why it went unnoticed.

   So the name is now a sibling of the tile, positioned on the cell, and
   `translateY(50%)` centres it ON the border: half over the circle, half
   below it. Its own ground covers the border behind it, which is what makes
   it legible over a photograph as well as over the flat tiles.

   `pointer-events: none` matters — the cell is not the label, so without it
   this would sit between the customer and the tile she is trying to open. */
.gv-charm__name {
	position: absolute;
	left: 50%;
	bottom: 0;
	transform: translate( -50%, 50% );
	max-width: 100%;
	padding: 0.125rem 0.375rem;
	border-radius: 1rem;
	background: var( --gv-ivory-92 );
	font-size: 0.5rem; /* literal — no preset is smaller than `micro` */
	line-height: 1.4;
	text-align: center;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	pointer-events: none;
	/* 60% charcoal, not the `muted-medium` preset this used to carry.
	   Measured against the ivory ground: 3.10:1 before, 4.03:1 now — the
	   ~+30% the change asked for. `muted-soft` (+14%) undershot and `muted`
	   (+81%) overshot, so this is a local token rather than a palette one;
	   it is not worth a new site-wide preset for one label. */
	color: var( --gv-charcoal-60 );
}

@media ( min-width: 768px ) {
	.gv-charm__name {
		font-size: var( --wp--preset--font-size--micro, 0.625rem );
	}
}

.gv-charm__check {
	display: none;
	align-items: center;
	justify-content: center;
	position: absolute;
	top: -0.25rem;
	right: -0.25rem;
	width: 1.25rem;
	height: 1.25rem;
	border-radius: 50%;
	background: var( --wp--preset--color--gold, #d4c5a9 );
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* `.gv-charm__check` is a sibling of `.gv-charm` (both children of
   `.gv-charm-cell`), not its descendant — see builder.php. */
.gv-charm-select:checked ~ .gv-charm__check {
	display: flex;
}

/* Compound override of style.css's orphan `.gv-charm[aria-disabled="true"]`
   (same instruction as .gv-charm__icon above) -- kept as belt-and-braces
   alongside the sibling-combinator rule below, which is the one ADR-019
   actually keys the state off. */
.gv-charm-cell .gv-charm[aria-disabled="true"] {
	opacity: 0.4;
	cursor: not-allowed;
}

.gv-charm-select:disabled ~ .gv-charm {
	opacity: 0.4;
	cursor: not-allowed;
}

/* An unavailable charm must not rely on opacity alone to communicate. */
/* pointer-events: none — this is a sibling overlay of `.gv-charm` (the tile
   label), not its descendant (see builder.php), and sits on top of it in
   paint order (position:absolute; inset:0). Without this it intercepts every
   click meant for the label, including the open checkbox's `for=`
   association, so an unavailable charm's modal could never be opened (T-008
   QA finding). The select checkbox stays genuinely disabled below, so this
   does not make the tile choosable — it only lets the click reach the label
   that opens the read-only modal. */
.gv-charm__state {
	display: flex;
	align-items: center;
	justify-content: center;
	position: absolute;
	inset: 0;
	border-radius: 50%;
	background: var( --gv-ivory-70 );
	font-size: 0.4375rem; /* literal — no preset this small */
	letter-spacing: 0.32em;
	text-transform: uppercase;
	color: var( --wp--preset--color--muted-faint, #26241f66 );
	pointer-events: none;
}

.gv-grid--charms {
	/* Declared as custom properties because the fit calculation below needs
	   the column count, the row count and the gap as NUMBERS, not just as a
	   rendered result. Twelve per page is fixed, so rows follow columns:
	   3 -> 4 rows, 4 -> 3, 6 -> 2. */
	--gv-cols: 3;
	--gv-rows: 4;
	--gv-gap: 0.75rem;
	/* Rows need more room than columns: the charm name overlays the bottom of
	   each circle and hangs about half its height into the space below, which
	   left only 7px of clear air before the next row's tile. Derived from
	   `--gv-gap` rather than hard-coded so it tracks every breakpoint. */
	--gv-row-gap: calc( var( --gv-gap ) + 0.625rem );
	/* Everything on the charms step that is NOT the grid: site header, page
	   inset, builder padding, eyebrow, title, progress rail, panel title and
	   subtitle, the count/filter bar, the page readout and the nav. Measured
	   against the rendered page. This is only ever an ESTIMATE and only ever
	   matters without JavaScript — `measureCharmChrome()` in builder.js
	   replaces it with the real measurement on the charms step and on resize,
	   precisely so this number rotting is a cosmetic problem rather than a
	   layout one. Erring large is the safe direction: it makes tiles smaller,
	   never taller than the space. */
	--gv-chrome: 30rem;

	list-style: none;
	padding: 0;
	grid-template-columns: repeat( var( --gv-cols ), 1fr );
	/* NOTE: the gap is NOT set here. `.gv-grid` carries its own `gap` and the
	   mobile block later in this file overrides it — same specificity (0,1,0),
	   later in source, so it wins and silently flattened the row gap to the
	   column gap. It is set on the 0,2,0 compound selector below instead. */
}

@media ( min-width: 640px ) {
	.gv-grid--charms {
		--gv-cols: 4;
		--gv-rows: 3;
		--gv-chrome: 31rem;
	}
}

/* Six, not five (ADR-030). The page size is 12 at every breakpoint, so the
   column count has to divide it: 3 -> four rows, 4 -> three rows, 6 -> two.
   Five would leave every page ragged at 5 + 5 + 2, not just the last one.
   A deliberate divergence from the client's export, recorded in ADR-030
   rather than in a design-drift file -- there is no builder drift file. */
@media ( min-width: 768px ) {
	.gv-grid--charms {
		--gv-cols: 6;
		--gv-rows: 2;
		--gv-gap: 1rem;
		--gv-chrome: 33rem;
	}
}

/* Charm browse -------------------------------------------------------------
   ADR-030. Arrows flank the whole grid; the filter sits above it and the page
   readout below. Nothing here is reachable without JavaScript -- see the
   `.gv-js-only` block near the top of this file. */

/* Count and filter on one row. `space-between` at width, collapsing to a
   centred stack when there is not room for both — the count is the important
   half, so it leads. */
.gv-charmbrowse__bar {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: center;
	gap: 0.5rem 1.5rem;
	margin-bottom: 1rem;
}

.gv-charmbrowse__filter {
	align-items: center;
	gap: 0.5rem;
	justify-content: center;
	margin: 0;
	font-size: 0.6875rem;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var( --wp--preset--color--muted, #6b6459 );
}

.gv-charmbrowse__filter select {
	font-family: inherit;
	font-size: 0.75rem;
	letter-spacing: 0.04em;
	text-transform: none;
	padding: 0.35rem 0.5rem;
	border: 1px solid var( --gv-gold-15, #d4c5a926 );
	border-radius: 2px;
	background: transparent;
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* FIXED outer tracks and EXPLICIT placement, and both halves are load-bearing.
   This was `auto 1fr auto` with no placement, and it resized the charm circles
   whenever the filter changed — 133px under "All charms", 53px under a
   category that fits on one page.

   Two things go wrong at once when an arrow is hidden, because `display: none`
   removes an item from grid placement entirely rather than just painting
   nothing:

     1. The `<ul>` slides up into track 1. With `auto` that track is sized by
        its own content instead of filling the row, so six columns of `1fr`
        divide a much smaller box and every circle shrinks.
     2. The trailing `auto` track collapses to zero, moving the right edge.

   Fixed outer tracks fix (2); explicit `grid-column` fixes (1). Neither alone
   is enough. Keep both, and do not "simplify" these back to auto placement —
   the failure is silent, only appears once a category fits on one page, and
   looks like a design choice rather than a bug. */
.gv-charmbrowse__row {
	display: grid;
	grid-template-columns: 1.5rem 1fr 1.5rem;
	align-items: center;
	gap: 0.25rem;
}

.gv-charmbrowse__row > [data-gv-charm-prev] {
	grid-column: 1;
}

/* THE FIT RULE. Size the circles from the vertical space actually left over,
   rather than from width alone, so a full page of twelve lands on one screen
   instead of below the fold.

   A charm tile is `aspect-ratio: 1`, so its height follows its width, and its
   width is the grid column width. Work backwards: the rows have
   `100svh - <everything else on this step>` to share, minus the gaps between
   them; that quotient is the tallest a tile may be; six of those plus five
   gaps is the widest the grid may be.

   `svh`, not `vh`: on mobile `vh` is the tallest the viewport ever gets, with
   the browser chrome retracted, so a `vh` budget is wrong by the height of
   the address bar exactly when space is tightest.

   `clamp()` floors it at 2.75rem so tiles stay tappable on a short phone, and
   caps it at 8.25rem so a tall desktop window does not inflate them into
   dinner plates. Between those it is fluid, which is why there is no
   breakpoint list here. */
.gv-charmbrowse__row > .gv-grid--charms {
	--gv-cell: clamp(
		2.75rem,
		( 100svh - var( --gv-chrome ) - ( var( --gv-rows ) - 1 ) * var( --gv-row-gap ) ) / var( --gv-rows ),
		8.25rem
	);

	grid-column: 2;
	/* Rows get more air than columns, because the charm name overlays the
	   bottom of each circle and hangs into the space below it. Declared here,
	   on the 0,2,0 compound selector, rather than on `.gv-grid--charms` (0,1,0)
	   — `.gv-grid`'s own `gap`, and the mobile override of it, both come later
	   in this file and would otherwise win on source order. That is the
	   "modifier loses to a later same-specificity rule" trap this repo has now
	   shipped three times; see known-issues-and-gotchas.md. */
	gap: var( --gv-row-gap ) var( --gv-gap );

	/* An explicit WIDTH, not a max-width. `margin-inline: auto` makes this a
	   shrink-to-fit grid item, and a shrink-to-fit grid sizes to its content —
	   so a page holding two charms rendered two small circles while a page of
	   twelve rendered twelve large ones.

	   It was invisible until the charm name moved out of the tile: while the
	   name was in-flow inside the label, a cell's min-content width was the
	   width of "Birth Flower", which was enough to push the grid to its cap on
	   any page. Absolutely positioning the name removed that accidental floor
	   and the collapse appeared. Pinning the width makes the circles the same
	   size on every page and under every filter, whatever is on them. */
	width: calc( var( --gv-cols ) * var( --gv-cell ) + ( var( --gv-cols ) - 1 ) * var( --gv-gap ) );
	max-width: 100%;
	margin-inline: auto;
}

.gv-charmbrowse__row > [data-gv-charm-next] {
	grid-column: 3;
}

/* Modelled on `.gv-hero__arrow` (theme style.css) so the affordance matches
   the carousel the client's own design already teaches on the home page. */
.gv-charmbrowse__arrow {
	align-items: center;
	justify-content: center;
	width: 24px;
	height: 24px;
	padding: 0;
	border: 0;
	background: none;
	line-height: 0;
	cursor: pointer;
	color: var( --gv-fg-40, #26241f66 );
	transition: color 0.15s ease, opacity 0.15s ease;
}

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

.gv-charmbrowse__arrow:hover {
	color: var( --wp--preset--color--charcoal, #26241f );
}

.gv-charmbrowse__arrow:disabled {
	opacity: 0.25;
	cursor: default;
	color: var( --gv-fg-40, #26241f66 );
}

.gv-charmbrowse__status {
	margin: 0.625rem 0 0;
	text-align: center;
	font-size: 0.625rem;
	letter-spacing: 0.22em;
	text-transform: uppercase;
	color: var( --wp--preset--color--muted-faint, #26241f66 );
}

/* Compound selector at 0,2,0 so it outranks `.gv-charm-cell` (0,1,0, written
   earlier). This repo has shipped the "modifier silently loses to a later
   same-specificity rule" bug twice -- see known-issues-and-gotchas.md. The
   house fix is a compound selector, never `!important`; there is exactly one
   in this codebase and it stays that way.

   `display: none` and NOT unchecking the box: a hidden checked checkbox is
   still submitted, which is what lets a selection survive a page turn. */
.gv-grid--charms .gv-charm-cell--off {
	display: none;
}

/* Charm modal -------------------------------------------------------------
   T-002 spec §2/§3, ADR-019. The scrim and the modal are flat siblings of
   `.gv-charm` inside `.gv-charm-cell` (see builder.php), keyed off
   `.gv-charm-open`; the "Choose This Charm" control inside the modal keys
   off the OTHER checkbox, `.gv-charm-select`, so a click on it changes
   selection without also closing the tile label -- labels don't nest, and
   nesting one inside the other would fire both on a single click. Same
   z-index/centring/transition shape as `.gv-inside__panel` (style.css:1521,
   the tier "What's Inside" modal) -- proven live in this codebase already,
   which is what the composition check in ADR-019 rests on. */

/* Scroll-lock, CSS-only -- T-009 finding 3. Same `:has()` mechanism already
   proven for the header drawer (style.css's
   `html:has(.gv-drawer-toggle:checked)`), not a new dependency, and it keeps
   the lock working with no JavaScript at all rather than making it a
   scripting feature. Target both `html` and `body`: the drawer's own QA
   found the real scrolling element varies by doctype
   (`documentElement.scrollTop` moves while `body.scrollTop` stays 0 here),
   so both are cheap insurance. Lives here, not in style.css -- chain A does
   not edit that file this sprint. */
html:has( .gv-charm-open:checked ),
body:has( .gv-charm-open:checked ) {
	overflow: hidden;
}

.gv-charm-modal__scrim {
	position: fixed;
	inset: 0;
	z-index: 64;
	display: block;
	background: var( --gv-fg-40 );
	backdrop-filter: blur( 4px );
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
	transition: opacity 0.3s, visibility 0s linear 0.3s;
}

.gv-charm-open:checked ~ .gv-charm-modal__scrim {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transition: opacity 0.3s, visibility 0s linear 0s;
}

/* Centred via inset:0 + margin:auto, matching `.gv-inside__panel` -- not
   the export's translate pair, so the transform-free element (while
   closed, `transform` only applies once open, see below) doesn't create a
   containing block for anything inside it. z-index 65, one above the
   scrim's 64, both well above the header drawer's 60 (measured live on
   /build-gift/, see task note) and the sticky header's own 50.
   T-011 (S-005): `height: fit-content` added. `inset: 0` gives both `top`
   and `bottom` non-auto, so with `height: auto` the box was solving to the
   full viewport height (CSS 2.1 10.6.4) before `max-height: 88vh` clamped
   it -- 792px against 285px of real content at 1280. `fit-content` sizes
   to content instead and `margin: auto` still centres it. */
.gv-charm-modal {
	position: fixed;
	inset: 0;
	z-index: 65;
	margin: auto;
	width: 90vw;
	max-width: 32rem;
	height: fit-content;
	max-height: 88vh;
	overflow-y: auto;
	overflow-x: hidden;
	background: var( --wp--preset--color--ivory, #f9f7f2 );
	border-radius: var( --wp--custom--radius--base, 0.25rem );
	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-charm-open:checked ~ .gv-charm-modal {
	visibility: visible;
	opacity: 1;
	transform: none;
	transition: opacity 0.3s, transform 0.3s, visibility 0s linear 0s;
}

@media ( prefers-reduced-motion: reduce ) {
	.gv-charm-modal__scrim,
	.gv-charm-modal {
		transition-duration: 0.01ms;
	}
}

.gv-charm-modal__body {
	display: grid;
	grid-template-columns: 1fr;
}

@media ( min-width: 640px ) {
	.gv-charm-modal__body {
		grid-template-columns: 1fr 1fr;
	}
}

.gv-charm-modal__media {
	position: relative;
	aspect-ratio: 1;
	overflow: hidden;
	background: color-mix( in srgb, var( --wp--preset--color--blush, #e8d5d5 ) 20%, transparent );
	display: flex;
	align-items: center;
	justify-content: center;
}

.gv-charm-modal__img {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.gv-charm-modal__symbol {
	font-size: var( --wp--preset--font-size--display-lg, 3rem );
	opacity: 0.8;
}

.gv-charm-modal__content {
	display: flex;
	flex-direction: column;
	justify-content: center;
	padding: 2rem;
}

.gv-charm-modal__name {
	margin: 0;
	font-family: var( --wp--preset--font-family--display, serif );
	font-size: var( --wp--preset--font-size--xx-large, 1.875rem );
	font-weight: 500;
	color: var( --wp--preset--color--charcoal, #26241f );
}

.gv-charm-modal__desc {
	margin-top: 1rem;
	font-size: var( --wp--preset--font-size--small, 0.875rem );
	line-height: 1.625;
	color: var( --gv-fg-60 );
}

.gv-charm-modal__choose {
	display: block;
	margin-top: 1.5rem;
	padding-block: 12px;
	border-radius: var( --wp--custom--radius--pill, 999px );
	font-size: var( --wp--preset--font-size--micro, 0.625rem );
	letter-spacing: 0.32em;
	text-transform: uppercase;
	text-align: center;
	cursor: pointer;
	background: var( --wp--preset--color--charcoal, #26241f );
	color: var( --wp--preset--color--ivory, #f9f7f2 );
	border: 1px solid var( --wp--preset--color--charcoal, #26241f );
	transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.gv-charm-modal__choose:hover {
	background: var( --wp--preset--color--gold, #d4c5a9 );
	border-color: var( --wp--preset--color--gold, #d4c5a9 );
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* Selected: border only, no ground -- M-9. Keyed off `.gv-charm-select`,
   which precedes `.gv-charm-modal` in the same cell (see builder.php),
   exactly as the tile's own selected state is. */
.gv-charm-select:checked ~ .gv-charm-modal .gv-charm-modal__choose {
	background: transparent;
	border-color: var( --wp--preset--color--gold, #d4c5a9 );
	color: var( --wp--preset--color--gold, #d4c5a9 );
}

.gv-charm-select:checked ~ .gv-charm-modal .gv-charm-modal__choose-off,
.gv-charm-select:not( :checked ) ~ .gv-charm-modal .gv-charm-modal__choose-on {
	display: none;
}

/* Keyboard focus, same pattern as the tile and the base/tone/tier cards --
   the input driving this label is visually hidden elsewhere in the cell. */
.gv-charm-select:focus-visible ~ .gv-charm-modal .gv-charm-modal__choose {
	outline: 2px solid var( --wp--preset--color--charcoal, #26241f );
	outline-offset: 2px;
}

/* M-11: literal, no preset matches -- `--gv-fg-30` (charcoal 30%) already
   exists in style.css and is exactly this value; reused, not redeclared. */
.gv-charm-modal__state {
	margin-top: 1.5rem;
	font-size: 0.625rem;
	letter-spacing: 0.32em;
	text-transform: uppercase;
	color: var( --gv-fg-30 );
}

/* M-12. The export's own X sits ivory-on-ivory except where it overlays the
   photo -- our media column is on the left at >=640px and stacked on top
   below it, so the X always lands on the image at both widths. Keeping the
   export's colour there (rather than switching to a dark icon) but adding
   a drop-shadow so it survives a pale photograph -- a deliberate deviation
   from the export, recorded here and in the task note. */
.gv-charm-modal__close {
	position: absolute;
	right: 1rem;
	top: 1rem;
	z-index: 1;
	display: flex;
	cursor: pointer;
	color: var( --gv-ivory-60 );
	filter: drop-shadow( 0 1px 2px rgb( 0 0 0 / 0.35 ) );
	transition: color 0.15s ease;
}

.gv-charm-modal__close:hover {
	color: var( --wp--preset--color--ivory, #f9f7f2 );
}

.gv-charm-open:focus-visible ~ .gv-charm-modal .gv-charm-modal__close {
	outline: 2px solid var( --wp--preset--color--charcoal, #26241f );
	outline-offset: 2px;
}

/* Review ----------------------------------------------------------------
   S-13/S-14: the export's summary card has its own heading and a total
   row, neither of which existed. The three values below (border colour,
   padding, heading size) were the wrong tokens entirely -- see the delta
   table -- not a step change from what was already close. */

.gv-summary {
	max-width: 32rem;
	/* 2rem -> 1.25rem / 1.5rem. The review step is the tallest in charm mode
	   once a Royal order lists seven charms, and it was the last panel still
	   running past the fold on a 768x1024 tablet. */
	margin: 0 auto 1.25rem;
	padding: 1.5rem;
	border: 1px solid var( --gv-gold-20 );
	border-radius: var( --wp--custom--radius--card, 0.5rem );
}

.gv-summary__heading {
	margin: 0 0 1rem;
	text-align: center;
	font-family: var( --wp--preset--font-family--display, serif );
	font-size: var( --wp--preset--font-size--x-large, 1.5rem );
	font-weight: 500;
	color: var( --wp--preset--color--charcoal, #26241f );
}

.gv-summary__rows {
	display: grid;
	grid-template-columns: auto 1fr;
	gap: 0.375rem 1.5rem;
}

.gv-summary__rows dt {
	color: var( --wp--preset--color--muted-medium, #26241f80 );
	font-size: var( --wp--preset--font-size--small, 0.875rem );
}

.gv-summary__rows dd {
	margin: 0;
	text-align: right;
	font-size: var( --wp--preset--font-size--small, 0.875rem );
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* S-14: shown once builder.js can compute a price -- an empty "Total: "
   row before a tier is chosen would be worse than absent, same reasoning
   as the js-only nav/counter above. */
.gv-summary__total {
	display: none;
	align-items: center;
	justify-content: space-between;
	margin-top: 1rem;
	padding-top: 0.75rem;
	border-top: 1px solid var( --gv-gold-15 );
}

.gv-summary__total:has( [data-gv-summary-total]:not( :empty ) ) {
	display: flex;
}

/* S-13: "font-display text-xl" -- `large` is the 1.25rem token, not
   `x-large` (which is 1.5rem here, one step up from Tailwind's own scale). */
.gv-summary__total-label {
	font-family: var( --wp--preset--font-family--display, serif );
	font-size: var( --wp--preset--font-size--large, 1.25rem );
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* S-13: "font-display text-2xl" -- `x-large` (1.5rem), not `xx-large`
   (1.875rem, one step further up this theme's scale). */
.gv-summary__total-value {
	font-family: var( --wp--preset--font-family--display, serif );
	font-size: var( --wp--preset--font-size--x-large, 1.5rem );
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* T-013/B-048: treatment read directly from BuildGift.jsx:214 (H-007), not
   from T-002-charm-edit-delta.md. `.gv-field`'s own max-width/margin-inline
   already carried "max-w-lg mx-auto" (width cap + centring); this block adds
   the textarea's own properties. `rounded-lg` resolves to this project's
   `base` token (4px), not `card` (8px) -- BuildGift.jsx:214's own
   `--radius: 0.25rem` (index.css:34) makes `rounded`/`rounded-lg` land on
   the same value here, same finding already made for the base cards
   (builder.css:336-341). `border-palegold/20` is `--gv-gold-20`, already
   declared in style.css's :root -- no new token. Placeholder colour
   `placeholder:text-charcoal/30` is `--gv-fg-30`, same source. Neither the
   hint span below nor the placeholder STRING are touched here -- see
   builder.php's own ADR-024 docblock. */
.gv-field {
	max-width: 32rem; /* max-w-lg */
	margin-inline: auto; /* mx-auto */
}

.gv-field textarea {
	/* This project declares no global box-sizing reset (confirmed by grep --
	   no rule anywhere in style.css or builder.css). Tailwind's Preflight
	   gives BuildGift.jsx's textarea border-box for free; without this line
	   here the 1px border + 1rem padding below render OUTSIDE `width: 100%`
	   and the field overflows `.gv-field` by 34px at 390px -- measured live,
	   not assumed, in a same-origin iframe probe (T-013). */
	box-sizing: border-box;
	display: block; /* block */
	width: 100%; /* w-full, within .gv-field's max-w-lg */
	resize: none; /* resize-none */
	border: 1px solid var( --gv-gold-20 ); /* border border-palegold/20 */
	border-radius: var( --wp--custom--radius--base, 0.25rem ); /* rounded-lg */
	background: var( --wp--preset--color--ivory, #f9f7f2 ); /* bg-ivory */
	padding: 1rem; /* p-4 */
	font-size: var( --wp--preset--font-size--small, 0.875rem ); /* text-sm */
	color: var( --wp--preset--color--charcoal, #26241f ); /* text-charcoal */
}

.gv-field textarea::placeholder {
	color: var( --gv-fg-30 ); /* placeholder:text-charcoal/30 */
}

/* focus:border-palegold focus:outline-none -- full-strength border replaces
   the outline; WCAG 2.4.7 needs that replacement to be visible, and this
   project has already shipped one focus stop with none (state/repos/
   goldvion.md). Measured live, not assumed: see T-013's task note. */
.gv-field textarea:focus {
	border-color: var( --wp--preset--color--gold, #d4c5a9 );
	outline: none;
}

.gv-field__note {
	display: block;
	margin-top: 0.25rem;
	font-size: var( --wp--preset--font-size--small, 0.8125rem );
	color: var( --wp--preset--color--muted, #6f6a60 );
}

/* Below the tablet breakpoint the choice grids collapse to one column, and a
   `.gv-card` is a centred COLUMN with 2rem of padding — so a tone card is
   163px tall to show the word "Gold", and the tier step ran 720px for three
   cards. Both pushed Continue off a 390x844 phone.

   The fix is to turn the card on its side rather than to shrink the type:
   a centred row wraps only when it must, the swatch sits beside its label
   instead of above it, and each card costs roughly a third of what it did.
   Mobile for the builder is the team's own interpretation rather than a
   reconstruction of the client's export — state-of-the-build.md's warning box,
   item 2 — so this is a layout decision we are entitled to make, unlike the
   desktop composition.

   `max-width` rather than `min-width` because the multi-column rules it has
   to beat start at 768px; this is the "below that" case and reads as one. */
@media ( max-width: 767px ) {
	.gv-card {
		flex-direction: row;
		flex-wrap: wrap;
		align-items: center;
		justify-content: center;
		gap: 0.375rem 0.75rem;
		padding: 0.75rem 1rem;
	}

	.gv-card__title {
		font-size: var( --wp--preset--font-size--large, 1.25rem );
	}

	.gv-card__price {
		margin-top: 0;
	}

	.gv-card__select {
		margin-top: 0;
	}

	.gv-swatch {
		width: 1.75rem;
		height: 1.75rem;
	}

	.gv-grid {
		gap: 0.625rem;
		margin-block: 0.75rem;
	}

	/* The mystery flow's intro sentence wraps to three lines on a phone. */
	.gv-panel__note {
		font-size: var( --wp--preset--font-size--x-small, 0.75rem );
	}

	.gv-panel__title {
		margin-bottom: 0.75rem;
		font-size: var( --wp--preset--font-size--x-large, 1.5rem );
	}

	.gv-field {
		margin-block: 0.5rem;
	}

	.gv-panel__subtitle,
	.gv-panel__note {
		margin-bottom: 1rem;
	}

	/* The mystery flow's last two steps are the tallest in the whole builder:
	   `preferences` carries three style cards AND the free-text note, and
	   `review` carries the whole brief echoed back. Both still ran past the
	   fold after the card compaction above. */
	.gv-field textarea {
		padding: 0.75rem;
		/* `rows="3"` is an HTML attribute and cannot be responsive; this is
		   the CSS equivalent of two rows, and it overrides the attribute
		   rather than fighting it. The field still grows nothing — it is
		   `resize: none` — so this is the whole height. */
		height: 4.5rem;
	}

	.gv-field__note {
		font-size: var( --wp--preset--font-size--micro, 0.625rem );
	}

	.gv-summary {
		margin-bottom: 1rem;
		padding: 1.25rem;
	}

	.gv-summary__rows {
		gap: 0.375rem 1rem;
	}
}

/* The review step, side by side once there is room for it.
   ------------------------------------------------------------------------
   In charm mode this panel stacks three tall things — the brief echoed back,
   the free-text note, and the submit — for 698px on a 1366x768 laptop, while
   half the width sat empty. Putting the summary and the note in two columns
   costs nothing and removes about 200px of height.

   NOT done by compressing the summary: that echo is the customer's brief read
   back to her at the point of purchase, and ADR-019 and business-model.md make
   it load-bearing. Rearranging it is fair game;
   shrinking it to fit is not.

   `:has( > .gv-field )` gates the whole thing because mystery mode carries its
   note on the preferences step instead, so its review panel has one child to
   place, not two — and a half-width summary floating in a two-column grid
   would be worse than the stack it replaced. */
@media ( min-width: 768px ) {
	/* `:not([hidden])` is REQUIRED, not decorative. Any rule that sets
	   `display` on a panel outranks the UA's `[hidden] { display: none }`
	   (0,1,0) and the panel then never hides — it stays laid out on every
	   other step, silently pushing the nav below the fold everywhere. This is
	   the same trap ADR-019 hit with `.gv-panel--later`, and the same fix:
	   a display rule on a panel must exclude the hidden state itself. */
	/* The title sits above two columns rather than above a stack, so it needs
	   less air beneath it than a full-width heading does. */
	.gv-panel[ data-panel="review" ]:not( [hidden] ):has( > .gv-field ) > .gv-panel__title {
		margin-bottom: 0.75rem;
	}

	.gv-panel[ data-panel="review" ]:not( [hidden] ):has( > .gv-field ) {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: 0 2rem;
		align-items: start;
	}

	.gv-panel[ data-panel="review" ]:has( > .gv-field ) > .gv-panel__title,
	.gv-panel[ data-panel="review" ]:has( > .gv-field ) > .gv-submit {
		grid-column: 1 / -1;
	}

	/* `grid-row: 3` is explicit for a reason: with only a column assigned, grid
	   auto-placement back-fills the submit into row 2 alongside the summary
	   and the note, and "Add To Bag" ends up ABOVE the brief it is meant to
	   confirm. The customer must read the echo before she meets the button
	   (ADR-019), so this is not a cosmetic preference. */
	.gv-panel[ data-panel="review" ]:has( > .gv-field ) > .gv-submit {
		grid-row: 3;
		justify-self: center;
		margin-top: 1.25rem;
	}

	.gv-panel[ data-panel="review" ]:has( > .gv-field ) > .gv-summary {
		grid-column: 1;
		grid-row: 2;
		margin-inline: 0;
		max-width: none;
	}

	.gv-panel[ data-panel="review" ]:has( > .gv-field ) > .gv-field {
		grid-column: 2;
		grid-row: 2;
		margin-block: 0;
		margin-inline: 0;
		max-width: none;
	}
}

/* Step and page transitions ----------------------------------------------
   A 0.5s ease-in-out slide, in the direction of travel: forward steps enter
   from the right, Back enters from the left, and the charm grid does the same
   as the pager moves.

   TWO THINGS THIS MUST NOT BREAK, both of which are why the class is removed
   on `animationend` in builder.js rather than left to `animation-fill-mode`:

     1. A `transform` on an ancestor makes that ancestor the containing block
        for `position: fixed` descendants. The charm modals ARE fixed, and
        they live inside both the panel and the grid — so a lingering
        transform would re-anchor every modal to the panel instead of the
        viewport. `builder.js` also closes any open modal before a page turn,
        which is the other half of that guard.
     2. The animation must never be the thing that makes content visible. It
        animates opacity from 0, so a failure to run leaves the panel at its
        normal opacity rather than invisible — `hidden` is still the only
        thing controlling what is shown, and no-JS never reaches this code.

   Enhancement only, and honoured as such: reduced-motion users get the step
   change with no movement at all. */
@media ( prefers-reduced-motion: no-preference ) {
	@keyframes gv-slide-from-end {
		from {
			opacity: 0;
			transform: translateX( 2.5rem );
		}

		to {
			opacity: 1;
			transform: none;
		}
	}

	@keyframes gv-slide-from-start {
		from {
			opacity: 0;
			transform: translateX( -2.5rem );
		}

		to {
			opacity: 1;
			transform: none;
		}
	}

	.gv-slide-next {
		animation: gv-slide-from-end 0.5s ease-in-out;
	}

	.gv-slide-prev {
		animation: gv-slide-from-start 0.5s ease-in-out;
	}
}

/* The slide travels 2.5rem horizontally, which on a full-width grid is enough
   to poke past the measure and give the page a horizontal scrollbar for half
   a second. Clip it at the row rather than on `body`, so nothing else on the
   page is affected. Safe for the modals: `overflow` does not clip a
   `position: fixed` descendant — only a transform would, and that is what the
   animationend cleanup above exists to prevent. */
.gv-charmbrowse__row {
	overflow: hidden;
}

/* Navigation ----------------------------------------------------------- */

.gv-nav {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 1rem;
	margin-top: 1.25rem;
}

/* Only occupy space when there is actually an error to show — as a permanent
   flex item this pseudo-element silently broke the row's centring. */
.gv-nav[ data-error ]::after {
	content: attr( data-error );
	flex-basis: 100%;
	text-align: center;
	color: #8a2b2b;
	font-size: var( --wp--preset--font-size--small, 0.8125rem );
}

/* T-001 §4.3 filled button primitive, applied here directly rather than via
   `.wp-element-button` because these are hand-rolled <button>s in
   builder.php, not core/button blocks — theme.json's element defaults only
   reach markup that carries that class. */
.gv-nav button,
.gv-submit {
	cursor: pointer;
	font-family: inherit;
	font-size: var( --wp--preset--font-size--micro, 0.625rem );
	letter-spacing: 0.32em;
	text-transform: uppercase;
	padding: 12px 32px;
	border-radius: var( --wp--custom--radius--pill, 999px );
	border: 1px solid var( --wp--preset--color--charcoal, #26241f );
	background: var( --wp--preset--color--charcoal, #26241f );
	color: var( --wp--preset--color--ivory, #f9f7f2 );
	transition: background-color 0.15s ease, color 0.15s ease;
}

.gv-nav button:hover,
.gv-submit:hover {
	background: var( --wp--preset--color--gold, #d4c6aa );
	color: var( --wp--preset--color--charcoal, #26241f );
}

/* Back is the outline variant — T-001 §4.3's is-style-outline treatment,
   reproduced here (not the core block style, since this isn't a core
   button): transparent ground, the fg-30 hairline, invert on hover. */
/* Specificity note: `.gv-nav button` above is class+type (0,1,1), one
   higher than a bare `.gv-nav__back` (0,1,0) — its background/color
   declarations would otherwise win. Matching the type selector keeps this
   override actually applying. */
button.gv-nav__back {
	background: transparent;
	border-color: var( --gv-fg-30 );
	color: var( --wp--preset--color--charcoal, #26241f );
}

button.gv-nav__back:hover {
	background: var( --wp--preset--color--charcoal, #26241f );
	color: var( --wp--preset--color--ivory, #f9f7f2 );
}

/* S-15: `w-full py-3.5` — a full-width bar under the summary card, not an
   inline pill centred under it. Overrides `.gv-nav button, .gv-submit`'s
   shared `padding: 12px 32px` above via source order at equal (0,1,0)
   specificity -- fine here since this is the more specific of the two
   declarations conceptually and nothing after it touches padding again. */
.gv-submit {
	display: block;
	width: 100%;
	max-width: 32rem;
	margin-inline: auto;
	padding-block: 14px;
}
