/**
 * Product Detail — patterns/product-detail/product-detail.css
 *
 * T-B (= PDP-1), the chassis. Two things and no more: the layout — column model,
 * source-order fix, sticky summary — and the type pass on the four chassis
 * elements (eyebrow, title, price; the fourth, the breadcrumb, went in I33). The gallery, add-to-cart, tabs,
 * related row and reviews widget are T-C through T-H, each with its own gate.
 *
 * Figma: desktop 43324:17374, tablet 43324:17111, mobile 43324:16850.
 * Glob-loaded by ftm_enqueue_pattern_styles() (includes/enqueue.php:70) with
 * `ftm-style` as its only dependency, like every other pattern stylesheet.
 *
 * ── SPECIFICITY ─────────────────────────────────────────────────────────────
 *
 * Pattern CSS prints at index 50-72; `woocommerce-general` prints at 79 (83 on
 * a product page). AGAINST WOOCOMMERCE, A TIE IS A LOSS — the inverse of the
 * Gravity Forms situation, where the theme wins ties on order. Anything here
 * that touches a Woo-owned element has to out-specify it outright.
 *
 * The floor is (0,3,0), not (0,2,0): the design system's own
 * `:root :where(...)` rules are already (0,2,0), so a two-class selector only
 * ties them. Everything below is either on theme-owned wrappers (which Woo
 * never selects) or doubled.
 *
 * ── MOBILE FIRST ────────────────────────────────────────────────────────────
 *
 * One column is the base state and needs no query. The two-column split is the
 * exception, added at 64rem. The boards say so directly: 320 and 768 are both
 * single-column, and only the 1920 board splits.
 */

/*
 * SOURCE ORDER vs VISUAL ORDER.
 *
 * The DOM nests Product Details inside the left column, because at desktop the
 * board draws it there — under the gallery, aligned to the main image. Stacked
 * on one column that would put the whole description ABOVE the title and the
 * price, which is wrong at every size below 64rem and is not what the tablet or
 * mobile boards draw.
 *
 * `display: contents` on the left column below 64rem dissolves it, so the
 * gallery and the details become direct grid items alongside the summary and
 * can be ordered independently. It is safe here specifically because these are
 * plain divs carrying no role and no accessible name — the known `display:
 * contents` bugs strip semantics from elements that have some.
 *
 * Order is set explicitly on all three rather than relying on source order, so
 * the intent is readable at the point it matters.
 */
.tc-pdp__top {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	row-gap: clamp(2rem, 1.286rem + 3.571vw, 3rem);
}

.tc-pdp__primary {
	display: contents;
}

.tc-pdp__gallery {
	order: 1;
}

.tc-pdp__summary {
	order: 2;
}

/*
 * ⚠ `order` IS REQUIRED ON EVERY CHILD OF `.tc-pdp__primary`, NOT JUST THE ONES
 * THAT MOVE — and T-E paid for that.
 *
 * Below 64rem `.tc-pdp__primary` is `display: contents`, so its children become
 * direct grid items of `.tc-pdp__top` and sort by `order`. The prose block was
 * added in T-E without one, took the initial `order: 0`, and rendered ABOVE the
 * gallery — first thing on the page, before the product image and the title.
 * Measured at 320: prose y=248, gallery y=844.
 *
 * Desktop hid it completely, because there `.tc-pdp__primary` is `display: block`
 * and source order applies. It was found by looking at a mobile screenshot.
 *
 * So the sequence is stated in full and explicitly: gallery, summary, prose,
 * details. Anything added inside `.tc-pdp__primary` needs a number here.
 */
.tc-pdp__prose {
	order: 3;
}

.tc-pdp__details {
	order: 4;
}

/*
 * ⚠ BELOW 64rem THE PROSE → ACCORDION GAP SUMMED FOUR MARGINS (QA batch A3,
 * 2026-09-28). The re-review saw a 100-140px empty band between the Product
 * Details text and the Description row at 320-834.
 *
 * Here the prose and the details are grid items of `.tc-pdp__top`, and grid
 * items do not collapse margins, so four spaces stacked: the excerpt's last
 * paragraph (16), the grid row-gap (clamp 32 → 48), the details block's
 * flow-layout margin (24) and the accordion's own margin (the same clamp).
 * Measured 109 at 390 and 136 at 834, against 48 at 1440 — where QA batch A
 * already restated the collapse (see "MARGIN COLLAPSE HAS TO BE RESTATED"
 * inside the 64rem query below).
 *
 * The same restatement here, and the accordion's clamp goes too, because the
 * row-gap is that same clamp and already separates the two items. Result: 32
 * at 320, 34.5 at 390, 48 from 768 — the accordion's own number, as at 1440.
 * Scoped below 64rem so the desktop rules below are untouched.
 */
@media (max-width: 63.9375rem) {
	.tc-pdp .tc-pdp__prose-body > :last-child {
		margin-block-end: 0;
	}

	.tc-pdp .tc-pdp__details {
		margin-block-start: 0;
	}

	.tc-pdp .tc-pdp__details .tc-pdp-accordion {
		margin-block-start: 0;
	}
}

@media (min-width: 64rem) {
	/*
	 * THE COLUMN MODEL — 807 / 529 inside 1400, with a 65px gutter.
	 *
	 * Deliberately NOT core/columns. Core writes
	 * `:not(.is-not-stacked-on-mobile) > .wp-block-column { flex-basis }` with
	 * `!important`, so a stylesheet cannot restate those widths — and Woo's own
	 * template hard-codes 512px + fill, which is the narrow column on the WRONG
	 * side. Two plain groups in a grid gets the board's proportions exactly and
	 * never meets that rule.
	 *
	 * fr units rather than percentages so the 807:529 ratio is stated once and
	 * the gutter is subtracted from the track sizing rather than fighting it.
	 * 807 + 65 + 529 = 1401, i.e. the 1400 boundary to within a pixel.
	 *
	 * minmax(0, …) on both tracks because the default `min-width: auto` on a
	 * grid item refuses to shrink below its content — which a product gallery
	 * with a wide image, or a long unbroken SKU, would otherwise force.
	 */
	.tc-pdp__top {
		column-gap: clamp(2.5rem, -0.607rem + 4.643vw, 4.0625rem);
		grid-template-columns: minmax(0, 807fr) minmax(0, 529fr);

		/*
		 * THE LEFT COLUMN IS THREE ROWS, NOT ONE BOX (QA batch A, 2026-09-27).
		 *
		 * Until then `.tc-pdp__primary` was a block in column 1 holding gallery,
		 * prose and details, and the summary FOLLOWED it in the source — so the
		 * H2 "Product Details" and the accordion H3s preceded the H1 in reading
		 * order. The source is now gallery, summary, prose, details (see
		 * product-detail.php), `.tc-pdp__primary` is `display: contents` here as
		 * it already was below 64rem, and each item is placed explicitly.
		 *
		 * The summary spans all three rows, so its grid area is still the full
		 * height of the left column and the sticky travel is unchanged. The last
		 * row is `1fr` so that if the summary is ever the TALLER column its
		 * spare height lands under the accordion, where the old block would have
		 * stretched, instead of opening gaps between gallery, prose and details.
		 * `row-gap: 0` because these three were one block flow before — their
		 * spacing is their own margins (prose's clamp), exactly as it was.
		 */
		grid-template-rows: auto auto 1fr;
		row-gap: 0;
	}

	.tc-pdp__primary {
		display: contents;
	}

	.tc-pdp__gallery,
	.tc-pdp__prose,
	.tc-pdp__details {
		align-self: start;
		grid-column: 1;
	}

	.tc-pdp__gallery {
		grid-row: 1;
	}

	.tc-pdp__prose {
		grid-row: 2;
	}

	.tc-pdp__details {
		grid-row: 3;
	}

	/*
	 * ⚠ MARGIN COLLAPSE HAS TO BE RESTATED, BECAUSE GRID ITEMS DO NOT COLLAPSE.
	 *
	 * In the old single block, three margins met between the prose text and the
	 * accordion and COLLAPSED to the largest: the excerpt's last paragraph (16),
	 * the details block's flow-layout gap (24) and the accordion's own
	 * `margin-block-start` (clamp → 48 at every width from 64rem). As separate
	 * grid items each one is contained instead, and they summed to 88 — measured
	 * +40px on the page at 1280/1440/1920 before this rule.
	 *
	 * So the two smaller ones go to 0 here and the accordion's 48 is the gap
	 * again, exactly as the collapse resolved it. Pixel-diffed against main at
	 * 1280, 1440 and 1920 (QA batch A report).
	 */
	.tc-pdp .tc-pdp__prose-body > :last-child {
		margin-block-end: 0;
	}

	.tc-pdp .tc-pdp__details {
		margin-block-start: 0;
	}

	/*
	 * THE STICKY SUMMARY.
	 *
	 * `align-self: start` is not optional and is not cosmetic. A grid item
	 * stretches to its row's height by default, so before this the summary
	 * measured exactly as tall as the left column and had no room to travel —
	 * sticky computed fine and did nothing. Measured on the pre-PDP-1 page,
	 * where the two core/columns children both came out at 737px against a
	 * gallery of 701px. The flex equivalent of this trap is `align-self:
	 * flex-start`; the symptom is identical and looks like "sticky is broken".
	 *
	 * `inset-block-start: 0` plus a small offset: the theme has no sticky or
	 * fixed header — verified, and tabbed-timeline.css:585 records the same
	 * check — so there is no chrome to clear, only breathing room.
	 *
	 * WHAT KILLS THIS SILENTLY. Sticky positions against its nearest scrolling
	 * ancestor. Give ANY ancestor an `overflow` other than `visible` and that
	 * ancestor becomes the scrollport; it is not scrollable, so nothing moves
	 * and nothing errors. Measured at PDP-1: every ancestor from here to <html>
	 * computes `overflow: visible`, with no `contain`, `transform`, `filter` or
	 * `will-change` anywhere in the chain. Keep it that way. In particular, do
	 * NOT add `overflow-x: hidden` to `body`, `.wp-site-blocks` or `main` to
	 * chase a horizontal scrollbar — the gallery and cross-sell sliders in
	 * PDP-2 and PDP-5 are exactly the work that invites it. Each slider must
	 * contain its own overflow on its own track element. If a clip is genuinely
	 * needed on an ancestor, `overflow: clip` does not create a scroll container
	 * and is safe.
	 *
	 * The travel is modest and that is per the board: the summary ends at
	 * y=992 and the left column runs to y=1631, so roughly 640px of stick. On a
	 * product with a short description and no gallery the summary will be the
	 * taller column and this correctly does nothing at all.
	 */
	.tc-pdp__summary {
		align-self: start;
		grid-column: 2;
		grid-row: 1 / -1;
		inset-block-start: clamp(1rem, 0.5rem + 1vw, 2rem);
		position: sticky;
	}
}

/*
 * ── THE FOUR CHASSIS ELEMENTS ───────────────────────────────────────────────
 *
 * Category eyebrow, title, price (the breadcrumb was the fourth until I33). Everything else is
 * T-C through T-H.
 *
 * ── THE RAMP GOES FLAT AT 48rem, AND THAT IS THE WHOLE POINT OF CHECKING 768 ─
 *
 * `get_variable_defs` per breakpoint, read off the boards themselves:
 *
 *   role          320      768      1920
 *   Title/Large   19 / 24  27 / 32  27 / 32   <- h1
 *   Title/Small   18 / 24  20 / 28  20 / 28   <- price
 *   Label/Medium  14 / 20  14 / 20  14 / 20   <- eyebrow
 *
 * **Tablet already equals desktop.** So the ramp runs 320 -> 768 and is FLAT
 * above it, and the theme's usual 320 -> 1400 ramp would be wrong here: at 1400
 * it would have carried the title to roughly 35px against the board's 27. This
 * is exactly the case the standing lesson ("the fluid ramp is 320->1400, checked
 * at 768 specifically") exists to catch, and it is the first build where the
 * check has changed the answer rather than confirmed it.
 *
 * The clamps therefore top out at the 768 value: max is reached at 48rem, not at
 * 87.5rem. Slope is (max-min)/448px expressed in vw.
 *
 * ⚠ THESE TYPE ROLES DIVERGE FROM theme.json AND THE FRAMES WIN.
 *
 * CLAUDE.md is explicit that frames are canonical, so the board's numbers are
 * what ship. But the divergence is real and someone should reconcile it:
 *
 *   role          board (Figma)   theme.json preset
 *   Title/Large   19 -> 27px      title-large: 18 -> 23px
 *   Title/Small   18 -> 20px      title-small: 16 -> 18px
 *   Label/Medium  14px flat       label-medium: 14px flat   <- agrees
 *
 * Two of the three disagree by 2-4px at the top of the ramp, which means either
 * the Figma library or theme.json is stale. **Flagged, not silently resolved** —
 * if theme.json is the stale one, fixing it there deletes most of this block.
 * Recorded on the designer list.
 *
 * The h1 is deliberately SMALL — 27px, against the theme's `display` role which
 * runs to 52px. A product name is not a page display heading. Do not "fix" it
 * upward to match other h1s on the site.
 */

/*
 * (0,3,0) is the floor, so every rule here doubles its own class inside the
 * pattern scope. `.tc-pdp .tc-pdp__title` alone is (0,2,0), which only TIES the
 * design system's `:root :where(...)` rules and loses to `woocommerce.css` on
 * print order. See the specificity note at the top of this file.
 */

/*
 * ── THE "New" PILL AT THE TOP OF THE SUMMARY (2026-09-01) ──────────────────
 *
 * `docs/design/product-detail/desktop.png` draws it directly above the category
 * link. Injected by `includes/product-new-badge.php`, which prepends it to the
 * `core/post-terms` block rather than adding a template block — pattern PHP
 * cannot see a product, so per-product markup arrives at render time or not at
 * all.
 *
 * ⚠ IT IS THE CARD BADGE, NOT A NEW COMPONENT, AND THE FRAME IS WHAT SETTLES
 * THAT. Measured off the export at 20x, the pill is a rectangle with ONE
 * rounded corner — bottom-left — which is exactly the shape
 * `.tc-product-card__badge` already draws (product-grid.css). Same word, same
 * shape, same weight, same fill. So every value below is that rule's, and the
 * two must move together if either moves.
 *
 * ⚠ WHAT IS **NOT** COPIED IS THE POSITIONING, and that is the whole difference
 * between the two. The card badge is `position: absolute` over the product
 * photo, with `pointer-events: none` because it sits inside the image's link.
 * This one is a static box in the summary's flow, above a real link it must not
 * cover, so it takes neither.
 *
 * ⚠ AND THE FRAME'S FILL IS #059669 WHERE THIS DRAWS tertiary-600 (#04865D).
 * Measured, deliberate, and flagged for the designer rather than reproduced:
 * #059669 belongs to no part of this palette, tertiary-600 is the token the
 * card badge already carries, and one word rendered in two greens on two
 * surfaces is worse than a four-unit difference from one export.
 *
 * `inline-flex` on a `<p>` makes the box shrink to its text while still taking
 * its own line — every sibling in this column is block-level, so there is
 * nothing for it to share a line with.
 */
.tc-pdp .tc-pdp__badge {
	display: inline-flex;
	align-items: center;
	border-radius: 0 0 0 0.5rem; /* bottom-left 8 only — the card badge's shape */
	background-color: var(--wp--custom--color--tertiary--600, #04865d);

	/* 4.5 + 16 + 4.5 = 25, the boards' height — G19, 2026-09-17, moved together
	   with the card badge as the note above requires. product-grid.css carries
	   the full account (and why `text-box-trim` was not used). */
	padding-block: 4.5px;
	padding-inline: var(--wp--preset--spacing--xsmall, 0.5rem); /* 8 */
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: var(--wp--preset--font-size--label-small, 0.75rem); /* 12 */
	font-weight: 700;
	line-height: 1.3333; /* 16 */
	letter-spacing: 0.002em;
	color: var(--wp--custom--color--base--white, #fff);
	margin-block: 0 0.5rem;
}

.tc-pdp .tc-pdp__eyebrow.tc-pdp__eyebrow {
	color: var(--color-text);
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.875rem;
	font-weight: 500;
	line-height: 1.4286;
	margin-block-end: 0.5rem;
}

/*
 * post-terms renders LINKS, which pick up --color-link (#123B9E) from the
 * global anchor rule. The board draws the eyebrow in body colour, so the colour
 * is restated here rather than inherited. Focus styling is deliberately
 * untouched — it comes from the global :focus rule and must keep working,
 * because this is still a link.
 *
 * ⚠ UNDERLINED AT REST SINCE 2026-09-01. This rule used to declare
 * `text-decoration: none` with a hover underline, and the comment above it said
 * the board drew no underline. RE-READ AT 4x, THE BOARD DRAWS ONE: "Transmitters"
 * in `docs/design/product-detail/desktop.png` carries a rule under it in the
 * same ink as the text. The original reading was wrong, not overruled — at the
 * export's native 962px the 1px underline is close to invisible.
 *
 * ⚠ THE HOVER RULE IS GONE BECAUSE IT WOULD NOW BE A NO-OP. It declared exactly
 * this underline; keeping it would leave a rule that changes nothing on hover
 * and reads as if it did. The link is still distinguishable at rest — it is the
 * only underlined text in the summary — and it still takes the global focus
 * ring.
 */
.tc-pdp .tc-pdp__eyebrow.tc-pdp__eyebrow a {
	color: inherit;
	text-decoration: underline;
}

.tc-pdp .tc-pdp__title.tc-pdp__title {
	color: var(--color-text);
	font-family: var(--wp--preset--font-family--display);
	font-size: clamp(1.1875rem, 0.8304rem + 1.7857vw, 1.6875rem);
	font-weight: 700;
	letter-spacing: -0.02em;
	line-height: 1.2632;
	margin-block: 0 0.75rem;
}

.tc-pdp .tc-pdp__price.tc-pdp__price {
	color: var(--color-text);
	font-family: var(--wp--preset--font-family--display);
	font-size: clamp(1.125rem, 1.0357rem + 0.4464vw, 1.25rem);
	font-weight: 700;
	line-height: 1.3333;
}

/*
 * LINE HEIGHTS ARE UNITLESS AND THEREFORE SWITCH RATHER THAN RAMP.
 *
 * The repo's stylelint config forbids units on `line-height`, so a vw-based
 * clamp is not available and a single unitless ratio cannot hit both ends: the
 * board's title runs 19/24 (1.2632) at 320 and 27/32 (1.1852) at 768, and the
 * price 18/24 (1.3333) then 20/28 (1.4). The ratios move in OPPOSITE directions,
 * so no compromise value is close at both ends.
 *
 * They are switched at 48rem — the same boundary where the font-size clamps top
 * out and where the boards themselves stop changing. That buys exactness at both
 * specified breakpoints, which is what the frames actually state, at the cost of
 * a ~2px step in the title's leading at exactly 768. The alternative (one ratio
 * everywhere) was ~1.5px wrong at 320 with no step. **Exact at both boards won,
 * because 320 and 768 are the only widths the design specifies at all** — the
 * interval between them is interpolation nobody drew.
 */
@media (min-width: 48rem) {
	.tc-pdp .tc-pdp__title.tc-pdp__title {
		line-height: 1.1852;
	}

	.tc-pdp .tc-pdp__price.tc-pdp__price {
		line-height: 1.4;
	}
}

/*
 * THE TOP OF THE PAGE — header to the first row (I33, 2026-10-15).
 *
 * The breadcrumb that used to open this pattern is gone (no board draws one;
 * see product-detail.php), and with it the margin that had been standing in
 * for this space. Measured before: 145 / 100 at 390 (the trail wrapped to
 * three or two 45-tall lines) and 68 at 1440. The boards, header bottom to
 * the gallery / summary row:
 *
 *   board     header ends   row starts   gap
 *   320       130           146          16    43324:16850
 *   768       145           169          24    43324:17111
 *   1920      205           245          40    43324:17374
 *
 * 24 of that is NOT this file's, at every width. Woo's single-product wrapper
 * is a child of `.wp-site-blocks`, so it takes the root block gap (24), and
 * nothing between it and `.tc-pdp` has padding, so a margin on `.tc-pdp`
 * collapses into that 24 and a padding adds to it. Hence two properties:
 *
 *   below 48rem   margin  −8 at 320 → 0 at 768   (24 − 8 = 16 … 24)
 *   48rem up      padding  0 at 768 → 16 at 1400 (24 + 16 = 40), flat above —
 *                 the 1920 board's content box is the 1400 one
 *
 * A ramp, not a step, between 320 and 768: exact at both boards, as the type
 * ramp above is. ⚠ The page used to start 11 under the header on a phone, not
 * 24, and that was the BREADCRUMB's own phone rule (`margin-block-start:
 * -13px`, the tap-target pull-up) collapsing up through the same chain. A
 * positive padding here was measured at 29 for exactly that reason.
 *
 * `.tc-pdp__top` loses its flow-layout gap so the row sits on that padding,
 * EXCEPT when a store notice is showing ("added to your cart"), where the gap
 * is what keeps the banner off the gallery. An empty notices block holds an
 * empty `.woocommerce-notices-wrapper`, so the test is for a notice inside it.
 */
.tc-pdp.tc-pdp.tc-pdp {
	margin-block-start: clamp(-0.5rem, -0.8571rem + 1.7857vw, 0rem);
}

@media (min-width: 48rem) {
	.tc-pdp.tc-pdp.tc-pdp {
		margin-block-start: 0;
		padding-block-start: clamp(0rem, -1.2152rem + 2.5316vw, 1rem);
	}
}

.tc-pdp .tc-pdp__top.tc-pdp__top {
	margin-block-start: 0;
}

.tc-pdp .wc-block-store-notices:has(.wc-block-components-notice-banner, .woocommerce-message, .woocommerce-error, .woocommerce-info) + .tc-pdp__top.tc-pdp__top {
	margin-block-start: var(--wp--style--block-gap, 1.5rem);
}

/*
 * At 64rem+ the summary is the row's second column, and the board starts it
 * level with the gallery (both y=245). It sat 24 lower: it follows the gallery
 * inside `.tc-pdp__primary`, a flow group, and kept that group's block gap
 * even after the column model made it a grid item beside the gallery. Below
 * 64rem it is a stacked row and the gap is part of the stack, so it stays.
 */
@media (min-width: 64rem) {
	.tc-pdp .tc-pdp__summary.tc-pdp__summary {
		margin-block-start: 0;
	}
}

/*
 * ═══════════════════════════════════════════════════════════════════════════
 * T-C — THE GALLERY
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * The markup is ftm-blocks', mounted by `firsttracks-theme/product-gallery`
 * (blocks/product-gallery/render.php) inside `.tc-pdp__gallery-mount`. Every
 * selector below therefore names a THIRD PARTY'S class. That is a deliberate,
 * recorded risk — see the maintainer note in DECISIONS.md — and it is why the
 * regression spec asserts the geometry rather than trusting it.
 *
 * ── SPECIFICITY, AND WHY THE MOUNT CLASS EXISTS ─────────────────────────────
 *
 * `ftm-blocks-product-gallery` prints AFTER this stylesheet — last of 62 sheets
 * on a product page, and structurally so, because it is registered as depending
 * on `ftm-blocks-swiper`, which is itself registered after the theme's pattern
 * sheets. AGAINST THE PLUGIN, AS AGAINST WOOCOMMERCE, A TIE IS A LOSS.
 *
 * The plugin's selectors top out at (0,2,0). The floor here stays (0,3,0) for
 * the reason at the top of this file — the design system's `:root :where(...)`
 * rules are already (0,2,0) — and `.tc-pdp .tc-pdp__gallery-mount .ftm-…` gets
 * there without doubling anything. Audited in BOTH directions: nothing below is
 * so specific that a later, more local rule cannot reach it, and no rule reaches
 * outside the mount.
 *
 * NO `!important` ANYWHERE. The one place it would be tempting — Swiper's inline
 * `transform` on the thumbnail wrapper — is designed out by calling
 * `changeDirection()` rather than fighting the transform. See product-detail.js.
 *
 * ── THE BOARDS DRAW A TWO-IMAGE PRODUCT, NOT "A GALLERY" ────────────────────
 *
 * ⚠ Read this before treating the frames as authority over anything below.
 *
 * The desktop rail (43324:17622) is a 95 x 198 frame holding exactly TWO 95x95
 * thumbnails, and the board's own product (#67382) has exactly two media. It is
 * a literal rendering of a two-image product, not an abstraction of a gallery.
 *
 * Measured across all 1,171 published products: 1 has no image, 1,091 have
 * exactly ONE (93.2%), 72 have 2-4, and 7 have 5+ (max 10). So the board depicts
 * a case that 6% of the catalogue is in, and BOTH ENDS OF THE RANGE ARE
 * UNDESIGNED — nothing draws the single-image state, and nothing draws ten
 * thumbnails. What ships for each is a ruling, recorded in DECISIONS.md
 * (2026-08-05), not something read off a frame. A future frame-verification pass
 * must not read the board as authority over cases it never depicted.
 *
 * ── GEOMETRY, MEASURED FROM THE CANONICAL 43324 SET ─────────────────────────
 *
 *   desktop 43324:17374   rail 95 + gap 24 + image 688 = 807  <- T-B's column
 *                         thumbs 95x95, 8px vertical gap, rail at x=260
 *   tablet  43324:17111   image 688 square, 40px gutters, no rail
 *   mobile  43324:16850   image 288 square, 16px gutters, no rail
 *
 * Every image box is 1:1 at every breakpoint. The 688 is a CONSEQUENCE of the
 * rail taking 95 + 24 out of 807, not an independent value — which is what makes
 * the single-image ruling below (image fills the whole 807) the coherent one
 * rather than a compromise.
 */

/*
 * RESTATING THE PLUGIN'S OWN CUSTOM PROPERTIES, RATHER THAN ITS RULES.
 *
 * `--ftm-gallery-border` and `--ftm-gallery-radius` are declared on
 * `.ftm-product-gallery` itself, so re-declaring them here at (0,3,0) re-points
 * every rule that consumes them — main border, thumb border, expand button,
 * lightbox close — in four lines instead of a dozen overrides. Cheaper to read
 * and much cheaper to keep true if the plugin adds another consumer.
 *
 * ⚠ TWO OF THE PLUGIN'S THREE CUSTOM PROPERTIES ARE UNDEFINED IN THIS STACK.
 * `--color-border-muted` and `--color-surface` are declared NOWHERE in the theme
 * or the plugin, so every border in the plugin's gallery currently paints its
 * `#d4d4d4` fallback and every surface its `#fff`. `--color-primary` IS defined
 * (assets/css/theme-tokens.css:49 -> primary-600 #123B9E), which is why the
 * active thumbnail outlines brand navy rather than the board's sky. All three
 * are corrected here.
 *
 * The stroke values are scanned pixel-by-pixel from 1:1 Figma node renders, not
 * read off the node tree, per the standing lesson. The committed PNGs in
 * docs/design/product-detail/ are downscaled and useless for a stroke.
 */
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery {
	--color-surface: var(--wp--custom--color--base--white, #fff);
	--ftm-gallery-border: 1px solid
		var(--wp--custom--color--neutral--100, #e9ecef);
	--ftm-gallery-radius: 4px;
}

/*
 * `object-fit: contain`, and the plugin ships `cover`.
 *
 * Cropping a spec-sheet photograph of a piece of equipment destroys information,
 * and this catalogue's source images are not a uniform aspect. `contain` on the
 * white ground the box already has keeps the whole product visible.
 *
 * ⚠ THIS CANNOT BE JUDGED HERE AND HAS NOT BEEN. The two boards that would show
 * it (tablet, mobile) are empty placeholders, and NO product image file resolves
 * in this Codespace — uploads 302 to a remote host that answers 401. It goes to
 * the designer with three real photographs once media syncs. It is one
 * declaration. Client TBD, DECISIONS.md 2026-08-05.
 *
 * `block-size: 100%` is required for `contain` to mean anything: the plugin's
 * `height: auto` sizes the box to the image, which leaves object-fit with
 * nothing to fit into.
 *
 * ── AND THE IMAGE IS TAKEN OUT OF FLOW, WHICH IS A BUG FIX, NOT A PREFERENCE ─
 *
 * The obvious form — `block-size: 100%; inline-size: 100%; object-fit: contain`
 * on an in-flow image — CREATES A CYCLIC SIZE DEPENDENCY here, and Chromium
 * resolves the cycle to its maximum rather than erroring:
 *
 *   the slide is a flex item with `min-width: auto`, so its automatic minimum
 *   consults its content; the content is an image whose height is 100% of the
 *   slide; the slide's height comes from `aspect-ratio: 1/1` off its own width.
 *
 * Measured, not theorised: the main slider computed **33,554,433px** wide, and
 * Swiper — which reads the container once at init — cached that and wrote
 * `width: 3.35544e+07px` inline onto the slide, so it never recovered. The page
 * rendered a single image running off the right of the viewport.
 *
 * It only bites at >=64rem. Below that the slider is a plain block child whose
 * width is definite from its parent, which breaks the cycle before it starts —
 * which is exactly why the tablet and mobile shots looked correct while desktop
 * did not, and why this needed a screenshot rather than a passing spec.
 *
 * `position: absolute` removes the image from intrinsic sizing altogether, so
 * the slide's size comes only from Swiper's inline width and the aspect ratio.
 * `object-fit: contain` then does what it was chosen for, in a box that is
 * genuinely square.
 *
 * `position: relative` on the slide is required for the above and has a second,
 * wanted effect: the plugin's expand button is `position: absolute` with no
 * positioned ancestor nearer than `.ftm-product-gallery`, so it currently
 * anchors to the whole gallery rather than to the image it zooms. This
 * re-anchors it to its own slide, which is where the button belongs.
 */
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-slide {
	position: relative;
}

.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-slide img,
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-slide video {
	block-size: 100%;
	inline-size: 100%;
	inset: 0;
	object-fit: contain;
	position: absolute;
}

/*
 * THE ACTIVE THUMBNAIL IS secondary-600 AND MUST STAY secondary-600.
 *
 * The standing "CTAs at secondary-700" lesson is an AA text-contrast rule for
 * foreground text on interactive controls. This is a 2px BORDER on a thumbnail,
 * not a CTA label, and the board binds 600. Do not "correct" it in a later sweep.
 */
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery .swiper-slide-thumb-active {
	outline: 2px solid var(--wp--custom--color--secondary--600, #057db9);
	outline-offset: -2px;
}

.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-thumb {
	border-radius: 2px;
}

/* ─────────────────────────────────────────────────────────────────────────
 * The glyphs — because the icon font does not exist in this installation
 *
 * The renderer emits `ftmicon-angle-left`, `ftmicon-angle-right`,
 * `ftmicon-search-plus` and `ftmicon-play`; the lightbox adds `ftmicon-close`.
 * `ftmicon` is defined NOWHERE — no @font-face for it, no glyph mapping, no font
 * file of that name, in the theme, the plugin, or anywhere else. Verified with a
 * positive control on the identical grep invocation, because a bare empty result
 * is not evidence.
 *
 * (Two near-misses, both dismissed: one `.ftmicon-heart` rule exists in a legacy
 * plugin block, setting font-size and margin only with no `content:`; and font
 * files do exist — dashicons, and Shoptimizer's Rivolicons — but none is
 * `ftmicon` and Shoptimizer is not the active theme.)
 *
 * The names map onto Font Awesome's `fa-angle-left` / `fa-search-plus` /
 * `fa-play`, consistent with the font living on the legacy Shoptimizer/Elementor
 * stack and never having been packaged into ftm-blocks. So all five buttons
 * currently render as EMPTY BOXES.
 *
 * Painted the way every other pattern in this theme does — a per-pattern
 * `--tc-icon-*` holding an inline data URI, masked onto the empty span.
 * build-knowledge.md §6 is explicit that there is no shared icon layer and each
 * pattern declares its own copy. Ninth definition of the chevron in this theme.
 * All five are alpha-only, so `background-color` paints them and one chevron
 * serves both directions via scaleX(-1).
 * ───────────────────────────────────────────────────────────────────────── */

.tc-pdp .tc-pdp__gallery-mount,
.ftm-gallery-lightbox {
	--tc-icon-chevron-left: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15.4 7.4 14 6l-6 6 6 6 1.4-1.4-4.6-4.6z'/%3E%3C/svg%3E");
	--tc-icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 6.4 17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z'/%3E%3C/svg%3E");
	--tc-icon-play: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E");
	--tc-icon-zoom-in: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9.5 3a6.5 6.5 0 0 1 5.25 10.34l.25.25h.5L20 18.09 18.09 20l-4.5-4.5v-.5l-.25-.25A6.5 6.5 0 1 1 9.5 3m0 2a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9m-1 2h2v2h2v2h-2v2h-2v-2h-2V9h2z'/%3E%3C/svg%3E");
}

.tc-pdp .tc-pdp__gallery-mount .ftmicon-angle-left,
.tc-pdp .tc-pdp__gallery-mount .ftmicon-angle-right,
.tc-pdp .tc-pdp__gallery-mount .ftmicon-search-plus,
.tc-pdp .tc-pdp__gallery-mount .ftmicon-play,
.ftm-gallery-lightbox .ftm-gallery-lightbox-close .ftmicon-close {
	background-color: currentcolor;
	block-size: 1.5rem; /* 24 glyph, all four boards */
	display: block;
	inline-size: 1.5rem;

	/* stylelint-disable-next-line property-no-vendor-prefix -- mask needs the prefix pair */
	-webkit-mask: var(--tc-icon-chevron-left) no-repeat center / contain;
	mask: var(--tc-icon-chevron-left) no-repeat center / contain;
}

/* One glyph, both directions — same trick product-grid uses for its controls. */
.tc-pdp .tc-pdp__gallery-mount .ftmicon-angle-right {
	transform: scaleX(-1);
}

.tc-pdp .tc-pdp__gallery-mount .ftmicon-search-plus {
	/* stylelint-disable-next-line property-no-vendor-prefix -- mask needs the prefix pair */
	-webkit-mask-image: var(--tc-icon-zoom-in);
	mask-image: var(--tc-icon-zoom-in);
}

.tc-pdp .tc-pdp__gallery-mount .ftmicon-play {
	/* stylelint-disable-next-line property-no-vendor-prefix -- mask needs the prefix pair */
	-webkit-mask-image: var(--tc-icon-play);
	mask-image: var(--tc-icon-play);
}

.ftm-gallery-lightbox .ftm-gallery-lightbox-close .ftmicon-close {
	/* stylelint-disable-next-line property-no-vendor-prefix -- mask needs the prefix pair */
	-webkit-mask-image: var(--tc-icon-close);
	mask-image: var(--tc-icon-close);
}

/* ─────────────────────────────────────────────────────────────────────────
 * The prev/next buttons
 *
 * 40 x 40 circle, white, NO border, `Shadow/Light/200`, 24px navy glyph,
 * overhanging the image by 12px at mobile and 16px from tablet up. Read off 1:1
 * renders of both small boards; the button is circular, confirmed from the
 * shadow's footprint rather than from a corner radius in the node tree.
 *
 * ⚠ THIS DELIBERATELY DIVERGES FROM product-grid'S CONTROLS — by 4px in overhang
 * at mobile and 8px in size. product-grid.css:719-756 uses 32px buttons
 * overhanging 8px below 48rem; the gallery boards draw 40px at BOTH breakpoints.
 * The boards' numbers ship. The two sliders' chrome is deliberately not
 * identical, so do not "fix" one to match the other — unifying them is a design
 * decision and it is on the designer list (DECISIONS.md, 2026-08-05).
 *
 * The shadow is the design system's `Shadow/Light/200`, read via
 * get_variable_defs on both nav frames — not an ad-hoc value. (product-grid.css
 * :704 uses an ad-hoc `0 2px 8px rgb(5 19 58 / 25%)` where this token exists.
 * Not this build's to fix; on the maintainer list.)
 *
 * THE BUTTONS ARE REPARENTED BY product-detail.js. The renderer emits them
 * INSIDE `.ftm-product-gallery-main`, which carries `overflow: hidden` from its
 * own rule and from swiper.css's `.swiper { overflow: hidden }` — so the board's
 * overhang cannot render from where they start. Positioning below assumes the
 * move has happened; assertion 5 in the spec fails loudly if it has not.
 * ───────────────────────────────────────────────────────────────────────── */

.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-nav {
	background: var(--wp--custom--color--base--white, #fff);
	block-size: 2.5rem; /* 40 at every breakpoint, per the boards */
	border: 0;

	/* Shadow/Light/200, read via get_variable_defs on both nav frames. */
	box-shadow:
		0 2px 4px rgb(48 49 51 / 10%),
		0 0 1px rgb(48 49 51 / 5%);
	color: var(--wp--custom--color--primary--800, #0a205b);
	cursor: pointer;
	inline-size: 2.5rem;
	inset-block-start: 50%;
	padding: 0;
	translate: 0 -50%;
}

.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-prev {
	inset-inline-start: -0.75rem; /* 12 overhang at mobile */
}

.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-next {
	inset-inline-end: -0.75rem;
}

/*
 * NO DEAD CONTROL SHIPS TO A NO-JS READER.
 *
 * Without JS, Swiper never runs, swiper.css shows the first slide and clips the
 * rest, and the two arrows sit there inert. The plugin sets
 * `data-ftm-initialised="1"` on the gallery root when it initialises, so one
 * rule keyed off its absence covers the whole case.
 */
.tc-pdp
	.tc-pdp__gallery-mount
	.ftm-product-gallery:not([data-ftm-initialised])
	.ftm-product-gallery-nav {
	display: none;
}

@media (min-width: 48rem) {
	/*
	 * breakpoint/md — and it is where the PAGE GUTTER changes, which is the
	 * reason the overhang changes with it. 16 at tablet, 12 at mobile: the
	 * mobile gutter is 16, so a 16px overhang would put the button flush
	 * against the viewport edge.
	 */
	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-prev {
		inset-inline-start: -1rem;
	}

	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-next {
		inset-inline-end: -1rem;
	}
}

/* ─────────────────────────────────────────────────────────────────────────
 * The 64rem switch — thumbs appear, arrows go away
 *
 * The boards give two data points: desktop HAS a rail and NO arrows; tablet and
 * mobile have arrows and NO rail. Between 768 and 1920 they say nothing.
 *
 * The driver is not a guess. The rail only fits because the gallery is in an
 * 807px column, and that column only exists at 64rem — the `807fr / 529fr` split
 * at the top of this file. Below it the gallery is the full single-column width
 * and there is no room to give 119px to a rail. So the gallery's switch is the
 * COLUMN's switch, and there is exactly one boundary rather than two.
 *
 * ⚠ THIS ALSO CLOSES A SUB-PIXEL DEFECT IN THE PLUGIN'S OWN PAIR.
 * product-gallery.css:166-178 hides the thumbs at `max-width: 63.99em` and the
 * nav at `min-width: 64em`. 63.99em matches widths <= 1023.84px and 64em matches
 * >= 1024px, so widths in (1023.84, 1024) match NEITHER and both the rail and the
 * arrows render. Sub-pixel, but reachable at fractional browser zoom. Owning both
 * halves at a single `min-width: 64rem` closes it. (In a media query `em` and
 * `rem` both resolve against the initial font size, so 64em and 64rem are the
 * same width — this confirms the plugin's breakpoint rather than moving it.)
 * Assertion 6 in the spec checks 1023 and 1024 explicitly.
 * ───────────────────────────────────────────────────────────────────────── */

/* Mobile-first: no rail below 64rem, which is what both small boards draw. */
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-thumbs {
	display: none;
}

@media (min-width: 64rem) {
	/*
	 * ⚠ THE RAIL IS OUT OF FLOW, AND NEITHER GRID NOR FLEX WORKS HERE.
	 *
	 * 95 + 24 + 688 = 807 exactly at the 1400 boundary. The obvious way to write
	 * that is `grid-template-columns: 5.9375rem minmax(0, 1fr)`. It was written
	 * that way first, and it does not work, for a reason worth recording because
	 * it will recur on T-F's Swiper cross-sells:
	 *
	 * A SWIPER MEASURES ITS OWN CONTAINER AT INIT AND CACHES THE RESULT. Put one
	 * in a grid or flex track and it becomes an item whose automatic minimum size
	 * (`min-width: auto`) consults its own content — which for the main slider is
	 * a flex row of ten 100%-wide slides, and for the rail is ten 95px
	 * thumbnails. The track sizes correctly (measured: `95px 687.391px`) and the
	 * ITEMS still blow past it. `min-inline-size: 0` on both did not fix it:
	 * Swiper had already written `width: 3.35544e+07px` inline onto the slide —
	 * Chromium's infinity, cached from the first bad layout — and never
	 * recovered. The page rendered one image running off the right of the
	 * viewport, with the rail correct beside it.
	 *
	 * There is a second, independent problem with stretch that the same structure
	 * would have hit next: ten 95px thumbnails plus nine 8px gaps is 1,022px, so
	 * the RAIL's content would have sized the row and the square image would have
	 * been stretched to 1,022 tall. Seven products reach that.
	 *
	 * Absolute positioning answers both at once, and needs no measurement from
	 * anything:
	 *
	 *   - the rail is out of flow, so it contributes NOTHING to the gallery's
	 *     height; the gallery's height is the image's height, full stop;
	 *   - `inset-block: 0` then makes the rail exactly as tall as the image
	 *     (~688), which is the definite height a vertical Swiper needs and the
	 *     height the board draws;
	 *   - the main slider stays an ordinary block, so its width is its parent's
	 *     minus the indent — definite, and never shrink-to-fit.
	 *
	 * FIXED 95px, NOT 95fr, either way — a deliberate departure from the all-fr
	 * column model above. 95x95 is a CONTROL SIZE, sourced from Woo's 100x100
	 * `gallery_thumbnail`, not a proportion the design states. T-B's 807:529 is a
	 * ratio the board asserts; this is not, and letting the thumbnails shrink at
	 * 1024 would make them illegible for nothing.
	 *
	 * Caught by SCREENSHOT, not by the spec — the first spec checked the rail's
	 * width and the 24px gap and never measured the image, which is exactly the
	 * hole build-workflow §4 exists to close. The spec now measures it too.
	 */
	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-thumbs {
		display: block;
		inline-size: 5.9375rem; /* 95 */
		inset-block: 0;
		inset-inline-start: 0;
		margin-block-start: 0; /* the plugin's 12px stack gap, off in two columns */
		position: absolute;
	}

	/* 95 rail + 24 gap = 119, leaving 688 of an 807 column. */
	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-main {
		margin-inline-start: 7.4375rem;
	}

	/*
	 * `slidesPerView: 'auto'` reads slide HEIGHT in vertical mode, and swiper.css
	 * sets `.swiper-slide { height: 100% }` — which would make one thumbnail fill
	 * the whole rail. Squaring each slide off its own width is what makes 95x95
	 * come out, and what makes the 8px pitch Swiper's `spaceBetween` rather than
	 * a stack of full-height slides.
	 */
	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-thumb {
		aspect-ratio: 1 / 1;
		block-size: auto;
		inline-size: 100%;
	}

	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-thumb-image {
		block-size: 100%;
		inline-size: 100%;
		object-fit: contain;
	}

	/* Arrows are the small-screen affordance; the rail replaces them here. */
	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-nav {
		display: none;
	}
}

/* ─────────────────────────────────────────────────────────────────────────
 * Focus — the footer's finding, for the third time
 *
 * ⚠ THIS THEME GIVES <button> NO FOCUS RING AT ALL. `theme.json` declares
 * `:focus` outline in exactly three places and ALL THREE are under
 * `elements.link` — there is no declaration for `<button>` anywhere in the
 * global styles. So every button in every pattern falls back to Chromium's UA
 * ring, `outline: auto 1px rgb(16, 16, 16)`, near-black.
 *
 * That is a general fact about this theme, not about this build, and it is now
 * recorded as such in build-knowledge.md: EVERY PATTERN THAT SHIPS A BUTTON MUST
 * DECLARE ITS OWN FOCUS RING. It has now cost three builds — the footer's
 * accordion toggle and social icons, and these four controls.
 *
 * Four controls here, and one is worse than the footer's case:
 *
 *   prev / next      white page, straddling the image edge   undesigned
 *   thumbnail        white, beside a 2px secondary-600 ring  CONFUSABLE with
 *                                                            the active state
 *   expand           an arbitrary product photograph         unpredictable
 *   lightbox close   an 88%-BLACK SCRIM                      near-black on
 *                                                            near-black —
 *                                                            INVISIBLE
 *
 * The boards draw no focus state, so this SUPERSEDES them rather than deviating
 * from them — the same standing the footer fix and the header's hover treatment
 * already have.
 *
 * `--ftm-scheme-accent` on the page, and WHITE inside the lightbox, because the
 * scheme variable resolves against the page's scheme and the lightbox is a fixed
 * overlay that is not in it.
 * ───────────────────────────────────────────────────────────────────────── */

.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-nav:focus-visible,
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-expand:focus-visible,
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-thumb:focus-visible {
	outline: 2px solid var(--ftm-scheme-accent);
	outline-offset: 2px;
}

/*
 * ── THE MAGNIFIER CONTROL — TOP-RIGHT, SQUARE, NO BORDER (2026-09-01) ──────
 *
 * Design review. `plugins/ftm-blocks/assets/css/product-gallery.css:64` draws
 * it bottom-right as a 999px circle with `border: var(--ftm-gallery-border)`;
 * measured on a live PDP that computes `1px solid rgb(233, 236, 239)` at
 * `bottom: 12px; right: 12px`. The review asked for it at the top-right, as a
 * square with a radius, and with the border gone.
 *
 * ⚠ OVERRIDDEN HERE, NOT EDITED IN THE PLUGIN. `ftm-blocks` is shared across
 * the fleet and its gallery serves other sites; this is one theme's opinion
 * about where a control sits. The `.tc-pdp .tc-pdp__gallery-mount` scope is the
 * seam the focus rules above already established for exactly this reason.
 *
 * ⚠ `bottom: auto` IS REQUIRED, NOT TIDINESS. The plugin sets `bottom` and the
 * button is absolutely positioned; setting `top` alone leaves both edges bound
 * and the button stretches the full height of the frame.
 *
 * 2px, which is the radius every other control on this page carries — the Call
 * button, the accordion, the Klaviyo review button, the swatches. "Square with
 * a border-radius" against a `999px` pill is the change; matching the page's
 * own control radius is what stops it being a fourth arbitrary number.
 *
 * The background stays `--color-surface` white: it is what makes the glyph
 * legible over an arbitrary product photograph, and removing the border does
 * not remove that job.
 */
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-expand {
	border: 0;
	border-radius: 2px;
	inset-block-start: 0.75rem;
	inset-block-end: auto;
}

/*
 * The thumbnail's focus ring has to clear its own active ring, which sits at
 * `outline-offset: -2px` INSIDE the border box. At the shared +2px they read as
 * one thick two-colour band and neither state is legible. +4px separates them.
 */
.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-thumb:focus-visible {
	outline-offset: 4px;
}

.ftm-gallery-lightbox .ftm-gallery-lightbox-close:focus-visible {
	outline: 2px solid var(--wp--custom--color--base--white, #fff);
	outline-offset: 2px;
}

/* ═════════════════════════════════════════════════════════════════════════
 * ▼▼▼ SINGLE-IMAGE STATE — FENCE START ▼▼▼
 *
 * DELETING THIS FENCE RESTORES THE PLUGIN'S BEHAVIOUR. Nothing outside it
 * depends on it. That is deliberate: this is a flagged behavioural unknown, the
 * designer has not drawn it, and the answer must stay cheap when it arrives.
 *
 * ── WHAT IT IS FOR ─────────────────────────────────────────────────────────
 *
 * 1,091 of 1,171 published products — 93.2% — have exactly ONE slide. No board
 * draws that state. This is the DEFAULT state of the catalogue, not an edge
 * case, and the board's two-thumbnail rail is the exception.
 *
 * ── WHAT SHIPS WITHOUT IT, AND WHY THAT IS NOT ACCEPTABLE ──────────────────
 *
 * For a one-slide product the renderer emits, unconditionally, a main Swiper
 * with one slide, BOTH nav buttons, and a thumbnail Swiper with ONE thumbnail.
 * Swiper's Navigation module then adds `swiper-button-disabled`, sets the
 * `disabled` PROPERTY (they are <button>s, so they leave the tab order) and adds
 * `swiper-button-lock`.
 *
 * And they stay VISIBLE anyway, because two rules tie at (0,1,0) and print order
 * decides it in the plugin's favour:
 *
 *   .swiper-button-lock { display: none }            swiper.css          61 of 62
 *   .ftm-product-gallery-nav { display: inline-flex} product-gallery.css 62 of 62
 *
 * Nor does the disabled dimming apply — swiper.css scopes `opacity: .35` inside
 * `.swiper-button-next, .swiper-button-prev`, and the plugin's buttons carry
 * neither class. So the unfenced result on 1,091 product pages is two fully
 * opaque, undimmed, UNFOCUSABLE arrows that do nothing, beside a lone thumbnail
 * wearing a 2px "you are here" ring.
 *
 * That is a dead control, which this project has ruled against twice already —
 * the rejected `quantity-input.php` override, and product-grid's `aria-disabled`
 * treatment, which announces a spent control rather than removing it.
 *
 * ── WHY `:has()` AND NOT A COUNT IN PHP ────────────────────────────────────
 *
 * render.php could count slides and stamp a class, but it would have to rebuild
 * the featured-plus-gallery-plus-video merge that product-gallery.php:485-495
 * already does, and the two copies would drift the first time the plugin
 * changes. `:has()` reads the markup that actually shipped. Shipped precedent:
 * product-grid.css:604 uses `:not(:has(.woocommerce-Price-amount))` for the
 * price-less products.
 *
 * ── HOW TO CHANGE IT ───────────────────────────────────────────────────────
 *
 * To show a lone thumbnail instead: drop `.ftm-product-gallery-thumbs` from the
 * `:is()` below. To dim the arrows rather than remove them: swap `display: none`
 * for `opacity: .4; pointer-events: none` and have product-detail.js set
 * `aria-disabled` — product-grid.css:733-741 is the working precedent.
 * ═════════════════════════════════════════════════════════════════════════ */

.tc-pdp
	.tc-pdp__gallery-mount
	.ftm-product-gallery:not(
		:has(.ftm-product-gallery-slide + .ftm-product-gallery-slide)
	)
	:is(.ftm-product-gallery-thumbs, .ftm-product-gallery-nav) {
	display: none;
}

@media (min-width: 64rem) {
	/*
	 * THE IMAGE TAKES THE WHOLE 807 COLUMN when there is no rail.
	 *
	 * The board's 688 is a CONSEQUENCE of the rail taking 95 + 24 out of 807,
	 * not an independent value — so with the rail gone, 807 is the honest width
	 * and 688 would be an arbitrary one. The two alternatives both read as bugs:
	 * keeping the tracks leaves 119px of unexplained indent, and collapsing them
	 * while holding 688 leaves 119px of dead space to the right.
	 *
	 * ⚠ The board also indents Product Details to x=379 to align with the main
	 * image. T-C did not build that, on the grounds that under this rule the
	 * column edge is correct for 93% of products and 119 out for the other 7%.
	 * SUPERSEDED 2026-09-15 (P12, G31): the indent now exists, CONDITIONALLY,
	 * gated on this fence's own `:has()` test so it appears exactly when the
	 * rail does — see "THE INDENT" beside `.tc-pdp__prose` further down. Both
	 * states align with the image; neither is 119 out.
	 */
	.tc-pdp
		.tc-pdp__gallery-mount
		.ftm-product-gallery:not(
			:has(.ftm-product-gallery-slide + .ftm-product-gallery-slide)
		)
		.ftm-product-gallery-main {
		margin-inline-start: 0;
	}
}

/* ▲▲▲ SINGLE-IMAGE STATE — FENCE END ▲▲▲ */

/* ═════════════════════════════════════════════════════════════════════════
 * ▼▼▼ NO-IMAGE STATE — FENCE START ▼▼▼
 *
 * ONE product in the catalogue: #51164 `kona-5`. The renderer short-circuits at
 * product-gallery.php:497-502 and emits a bare
 * `.woocommerce-product-gallery--without-images` wrapping
 * `wc_placeholder_img( 'woocommerce_single' )` — note it does NOT carry the
 * `ftm-product-gallery` class, so none of the rules above reach it. No Swiper,
 * no rail, no arrows.
 *
 * ⚠ THE PLACEHOLDER FILE DOES NOT RESOLVE, AND WILL NOT AFTER MEDIA SYNC UNLESS
 * IT IS SYNCED TOO. `woocommerce-placeholder-640x640.png` is absent from
 * wp-content/uploads and requesting it returns a 302 to a remote host that
 * answers 401. So this box must stand up with a BROKEN IMAGE inside it, which is
 * why the fill, stroke and square are on the wrapper rather than on the img.
 *
 * `neutral-200` is not invented: it is what both the tablet and mobile boards
 * actually paint in the image area, bound to `color/neutral/200`, and it is the
 * closest thing the design states about an empty image slot. The box then reads
 * as deliberate emptiness whether or not the file ever arrives.
 * ═════════════════════════════════════════════════════════════════════════ */

.tc-pdp .tc-pdp__gallery-mount .woocommerce-product-gallery--without-images {
	align-items: center;
	aspect-ratio: 1 / 1;
	background: var(--wp--custom--color--neutral--200, #dee2e6);
	border: 1px solid var(--wp--custom--color--neutral--100, #e9ecef);
	border-radius: 4px;
	display: flex;
	justify-content: center;
	overflow: hidden;
}

.tc-pdp
	.tc-pdp__gallery-mount
	.woocommerce-product-gallery--without-images
	img {
	block-size: 100%;
	inline-size: 100%;
	object-fit: contain;
}

/* ▲▲▲ NO-IMAGE STATE — FENCE END ▲▲▲ */

/* ═══════════════════════════════════════════════════════════════════════════
 * T-D — THE BUY BOX. Variables, qty stepper, Add to Cart, shipping line.
 *
 * Figma: Variables 43324:17589 / 43324:17326 / 43324:17074 and Add to Cart
 * Section 43324:17577 / 43324:17314 / 43324:17062 (desktop / tablet / mobile),
 * canonical 43324:* set. Full account: docs/reports/pdp-buy-box-plan.md.
 *
 * ── ⚠ PRINT ORDER IS AGAINST THIS SECTION IN ALL THREE DIRECTIONS ───────────
 *
 * The rest of this file works to the standing (0,3,0) floor. That floor is NOT
 * enough here. Measured `document.styleSheets` order on the served page:
 *
 *    39  ftm-blocks/design-system/build/elements.css
 *    43  ftm-blocks/design-system/build/woocommerce.css        (the bridge)
 *    69  THIS FILE
 *    74  global-styles-inline-css
 *    80  woocommerce/assets/css/woocommerce.css
 *    85  woo-variation-swatches/assets/css/frontend.min.css
 *    86  woo-variation-swatches-inline-css                     (the :root sizes)
 *
 * Everything this section touches prints AFTER us, so a specificity tie is a
 * LOSS, three times over. The budgets that matter:
 *
 *    (0,4,0)  .woo-variation-swatches .variable-items-wrapper
 *               .variable-item:not(.radio-variable-item)          chip base
 *    (0,5,0)  …:not(.radio-variable-item).button-variable-item    chip font-size
 *    (0,7,0)  …:not(.radio).button-variable-item.selected:not(.no-stock)
 *    (0,4,2)  div.wc-block-add-to-cart-form.wc-block-add-to-cart-form--stepper
 *               form.cart div.wc-block-components-quantity-selector.quantity
 *    (0,3,1)  .wp-block-woocommerce-add-to-cart-form form.cart >
 *               .single_add_to_cart_button                        justify-self
 *
 * ── AND THE ESCAPE, WHICH IS WHY THIS IS NOT A SELECTOR WAR ─────────────────
 *
 * The swatches plugin drives almost every chip appearance property through a
 * CUSTOM PROPERTY with a literal fallback:
 *
 *    box-shadow: var(--wvs-item-box-shadow, 0 0 0 1px #A8A8A8);
 *    width:      var(--wvs-single-product-item-width, 30px);
 *
 * Custom properties resolve by INHERITANCE PROXIMITY, not by cascade order or
 * specificity. `.tc-pdp` is a nearer ancestor of the chip than the plugin's own
 * `:root` block at print index 86, so the values below win outright and the
 * (0,7,0) selector above never has to be beaten. Anything the plugin exposes as
 * a token is set as a token; only the handful of properties with no token get a
 * selector, and those carry `.tc-pdp .tc-pdp__summary .tc-pdp__cart` (three
 * classes) as a prefix so the budget is legible at every rule.
 *
 * `.tc-pdp__cart` is a `className` on the add-to-cart block in
 * product-detail.php — a theme-owned hook on third-party output, the same
 * mitigation `.tc-pdp__gallery-mount` provides for the gallery. It is what the
 * regression spec keys off, so it must not be renamed casually.
 *
 * ── ⚠ WHAT MUST NOT HAPPEN HERE, FOR T-F's SAKE ─────────────────────────────
 *
 * `.tc-pdp__summary` is a grid ITEM and is deliberately NOT a grid or flex
 * CONTAINER. The obvious way to state the rhythm below would be
 * `display: grid; row-gap` on the summary. DO NOT. T-F puts a second Swiper in
 * this column ("Recommended For This Application", 43324:17558, y=848), and T-C
 * measured a Swiper in a grid track computing 33,554,433px — Chromium's
 * infinity — because the track item's `min-width: auto` consults its own content
 * and `min-inline-size: 0` does not save it, since Swiper writes the width
 * inline at init. The rhythm below is therefore margins, not gaps.
 * ═══════════════════════════════════════════════════════════════════════════ */

/*
 * SWATCH TOKENS. Set on `.tc-pdp` so they reach the chips by proximity and stop
 * at the pattern boundary — the archive swatches (Shop Archive, unbuilt) inherit
 * nothing from here, which is the "audit both directions" half of the budget.
 *
 * `width: auto` cannot be set through `add_theme_support()`: the plugin runs the
 * option through `absint()` (class-woo-variation-swatches-product-page.php:268),
 * so `auto` becomes 0. It has to be the custom property. Height and font-size
 * could go either way and are kept here so the three read together.
 *
 * ⚠ `inset`, NOT the plugin's outset default. Figma draws the chip stroke INSIDE
 * the 44px box (12 + 20 + 12 = 44 with the border drawn inward), and the
 * selected chip steps 1px -> 2px without changing size. An outset shadow grows
 * the visual edge instead, so the selected chip reads a pixel larger than its
 * neighbours and the row's optical baseline breaks.
 */
.tc-pdp {
	--wvs-single-product-item-font-size: 0.875rem;
	--wvs-single-product-item-height: 2.75rem;
	--wvs-single-product-item-width: auto;
	--wvs-item-background-color: var(--wp--custom--color--base--white, #fff);
	--wvs-item-text-color: var(--wp--custom--color--base--black, #0a205b);
	--wvs-item-box-shadow: inset 0 0 0 1px var(--wp--custom--color--neutral--300, #ced4da);
	--wvs-hover-item-background-color: var(--wp--custom--color--base--white, #fff);
	--wvs-hover-item-text-color: var(--wp--custom--color--base--black, #0a205b);
	--wvs-hover-item-box-shadow: inset 0 0 0 1px var(--wp--custom--color--neutral--500, #6c757d);
	--wvs-selected-item-background-color: var(--wp--custom--color--base--white, #fff);
	--wvs-selected-item-text-color: var(--wp--custom--color--base--black, #0a205b);
	--wvs-selected-item-box-shadow: inset 0 0 0 2px var(--wp--custom--color--secondary--600, #057db9);

	/*
	 * Stepper glyphs. The board draws `Math / Minus` and `Math / Plus` as 20x20
	 * icon instances; WooCommerce emits the text characters U+2212 MINUS SIGN
	 * and `+`. The text is masked out and these are painted in its place, which
	 * is this theme's only icon mechanism — there is no PHP icon helper and no
	 * shared layer, so each pattern declares its own (build-knowledge.md §6).
	 *
	 * Both geometries are read off the board's own insets rather than eyeballed:
	 * Minus is inset 45.83% vertically and 16.67% horizontally (4..20 and 11..13
	 * in a 24 box); Plus is inset 16.67% on all four sides (4..20 both axes).
	 */
	--tc-icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 11h16v2H4z'/%3E%3C/svg%3E");
	--tc-icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M11 4h2v7h7v2h-7v7h-2v-7H4v-2h7z'/%3E%3C/svg%3E");

	/*
	 * The details accordion's chevron (T-E). `Arrows / Chevron Down`, 24x24, with
	 * the glyph inset 33.33% vertically and 20.83% horizontally per the board —
	 * 5..19 across and 8..16 down in a 24 box. Since 2026-09-28 the path is the
	 * component's own export (542:31243), byte-identical to every other copy of
	 * the slug; the earlier redraw measured 58.3 x 30.8 against the export's
	 * 58.3 x 33.3. docs/reports/icon-audit.md §7.
	 *
	 * ⚠ NOT the same glyph as `--tc-icon-chevron-left` five hundred lines below,
	 * which is the gallery's and is a different path. This theme has no shared
	 * icon layer; each pattern declares its own (build-knowledge.md §6), and this
	 * pattern now declares five.
	 */
	--tc-icon-chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5.29289 8.29289C5.65338 7.93241 6.22061 7.90468 6.6129 8.2097L6.70711 8.29289L12 13.585L17.2929 8.29289C17.6534 7.93241 18.2206 7.90468 18.6129 8.2097L18.7071 8.29289C19.0676 8.65338 19.0953 9.22061 18.7903 9.6129L18.7071 9.70711L12.7071 15.7071C12.3466 16.0676 11.7794 16.0953 11.3871 15.7903L11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z'/%3E%3C/svg%3E");

	/*
	 * The shipping line's truck. Board node `shipping-fast 1`, 43324:17586.
	 *
	 * ⚠ REPLACED 2026-09-15 (P12, G28/G29) WITH THE DESIGNER'S OWN EXPORT.
	 * What shipped before was a hand-authored outline — redrawn from a 962px PNG
	 * in which the icon was twelve pixels wide, and flagged as exactly that on
	 * 2026-09-01. Gary read it against the mockup as "distorted and incomplete",
	 * and it was: an open cargo box, a squat separate cab, two floating speed
	 * lines, and ink that filled 84% x 69% of the box while sitting 4px low.
	 * None of that was the mask's doing. It was the drawing.
	 *
	 * The source of record is now `assets/images/icons/truck.svg`: a 20x19
	 * viewBox, ONE filled compound path, fill #0A205B, committed with this rule.
	 * The value below is that file transcribed to a data URI, because every
	 * masked icon in this theme is an inline data URI (build-knowledge.md §6)
	 * and the files under assets/images/icons/ are consumed as <img> by
	 * grid-with-icons, never as masks. A file-URL mask for this one icon would
	 * be a second convention. If the file changes, re-encode it here.
	 *
	 * ⚠ TWO ENCODING TRAPS, EACH OF WHICH RENDERS AN EMPTY MASK:
	 *   1. The `#` in the fill MUST be `%23`. A raw `#` inside a data URI
	 *      starts a fragment, the SVG body ends there, and nothing paints.
	 *   2. The path's `fill` MUST STAY, because the root carries `fill='none'`
	 *      as exported. Strip it "to avoid the # problem" and the path inherits
	 *      none, paints nothing, and the mask is empty again. The colour is
	 *      irrelevant — `mask` reads alpha, and the pseudo-element's
	 *      `background-color: currentcolor` supplies the navy — but the path
	 *      has to be opaque.
	 *
	 * G29 ("size to 20px") needed no second change. The ::before box was 20x20
	 * all along — buy-box-verify test 14 pins it — and only the ink inside it
	 * was small. Under `contain` this asset scales to 20 wide by 19 tall,
	 * centred: measured ink 20 x 18.25 at the alpha threshold, offset 1.0 top
	 * and 0.75 bottom, identical at 1920/1024/768/320. DECISIONS 2026-09-15.
	 */
	--tc-icon-shipping: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 19' fill='none'%3E%3Cpath d='M20 7.5C20 5.2025 18.1308 3.33333 15.8333 3.33333H14.1667V2.5C14.1667 1.12167 13.045 0 11.6667 0H10V1.66667H11.6667C12.1267 1.66667 12.5 2.04083 12.5 2.5V13.3333H1.66667V10H0V15H1.6975C1.6775 15.1375 1.66667 15.2758 1.66667 15.4167C1.66667 17.025 2.975 18.3333 4.58333 18.3333C6.19167 18.3333 7.5 17.025 7.5 15.4167C7.5 15.2758 7.48917 15.1375 7.46917 15H12.5308C12.5108 15.1375 12.5 15.2758 12.5 15.4167C12.5 17.025 13.8083 18.3333 15.4167 18.3333C17.025 18.3333 18.3333 17.025 18.3333 15.4167C18.3333 15.2758 18.3225 15.1375 18.3025 15H20V7.5ZM5.83333 15.4167C5.83333 16.1058 5.2725 16.6667 4.58333 16.6667C3.89417 16.6667 3.33333 16.1058 3.33333 15.4167C3.33333 15.2592 3.36417 15.12 3.40917 15H5.75833C5.80333 15.12 5.83333 15.2592 5.83333 15.4167ZM15.8333 5C17.2117 5 18.3333 6.12167 18.3333 7.5V8.33333H14.1667V5H15.8333ZM16.6667 15.4167C16.6667 16.1058 16.1058 16.6667 15.4167 16.6667C14.7275 16.6667 14.1667 16.1058 14.1667 15.4167C14.1667 15.2592 14.1975 15.12 14.2425 15H16.5917C16.6367 15.12 16.6667 15.2592 16.6667 15.4167ZM14.1667 13.3333V10H18.3333V13.3333H14.1667ZM8.33333 1.66667H0V0H8.33333V1.66667ZM6.66667 5H0V3.33333H6.66667V5ZM5 8.33333H0V6.66667H5V8.33333Z' fill='%230A205B'/%3E%3C/svg%3E");
}

/*
 * THE SECTION RULE ABOVE THE BUY BOX.
 *
 * Board: `border-block-start: 1px #DEE2E6` with 24px of padding under it
 * (16 at mobile), and 24px of margin above (16 at mobile) separating it from the
 * price. Measured off the tablet board — Frame 203 ends at y=1090 and Variables
 * starts at 1114 — and the mobile board, 651 -> 667.
 *
 * It sits on the BLOCK WRAPPER rather than on `table.variations`, because 1,120
 * of the 1,171 published products are simple and have no variations table at
 * all. On those the rule still needs to separate the price from the qty row.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart {
	border-block-start: 1px solid var(--wp--custom--color--neutral--200, #dee2e6);
	margin-block-start: 1rem;
	padding-block-start: 1rem;
}

/*
 * THE FORM GRID — restated, not unpicked.
 *
 * Woo declares `display: grid; grid-auto-flow: column;
 * grid-template-columns: min-content auto auto` on `form.cart` and
 * `.variations_button`, then places children with `grid-column: 1/-1` and the
 * submit button with `grid-column: 2/3`. Measured at 1440 that resolved to four
 * tracks, `51.9px 207px 207px 50px`.
 *
 * Those PLACEMENTS are already what the board wants once the tracks are right:
 * `auto minmax(0, 1fr)` puts the 120px stepper in column 1 and lets the button
 * fill column 2, which is exactly the board's row at all three widths
 * (120 + 16 + button = the column, exactly, at 529 / 688 / 288). So only the
 * template and the flow direction change. Unpicking the placements one at a time
 * would be more rules and would fail subtly rather than visibly.
 *
 * BOTH containers get the same grid because the qty row's parent differs by
 * product type: on a variable product it is `.variations_button`, nested inside
 * `.single_variation_wrap`; on a simple product the controls are direct children
 * of `form.cart`. Giving `form.cart` two tracks is harmless in the variable case
 * — every one of its children carries Woo's `grid-column: 1/-1` and spans both.
 *
 * `minmax(0, …)` on the second track for the usual reason: a grid item's default
 * `min-width: auto` refuses to shrink below its content, and a long button label
 * would otherwise push the row wider than the column.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart form.cart,
.tc-pdp .tc-pdp__summary .tc-pdp__cart .variations_button {
	align-items: center;
	column-gap: 1rem;
	display: grid;
	grid-auto-flow: row;
	grid-template-columns: auto minmax(0, 1fr);
	margin: 0;
	row-gap: 1.5rem;
}

/*
 * ⚠ ON A VARIABLE PRODUCT, `form.cart` HAS EXACTLY ONE CHILD.
 *
 * The swatches plugin wraps the whole form body in a bare `<div>` carrying only
 * data attributes — `before_variations_form()` / `after_variations_form()`,
 * class-woo-variation-swatches-product-page.php:57-74. So the grid above has one
 * item on a variable product and its `row-gap` spaces nothing; the real stack of
 * variations-table-then-controls lives a level down.
 *
 * Measured: the board's 32px between the Variables section and the controls came
 * out at 0, and `form.cart` reported exactly one child, a DIV. Every reading
 * before that had been of the wrong element.
 *
 * There is no class to hook, so this uses the attribute the plugin does emit.
 * `display: contents` was the tempting alternative and is wrong: selectors match
 * the DOM, not the box tree, so Woo's `form.cart > :not(.quantity)` placement
 * rules would stop applying to the promoted grandchildren and the table would
 * land in column 1 beside the controls.
 *
 * Simple products have no wrapper — the controls are direct children of
 * `form.cart` — which is why both levels carry a grid.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart form.cart > div[data-product_id] {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	row-gap: 1.5rem;
}

/*
 * ⚠ WOO'S CLEARFIX PSEUDO-ELEMENTS BECOME GRID ROWS.
 *
 * `.woocommerce div.product form.cart::before/::after` are a float clearfix from
 * the classic stylesheet — `content: " "; display: table` — and in a grid
 * container a generated box is a GRID ITEM. Woo half-knows this: its own
 * blocktheme rule gives `form.cart:before` a `grid-column: 1/-1` so it at least
 * spans. It still takes a row, and with `row-gap: 2rem` that is a silent 32px
 * above the first control, on every product page.
 *
 * Measured before this rule: the variations table started 32px lower than the
 * block's padding accounted for. It reads as "the spacing is a bit loose" rather
 * than as a phantom element, which is why it is called out here.
 *
 * (0,4,1) — the doubled class clears Woo's (0,3,2).
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart form.cart.cart::before,
.tc-pdp .tc-pdp__summary .tc-pdp__cart form.cart.cart::after,
.tc-pdp .tc-pdp__summary .tc-pdp__cart .variations_button.variations_button::before,
.tc-pdp .tc-pdp__summary .tc-pdp__cart .variations_button.variations_button::after {
	display: none;
}

/*
 * `.single_variation_wrap` holds the variation price/availability div and the
 * qty row. The price div is EMPTY until a variation is chosen, and an empty
 * block still occupies a grid row, so it is collapsed rather than left to add a
 * phantom gap above the controls on first paint.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart .single_variation_wrap {
	display: grid;
	row-gap: 1rem;
}

.tc-pdp .tc-pdp__summary .tc-pdp__cart .woocommerce-variation:empty {
	display: none;
}

/*
 * THE VARIABLES BLOCK.
 *
 * Woo already sets `display: block` on `table.variations` and marks it
 * `role="presentation"`, so there is no table semantics to undo — only the
 * remaining row/cell display values. `tbody` becomes the stack of attributes
 * (two of them on #65156, the only multi-attribute product in the catalogue),
 * and each `tr` is the board's 12px-gap column of label row then chip row.
 */

/*
 * (0,5,2). `.woocommerce div.product form.cart .variations` sets
 * `margin-bottom: 1em` at (0,4,2) from woocommerce.css, print 80 — so a (0,4,2)
 * restatement here would tie and lose. The doubled class is the cheapest way to
 * clear it, and the 18px it was adding sat between the chip row and the controls
 * on top of the grid's own 32px.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart form.cart table.variations.variations {
	margin: 0;
}

.tc-pdp .tc-pdp__summary .tc-pdp__cart table.variations tbody {
	display: grid;
	row-gap: 1.5rem;
}

.tc-pdp .tc-pdp__summary .tc-pdp__cart table.variations tr {
	display: grid;
	position: relative;
	row-gap: 0.75rem;
}

/*
 * The label row: attribute label and the selected value, 12px apart, both
 * Label/Medium at 14/20 with the label bold. `th` defaults to
 * `text-align: center`, which flex does not override on its own.
 *
 * ⚠ `.woo-selected-variation-item-name` IS INJECTED BY THE PLUGIN'S JS, not
 * served in the HTML. It is empty until a chip is chosen. `display: flex` is not
 * decorative: `variation_label_separator` is set to '' in
 * includes/woocommerce.php, and the plugin's JS still writes
 * `"<separator> <value>"` (frontend.js:257), so the text node keeps a LEADING
 * SPACE that would render on top of the 12px gap. A flex container collapses it.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart table.variations th.label {
	align-items: flex-start;
	color: var(--wp--custom--color--base--black, #0a205b);
	column-gap: 0.75rem;
	display: flex;
	flex-wrap: wrap;
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.875rem;
	font-weight: 400;
	line-height: 1.4286;
	padding: 0;
	text-align: start;
}

.tc-pdp .tc-pdp__summary .tc-pdp__cart table.variations th.label label {
	font-weight: 700;
	margin: 0;
}

.tc-pdp .tc-pdp__summary .tc-pdp__cart .woo-selected-variation-item-name {
	display: flex;
	font-weight: 500;
}

.tc-pdp .tc-pdp__summary .tc-pdp__cart table.variations td.value {
	display: grid;
	padding: 0;
	row-gap: 0.75rem;
}

/*
 * THE CHIP ROW.
 *
 * `gap: 8px` replaces the plugin's `margin: 4px` per item. The margin is not
 * only redundant once there is a gap — it also insets the ROW by 4px from the
 * column edge, so the chips sat 4px right of the price and the stepper. Measured
 * before the fix: wrapper x=884, first chip x=888.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart .variable-items-wrapper {
	gap: 0.5rem;
}

/*
 * (0,5,0) — one more than the plugin's chip base at (0,4,0), because a tie at
 * print 69 against print 85 is a loss. Only the two properties with no custom
 * property equivalent are here; everything else is a token on `.tc-pdp` above.
 */
.tc-pdp
	.tc-pdp__summary
	.variable-items-wrapper
	.variable-item.button-variable-item {
	margin: 0;
	padding: 0.75rem;
}

/*
 * The inner span carries the text. Its `line-height` is NOT set by the plugin,
 * so it inherits the theme's fluid body leading — measured 36px at 1440 and 32px
 * at 320 against the board's 20px, which is what made the chips read as loose.
 * The 5px horizontal padding is the plugin's and is folded into the li's 12px.
 */
.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	.variable-items-wrapper
	.variable-item.button-variable-item
	.variable-item-span.variable-item-span-button {
	font-family: var(--wp--preset--font-family--body);
	font-weight: 500;
	line-height: 1.4286;
	padding: 0;
}

/*
 * Selected chips go bold.
 *
 * ⚠ (0,9,0), AND THE RULE IT HAS TO BEAT IS THE ONE DIRECTLY ABOVE. The plugin
 * sets no `font-weight` anywhere, so the first version of this was (0,5,0) and
 * looked ample — and it lost to our OWN (0,8,0) span rule, which had to go that
 * high to clear the plugin's `padding: 0 5px`. Caught by the regression spec,
 * not by reading.
 *
 * That is the "audit the budget in BOTH directions" lesson landing on the author
 * rather than on a third party: escalating one rule to clear a plugin quietly
 * raises the floor for every later rule on the same element.
 */
.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	.variable-items-wrapper
	.variable-item.button-variable-item.selected
	.variable-item-span.variable-item-span-button {
	font-weight: 700;
}

/*
 * "Clear" — Woo's reset link, which the board does not draw.
 *
 * ⚠ IT IS TAKEN OUT OF FLOW, AND THAT IS NOT A TIDY-UP. Woo hides it with
 * `visibility: hidden` rather than `display: none`
 * (wc-add-to-cart-variation.js), so its box is reserved on EVERY product page
 * whether or not a variation is chosen. In flow it sat 12px under the chip row
 * and added 21px of its own, so the board's 32px between the Variables section
 * and the controls measured 65px — and the page looked merely "a bit loose"
 * rather than wrong, which is how this kind of thing survives a review.
 *
 * Removing the link instead was the other option and it removes a function:
 * on a product with a chosen variation, Clear is the only way back to the
 * "Choose an option" state without reloading.
 *
 * So it moves to the trailing edge of the label row, next to the attribute it
 * clears. That is a small design addition on top of a board that omits the
 * control entirely — flagged for the designer, not silently invented.
 *
 * The `tr` is `position: relative` (see above) so this anchors to the attribute
 * row, which also means the two-attribute product gets it on the right row.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart .reset_variations {
	color: var(--wp--custom--color--secondary--700, #0369a1);
	font-size: 0.875rem;
	inset-block-start: 0;
	inset-inline-end: 0;
	line-height: 1.4286;
	position: absolute;
}

/*
 * THE QTY STEPPER — 120 x 48, identical at all three breakpoints.
 *
 * The board never changes this control; only the button beside it flexes
 * (393 / 552 / 152). So there is no breakpoint here on purpose.
 *
 * (0,6,2), which is what it takes to beat Woo's
 * `div.wc-block-add-to-cart-form.wc-block-add-to-cart-form--stepper form.cart
 * div.wc-block-components-quantity-selector.quantity` at (0,4,2) — that rule
 * sets `width: unset`, `background-color: #fff` and `margin-right: .5em`, and it
 * prints at 18, ahead of us, so specificity is the only lever.
 *
 * `.wc-block-components-quantity-selector` is also styled by the design-system
 * bridge at `:root .wc-block-components-quantity-selector` (0,2,0),
 * ftm-blocks/design-system/build/woocommerce.css:137. Those rules had never
 * rendered before this build — they were written for a stepper the site did not
 * have — and they set border-color and radius from the scheme roles. This wins
 * where the board disagrees (the board's 2px radius is not the scheme's `sm`).
 */
.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	form.cart
	div.quantity.wc-block-components-quantity-selector {
	align-items: center;
	background-color: var(--wp--custom--color--base--white, #fff);
	block-size: 3rem;
	border: 1px solid var(--wp--custom--color--neutral--300, #ced4da);
	border-radius: 2px;
	column-gap: 1rem;
	grid-column: 1;
	inline-size: 7.5rem;
	justify-content: center;
	margin: 0;
	padding-inline: 0.75rem;
}

/*
 * The number itself: the board's 24px cell, Label/Extra Large at 16/24.
 *
 * `flex: 0 0 auto` IS LOAD-BEARING. Woo ships `flex: 1 1 auto; min-width: 40px`
 * on this input; `min-inline-size: 0` below beats the min-width, and the input
 * then shrank to 22px inside the 120px group. Fixing the basis is what makes the
 * seven internal measurements sum to the board exactly:
 * 12 + 20 + 16 + 24 + 16 + 20 + 12 = 120.
 *
 * ⚠ DESIGNER FLAG — 24px HOLDS TWO DIGITS. At 16px Inter a third digit scrolls
 * inside the field rather than widening it, and this catalogue sets no maximum
 * quantity on any product. The board draws the control at 120px with a 24px cell
 * and frames are canonical, so that is what ships; widening it silently would
 * break the drawn geometry the rest of the row depends on. If three-digit
 * quantities matter, the GROUP has to grow and that is a design decision.
 *
 * ⚠ (0,8,3), THE HIGHEST BUDGET IN THIS FILE, AND IT IS NOT DEFENSIVE PADDING.
 * The stepper mode ships its own rule at (0,7,2):
 *
 *   div.wc-block-add-to-cart-form.wc-block-add-to-cart-form--stepper
 *     .wc-block-components-quantity-selector
 *     input[type=number].input-text.qty.text
 *
 * which sets `font-size: var(--wp--preset--font-size--small)`, `padding: .9rem 0`
 * and `border: unset`. It only exists in the stepper branch, so nothing on this
 * site had ever met it before this build. The type role read 13px against the
 * board's 16 until this selector cleared it — caught by the spec, which is the
 * whole argument for asserting type roles separately.
 */
.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	form.cart
	div.quantity.wc-block-components-quantity-selector
	input.wc-block-components-quantity-selector__input.qty {
	background: none;
	border: 0;
	color: var(--wp--custom--color--base--black, #0a205b);
	flex: 0 0 auto;
	font-family: var(--wp--preset--font-family--body);
	font-size: 1rem;
	font-weight: 500;
	inline-size: 1.5rem;
	line-height: 1.5;
	min-inline-size: 0;
	padding: 0;
	text-align: center;
}

/*
 * THE − AND + BUTTONS.
 *
 * Woo emits the characters U+2212 and `+` inside the buttons at `.9em` and
 * `opacity: .6`; the board draws 20x20 icon instances. The text is suppressed
 * with `font-size: 0` and the glyph painted as a mask, so the ACCESSIBLE NAME is
 * untouched — it comes from Woo's own translated `aria-label` ("Reduce quantity
 * of …" / "Increase quantity of …"), not from the visible character.
 *
 * ⚠ DOM ORDER IS input, minus, plus. `add_steppers()` (AddToCartForm.php:75-99)
 * runs one regex twice and both passes insert AFTER the input; the visual order
 * is restored entirely by Woo's flex `order: 1 / 2 / 3`. Any rule that takes
 * `.quantity` off `display: flex` silently reverses the control, which is why
 * the spec asserts x-positions rather than DOM order.
 */
.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	form.cart
	.wc-block-components-quantity-selector__button {
	align-items: center;
	background: none;
	block-size: 1.25rem;
	border: 0;
	color: var(--wp--custom--color--base--black, #0a205b);
	display: inline-flex;
	flex: 0 0 auto;
	font-size: 0;
	inline-size: 1.25rem;
	justify-content: center;
	margin: 0;
	min-inline-size: 0;
	opacity: 1;
	padding: 0;
}

/*
 * ⚠ THE MASK GOES ON A PSEUDO-ELEMENT, NOT ON THE BUTTON — AND THAT IS A
 * FOCUS-RING BUG, NOT A STYLE PREFERENCE.
 *
 * The first version masked the button itself. CSS masking clips the element's
 * ENTIRE rendering, and that includes its outline. So the focus ring below was
 * computed exactly as declared — `outline: 2px solid` — and painted nowhere.
 *
 * ⚠⚠ THE REGRESSION SPEC PASSED WHILE THIS WAS BROKEN. `getComputedStyle`
 * reports the declared outline whether or not a mask erases it, so every
 * assertion about the ring was true and the control was still invisible to a
 * keyboard user. It was found by looking at a 3x screenshot of the focused
 * button, which is why assertion 26 in the spec now compares PIXELS either side
 * of focus rather than computed values.
 *
 * Same class of finding as T-C's: the measurements all agreed and the page was
 * wrong.
 */
.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	form.cart
	.wc-block-components-quantity-selector__button::before {
	background-color: currentcolor;
	block-size: 1.25rem;
	content: "";
	inline-size: 1.25rem;
}

.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	form.cart
	.wc-block-components-quantity-selector__button--minus::before {
	-webkit-mask: var(--tc-icon-minus) no-repeat center / contain;
	mask: var(--tc-icon-minus) no-repeat center / contain;
}

.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	form.cart
	.wc-block-components-quantity-selector__button--plus::before {
	-webkit-mask: var(--tc-icon-plus) no-repeat center / contain;
	mask: var(--tc-icon-plus) no-repeat center / contain;
}

/*
 * TAP TARGETS ON PHONES (QA batch A, 2026-09-27). The − and + measured 20×20
 * and the input 24×24 at 360-430, against the 44×44 the phone brief requires.
 * The drawn stepper does not change: 12 + 20 + 16 + 24 + 16 + 20 + 12 = 120.
 *
 * − / + get a transparent 44-wide ::after (the glyph owns ::before), starting
 * 12px before the button — i.e. at the group's inner edge — and 12px above
 * it. Measured in group coordinates they cover 1-45 and 77-121, 44×44 each,
 * and a tap there is a tap on the button.
 *
 * THE INPUT CANNOT ALSO REACH 44. Three 44-wide targets need 132 and the board
 * draws 120, so the input keeps what is left between them: it is widened to
 * 32 with -4px inline margins (the digit stays centred on the same pixel) and
 * made 48 tall, so the whole strip between the two button targets focuses the
 * field. 32×48 is a RECORDED EXCEPTION to the 44 brief, accepted by the repo
 * owner — DECISIONS.md, 2026-09-27 (QA batch A). Widening the group instead
 * would move off the frame's 120.
 */
@media (max-width: 47.9375rem) {
	.tc-pdp
		.tc-pdp__summary
		.tc-pdp__cart
		form.cart
		.wc-block-components-quantity-selector__button {
		position: relative;
	}

	.tc-pdp
		.tc-pdp__summary
		.tc-pdp__cart
		form.cart
		.wc-block-components-quantity-selector__button::after {
		block-size: 44px;
		content: "";
		inline-size: 44px;
		inset-block-start: -12px;
		inset-inline-start: -12px;
		position: absolute;
	}

	.tc-pdp
		.tc-pdp__summary
		.tc-pdp__cart
		form.cart
		div.quantity.wc-block-components-quantity-selector
		input.wc-block-components-quantity-selector__input.qty {
		block-size: 3rem;
		inline-size: 2rem;
		margin-inline: -0.25rem;
	}
}

/*
 * ADD TO CART.
 *
 * The board's `secondary-600`, restored 2026-08-06. It shipped at
 * `secondary-700` because the OLD secondary-600 (#0284C7) measured 4.09:1 on
 * white and FAILED WCAG AA. Design moved secondary-600 to #057DB9 for ADA on
 * 2026-08-06 and white on it is 4.53:1, so the standing rule is closed and this
 * is back on the board's binding — product-grid did the same. Note for anyone
 * diffing a hex against the frame: the frame's pixel is still #0284C7, and the
 * token it named is now #057DB9. The binding is what ships.
 *
 * The background is otherwise inherited correctly: theme.json now declares
 * `styles.elements.button`, so the (0,1,0) tie that rendered #32373C is gone.
 * This rule exists to move the CTA from the scheme accent to the board's sky,
 * not to fight anything.
 *
 * `justify-self: stretch` overrides Woo's `justify-self: start` at (0,3,1),
 * which is what kept the button at its 146px content width instead of filling
 * the track the board gives it.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__cart .single_add_to_cart_button {
	background-color: var(--wp--custom--color--secondary--600, #057db9);
	border-radius: 2px;
	color: var(--wp--custom--color--base--white, #fff);
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.9375rem;
	font-weight: 500; /* G01, corrected to 500 on 2026-09-15 */
	justify-self: stretch;
	letter-spacing: 0.002em;
	line-height: 1.6;
	min-block-size: 3rem;
	padding: 0.75rem 1rem;
	text-align: center;
	width: 100%;
}

.tc-pdp .tc-pdp__summary .tc-pdp__cart .single_add_to_cart_button:hover {
	background-color: var(--wp--custom--color--secondary--800, #075985);
}

/*
 * ── CALL FOR PRICE — the outline sibling of the button above ────────────────
 *
 * 409 of 1,171 published products carry no price, and WooCommerce emits no cart
 * form for them at all. includes/product-call-for-price.php fills that empty
 * slot with a `tel:` button; this is its appearance.
 *
 * ⚠ IT IS THE OUTLINE TREATMENT AND THE CARD'S IS FILLED. NOT AN OVERSIGHT.
 * `docs/design/product-grid/desktop.png` draws the price-less CARD's Call
 * button as a solid secondary-600 button identical to Add to Cart, so the card
 * follows the frame. No frame covers a price-less PDP — all three
 * `docs/design/product-detail/` exports draw a priced product — so here the
 * brief ("style as the house outline button") is the only authority and it is
 * followed. Flagged for the designer to settle; see
 * docs/reports/pre-client-review-fixes.md.
 *
 * EVERY BOX PROPERTY IS COPIED FROM `.single_add_to_cart_button` ABOVE rather
 * than inherited, because there is nothing to inherit from: this element is
 * NOT a `.single_add_to_cart_button` (it is not an add-to-cart, and saying so
 * in a class name would be false), and Woo's own form never renders here. The
 * numbers are the same numbers — 48px box, 2px radius, label-large at 700 —
 * so the two buttons occupy an identical footprint whichever one a product
 * gets, which is what stops the summary column reflowing between them.
 *
 * The border comes OUT of the padding rather than adding to it — the same
 * correction footer.css:216 documents for its own outline variant — so this
 * stands exactly 48 tall beside the filled one rather than 50.
 *
 * ⚠ THE SELECTOR DELIBERATELY DOES NOT MENTION `.tc-pdp__cart`, and the wrapper
 * the PHP emits deliberately does not carry it. The rule 80 lines below —
 * `.tc-pdp__summary:not(:has(.tc-pdp__cart)) .tc-pdp__shipping` — hides the
 * shipping lead-time line on exactly these products, and reusing the cart class
 * here would have switched that promise back on for all 409 of them. It did, in
 * the first cut of this work, and assertion 5 of
 * product-detail-buy-box-verify.spec.ts caught it. `.tc-pdp__call-slot` is its
 * own hook for that reason and must stay its own hook.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__call {
	display: block;
	box-sizing: border-box;
	border: 1px solid var(--wp--custom--color--secondary--600, #057db9);
	border-radius: 2px;
	background-color: transparent;
	padding: calc(0.75rem - 1px) calc(1rem - 1px);
	min-block-size: 3rem;
	inline-size: 100%;
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.9375rem; /* label-large */
	font-weight: 700;
	line-height: 1.6;
	letter-spacing: 0.002em;
	color: var(--wp--custom--color--secondary--600, #057db9);
	text-align: center;
	text-decoration: none;
}

.tc-pdp .tc-pdp__summary .tc-pdp__call:hover,
.tc-pdp .tc-pdp__summary .tc-pdp__call:focus-visible {
	border-color: var(--wp--custom--color--secondary--800, #075985);
	background-color: var(--wp--custom--color--secondary--800, #075985);
	color: var(--wp--custom--color--base--white, #fff);
}

/*
 * THE SHIPPING LINE — "Most orders ship same business day".
 *
 * Board 43324:17585: a 20x20 `shipping-fast` icon and Label/Medium text 8px
 * apart, sitting 24px under the controls (16 at mobile).
 *
 * The icon is a `::before` mask rather than markup, because it carries no
 * information — the sentence beside it says everything. A decorative `<img>`
 * with `alt=""` would be equivalent and one more thing to keep empty.
 *
 * `align-items: flex-start` with a 20px icon against a 20px line box means the
 * two agree at every width; `center` would drift the moment the sentence wraps
 * to two lines, which it does at 320.
 */
.tc-pdp .tc-pdp__summary .tc-pdp__shipping {
	align-items: flex-start;
	color: var(--wp--custom--color--base--black, #0a205b);
	display: flex;
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.875rem;
	font-weight: 500;
	gap: 0.5rem;
	line-height: 1.4286;
	margin-block: 1rem 0;
}

.tc-pdp .tc-pdp__summary .tc-pdp__shipping::before {
	background-color: currentcolor;
	block-size: 1.25rem;
	content: "";
	flex: 0 0 auto;
	inline-size: 1.25rem;
	-webkit-mask: var(--tc-icon-shipping) no-repeat center / contain;
	mask: var(--tc-icon-shipping) no-repeat center / contain;
}

/*
 * ⚠ NOT ON PRODUCTS THAT CANNOT BE BOUGHT.
 *
 * 409 of 1,171 published products return `is_purchasable() === false`, and
 * `AddToCartForm::render()` returns an empty string for them — so `.tc-pdp__cart`
 * is absent from the DOM entirely, not merely empty. The paragraph is static
 * theme markup and would otherwise promise same-day shipping under a product
 * with no price and no way to order it.
 *
 * `:has()` on the ancestor rather than a PHP condition, for the same reason
 * product-grid.css:604 uses `:not(:has(.woocommerce-Price-amount))` for the
 * price-less cards: it reads the markup that actually shipped instead of
 * duplicating Woo's purchasability logic in a second place where the two can
 * drift.
 */
.tc-pdp .tc-pdp__summary:not(:has(.tc-pdp__cart)) .tc-pdp__shipping {
	display: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * FOCUS — ⚠ EVERY CONTROL IN THIS SECTION NEEDED ITS OWN RING, AND ONE OF THEM
 * ALREADY HAD ONE THAT FAILS.
 *
 * `theme.json` declares a `:focus` outline on the LINK element only. Nothing in
 * this theme gives a bare `<button>` a ring, and the two third parties here both
 * make it worse rather than leaving it alone. Measured by real Tab, after the
 * 120ms `box-shadow` transition in elements.css settles — an immediate read
 * returns the pre-transition value and reports a working ring as absent.
 *
 *   control        before                              contrast
 *   swatch chip    box-shadow 0 0 0 3px #DDDDDD        1.24:1 on white  FAIL
 *   − and +        box-shadow inset 0 0 1px 1px, and
 *                  `outline: none` on :focus           1px, and not focus-visible
 *   qty input      same as above                       same
 *   Add to Cart    box-shadow 0 0 0 3px #123B9E @45%   2.42:1 on white  FAIL
 *
 * The chip is the worst of the four and reads as a subtlety rather than a bug:
 * the plugin's focus ring REPLACES its 1px #A8A8A8 idle ring with a lighter
 * 3px #DDDDDD one, so the focused chip is less visible than an unfocused one.
 *
 * ⚠ AND A TOKEN CANNOT FIX IT. frontend.css:791-800 reads
 * `--wvs-hover-item-box-shadow` for `:hover` AND `:focus` from one declaration.
 * Retuning the token to something accessible gives hover a focus ring. The
 * plugin also uses `:focus`, not `:focus-visible`, so a mouse click leaves the
 * ring on. Both are why this is a selector and not another entry in the token
 * block at the top of this section.
 *
 * ── Q3 IS ANSWERED HERE, AND THE ANSWER IS A LOCAL RING ─────────────────────
 *
 * The plan left Add to Cart open: measure the inherited ring once the button
 * ships at the board's sky, declare a local one if it fails. It fails. (The
 * button was at `secondary-700` when this was measured and is at
 * `secondary-600` #057DB9 since 2026-08-06; the ring is a primary-600
 * color-mix and is unaffected either way.)
 * `color-mix(#123B9E 45%, transparent)` over white composites to rgb(148,167,211),
 * which is 2.42:1 against the page — under the 3:1 that SC 1.4.11 requires for a
 * focus indicator. `box-shadow: none` here so there is one ring and not two.
 *
 * ⚠ THAT RING IS GLOBAL AND SO IS THE FAILURE. `elements.css:32-35` gives it to
 * every `.wp-element-button` on the site — Log in, Register, "Return to shop",
 * every CTA. Fixing it globally is not this build's to do (the ruling scoped Q3
 * to a local ring) and the file is generated output inside a plugin in any case.
 * Reported, not silently patched.
 *
 * ── THE BUDGET ──────────────────────────────────────────────────────────────
 *
 * The chip rule is (0,7,0). Pseudo-classes count as classes, so the plugin's
 * `…:not(.radio-variable-item):focus` is already (0,5,0), its `.selected` is
 * (0,5,0) and its `.selected:hover` is (0,6,0) — and all of them print at 85
 * against this file's 69, so a tie loses. (0,7,0) is what it takes for the ring
 * to survive on a chip that is BOTH selected and focused, which is the state a
 * keyboard user is in for most of the interaction.
 *
 * All four use `outline`, not `box-shadow`: it is the one property none of the
 * third parties here set with any weight, it renders outside the border box
 * without moving anything, and it matches the rings T-C declared on the gallery
 * controls three hundred lines above.
 * ═══════════════════════════════════════════════════════════════════════════ */

.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	.variable-items-wrapper
	.variable-item.button-variable-item:focus-visible {
	outline: var(--focus-width, 3px) var(--focus-style, solid) var(--color-focus, #123b9e);
	outline-offset: var(--focus-offset, 2px);
}

.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	form.cart
	.wc-block-components-quantity-selector__button:focus-visible,
.tc-pdp
	.tc-pdp__summary
	.tc-pdp__cart
	form.cart
	.wc-block-components-quantity-selector__input:focus-visible {
	box-shadow: none;
	outline: var(--focus-width, 3px) var(--focus-style, solid) var(--color-focus, #123b9e);
	outline-offset: var(--focus-offset, 2px);
}

.tc-pdp .tc-pdp__summary .tc-pdp__cart .single_add_to_cart_button:focus-visible {
	box-shadow: none;
	outline: var(--focus-width, 3px) var(--focus-style, solid) var(--color-focus, #123b9e);
	outline-offset: var(--focus-offset, 2px);
}

/*
 * The "Clear" link keeps the theme's global link focus treatment, which
 * theme.json does declare and which works — it is the one control in this
 * section that needed nothing.
 */

/*
 * TABLET AND UP. Two spacing values move and nothing else does — the controls
 * are drawn identically at 768 and 1920, and the chip row wraps on its own at
 * 320 without a query (3 + 2, which is what the mobile board draws).
 */
@media (min-width: 48rem) {
	.tc-pdp .tc-pdp__summary .tc-pdp__cart {
		margin-block-start: 1.5rem;
		padding-block-start: 1.5rem;
	}

	.tc-pdp .tc-pdp__summary .tc-pdp__cart form.cart,
	.tc-pdp .tc-pdp__summary .tc-pdp__cart .variations_button,
	.tc-pdp .tc-pdp__summary .tc-pdp__cart form.cart > div[data-product_id] {
		row-gap: 2rem;
	}

	.tc-pdp .tc-pdp__summary .tc-pdp__shipping {
		margin-block-start: 1.5rem;
	}
}

/*
 * ── STILL NOT DONE HERE, ON PURPOSE ─────────────────────────────────────────
 *
 * The tabs, the related row and the reviews widget are untouched, and two of the
 * sections the board draws that WooCommerce has no output for still do not
 * exist — the rating row's "Write a review", and "Ask an Expert" with
 * "Recommended For This Application". Those are T-E through T-H, each with its
 * own plan gate.
 */

/* ═══════════════════════════════════════════════════════════════════════════
 * T-G — RELATED PRODUCTS.
 *
 * Figma: 43324:17454 (desktop), 43324:17192 (tablet), 43324:16887 (mobile),
 * canonical 43324:* set.
 *
 * ⚠ THE CARD IS NOT HERE. It comes from product-grid.css, which shares its
 * nineteen card rules through `.tc-product-cards` — the class this section's
 * Related collection carries — and is enqueued on every page whether or not
 * that pattern renders. This section only owns what is genuinely different:
 * the track, and the horizontal card the mobile board draws.
 *
 *   desktop 1400   4 x 332, 24px gaps          (332*4 + 24*3 = 1400)
 *   tablet   688   2 x 332, 24px gap           (332*2 + 24   =  688)
 *   mobile   288   1 up, and a DIFFERENT CARD
 *
 * §2c of the plan is right that the mobile card "is a different card, not a
 * reflow" — 118px thumbnail on the left, text on the right — and it is the one
 * place Related Products cannot reuse product-grid's card unchanged.
 * ═══════════════════════════════════════════════════════════════════════════ */

/*
 * ⚠ THE DIVIDER ABOVE RELATED PRODUCTS (2026-09-01), AND THE PADDING IS PART
 * OF IT RATHER THAN AN EXTRA.
 *
 * `docs/design/product-detail/desktop.png` draws a rule between the product
 * body and this section; the build shipped the gap without the rule. Same
 * 1px #DEE2E6 the accordion rows and the buy-box divider already use — three
 * places, one line weight, one token.
 *
 * The existing `margin-block-start` clamp is the section RHYTHM and is left
 * exactly as it was; the rule needs space UNDER it too, or the heading sits on
 * the line. So the clamp stays as margin (outside the border) and the same
 * clamp is added as padding (inside it), which puts the rule at the midpoint of
 * the gap the board draws it in rather than at the top of it.
 */
.tc-pdp .tc-pdp__related {
	border-block-start: 1px solid var(--wp--custom--color--neutral--200, #dee2e6);
	margin-block-start: clamp(2.5rem, 1.071rem + 7.143vw, 4rem);
	padding-block-start: clamp(2.5rem, 1.071rem + 7.143vw, 4rem);
}

/*
 * ⚠ THE SIZE IS NOT HERE, AND MUST NOT COME BACK HERE. The block binds
 * `heading-medium` in product-detail.php, the same way brand-grid, product-grid
 * and category-grid bind theirs, and this rule deliberately declares no
 * `font-size` and no `line-height` so it cannot out-specify the role.
 *
 * It used to. The comment here said "Title/Small at every width, matching the
 * accordion above it — the board draws the same role for both section
 * headings", and pinned 1.25rem/1.4 at (0,3,0). The board draws no such thing.
 * Measured off `docs/design/product-detail/desktop.png` — a 0.501 export of a
 * 1920 board, scale confirmed against the accordion title in the same image
 * (frame 97.8px against the build's 99.6px for "Description" at 20px):
 *
 *   "Related Products"   frame 297.4px   build @41px 299.9px   (0.8% apart)
 *   "Related Products"   frame 297.4px   build @20px 146.6px   (half)
 *
 * 41px at desktop is `heading-medium`, which is what every other section
 * heading on the site binds — and binding the role rather than pinning 41 here
 * is what gives the mobile ramp (23 @320, 41 @768+) that no board exists to
 * supply, and what keeps the block's own type controls honest in the editor.
 * Identical defect and identical fix to the category grid's h2 in
 * `docs/reports/gary-review-fixes.md` item 3b.
 *
 * What is left is what the PRESET DOES NOT SUPPLY. `has-heading-medium-font-size`
 * carries the size and nothing else — every consumer of the role restates the
 * rest locally, and `.tc-brand-grid__heading` is the reference implementation
 * (F23): weight 700, -0.02em, and 48/41 = 1.171. Without the tracking this
 * heading measures 313px against the board's 297.4; with it, 300.
 *
 * `color: var(--color-text)` went with the size and is not a loss —
 * `--color-text` is defined nowhere in the design system, so the declaration
 * resolved to the guaranteed-invalid value and `color` inherited regardless.
 * Measured identical before and after: rgb(10, 32, 91).
 *
 * ⚠ 768 IS THE ONE WIDTH NO BOARD SETTLES. The preset's own clamp gives 30.5
 * there; `.tc-brand-grid__heading` restates the clamp with `!important` to
 * reach 41 by `breakpoint/md`, on the strength of ITS frames. The PDP's tablet
 * export is 210px wide and cannot answer. This takes the plain preset, which is
 * what the category grid's h2 took for the identical defect one batch ago.
 */

/*
 * THE GAP TO THE GRID IS THE SECTION'S OWN ITEM SPACING: 24 on the phone
 * board, 48 on tablet and desktop (2026-10-06, after #317). Each Related frame
 * is a vertical auto layout whose `itemSpacing` is exactly the distance from
 * the heading's text box to the first card's top: 43324:16887 24, 43324:17192
 * and 43324:17454 48. The heading text has `leadingTrim: NONE` and its box is
 * its line box (28 / 48), so recipe B takes nothing off. It was a flat 24, the
 * phone value, at every width. md is the boundary: the 320 board says 24 and
 * the 768 board says 48.
 *
 * ⚠ NO SPACING PRESET IS 48 (`xxlarge` is 64; build-knowledge records this).
 * Written as twice `large` so it stays on the scale, the way I14 wrote its 56
 * as 40 + 16.
 */
.tc-pdp .tc-pdp__related-title.tc-pdp__related-title {
	font-weight: 700;
	letter-spacing: -0.02em;
	line-height: 1.171; /* 48/41 */
	margin-block: 0 var(--wp--preset--spacing--large, 1.5rem); /* 24 */
}

@media (min-width: 48rem) { /* breakpoint/md */
	.tc-pdp .tc-pdp__related-title.tc-pdp__related-title {
		margin-block-end: calc(var(--wp--preset--spacing--large, 1.5rem) * 2); /* 48 */
	}
}

/* ═══════════════════════════════════════════════════════════════════════════
 * T-E — "PRODUCT DETAILS" PROSE AND THE DETAILS ACCORDION.
 *
 * Figma: 43324:17555 / 17541 (desktop), 43324:17292 / 17279 (tablet),
 * 43324:17051 / 16874 (mobile), canonical 43324:* set.
 *
 * ⚠ THE THREE ACCORDION BOARDS ARE PIXEL-IDENTICAL. 76px rows, 24px padding,
 * 16px gap, 1px #DEE2E6 rules, Title/Small 20/28 and a 24px chevron at 320, 768
 * and 1920 alike. There is no breakpoint in this section and that is not an
 * omission — it is the measurement, and it is also the whole reason this widget
 * is not a consumer of assets/js/tabs-accordion.js.
 *
 * The frame is named "Anchors". Nothing about the drawing is an anchor link;
 * product-detail-plan.md §5f already recorded the name as a leftover.
 * ═══════════════════════════════════════════════════════════════════════════ */

.tc-pdp .tc-pdp__prose {
	margin-block-start: clamp(2rem, 1.286rem + 3.571vw, 3rem);
}

/*
 * NO SHORT DESCRIPTION, NO "PRODUCT DETAILS" (2026-10-23).
 *
 * `firsttracks-theme/product-short-description` renders nothing when the
 * product has no short description — production's behaviour — so the heading
 * would otherwise stand over an empty section. Hidden here rather than moved
 * into the block, because the heading and the body already share this group
 * and the heading stays an ordinary `core/heading` in the pattern. 24 of 1,171
 * products. Removing the whole group also removes its margin, so the gallery
 * is followed directly by the accordion with the accordion's own gap.
 */
.tc-pdp .tc-pdp__prose:not(:has(.tc-pdp__prose-body)) {
	display: none;
}

/*
 * THE INDENT — the prose and the accordion align with the MAIN IMAGE's left
 * edge, in both of the image's states (G31, 2026-09-15).
 *
 * The desktop board puts Product Details and the accordion at x=379, the main
 * image's edge, past a 95 rail and a 24 gap. DECISIONS 2026-08-05 declined to
 * build that indent because the rail is only there on ~7% of products — the
 * single-image fence in the gallery block lets the image take the whole 807
 * column on the other 93% — and an unconditional 119 would have been 119 out
 * on those. The designer approved the CONDITIONAL version on 2026-09-15:
 * indent only when the rail is actually present. The constant a reader then
 * sees is alignment with the image — 379 beside 379 on a gallery product, 260
 * beside 260 on a single-image one — rather than the indent itself.
 *
 * Gated on the RAIL'S OWN TEST. The fence removes the rail with
 * `:not(:has(slide + slide))`; this is the same `:has()` without the `:not`,
 * so the indent and the rail can only ever switch together. And it is the
 * rail's own number — 7.4375rem is the `margin-inline-start` on the main
 * slider (95 + 24 = 119) — so the two cannot drift apart. ≥64rem only, because
 * the rail does not exist below that: the thumbs are hidden and the image sits
 * at the column edge on every product.
 *
 * `padding-inline-start` rather than a margin so the two boxes stay 807 wide
 * and their CONTENT moves: the accordion rows become 688, the image's width,
 * with their chevrons at the image's right edge — which is what `Anchors`
 * (43324:17541) draws. The indent and the row width are one change, not two.
 * gallery-verify test 14 asserts both states.
 *
 * ⚠ SUPERSEDES the 2026-08-05 ruling by decision, not by oversight — DECISIONS
 * 2026-09-15 (fifth entry). Do not "restore" the column edge from the older
 * entry or from the T-C comment inside the gallery fence.
 */
@media (min-width: 64rem) {
	.tc-pdp
		.tc-pdp__primary:has(
			.tc-pdp__gallery-mount .ftm-product-gallery-slide + .ftm-product-gallery-slide
		)
		:is(.tc-pdp__prose, .tc-pdp__details) {
		padding-inline-start: 7.4375rem; /* 95 rail + 24 gap = 119 */
	}
}

/*
 * Title/Small — Rethink Sans Bold 20/28, letter-spacing -2% (-0.4px at 20px).
 * Flat at all three breakpoints, so no clamp: the board does not ramp it.
 */
.tc-pdp .tc-pdp__prose-title.tc-pdp__prose-title {
	color: var(--color-text);
	font-family: var(--wp--preset--font-family--display);
	font-size: 1.25rem;
	font-weight: 700;
	letter-spacing: -0.02em;
	line-height: 1.4;
	margin-block: 0 1rem;
}

/*
 * THE TRACK. Mobile-first: one column, then two, then four.
 *
 * The doubled class is not decoration — WooCommerce sizes its own `li` at
 * (0,3,0) inside a `min-width: 600px` query, which product-grid.css:446 already
 * records paying for. `grid` rather than Woo's flex because the board's rows are
 * even and a grid does not need the width arithmetic a flex basis does.
 */
.tc-pdp .tc-pdp__related .wc-block-product-template.wc-block-product-template {
	display: grid;
	gap: 1.5rem;
	grid-template-columns: minmax(0, 1fr);
	list-style: none;
	margin: 0;
	padding: 0;
}

.tc-pdp .tc-pdp__related .wc-block-product-template.wc-block-product-template > li {
	margin-block: 0;
	max-inline-size: none;
	width: auto;
}

/*
 * ⚠ THE MOBILE CARD IS HORIZONTAL, AND THAT IS THE ONE PLACE THIS SECTION
 * DIVERGES FROM THE SHARED CARD.
 *
 * Board 43324:16887: a 118px square thumbnail at x=0 and the text column at
 * x=130 — so 118 + 12 between them. product-grid's card is a vertical stack at
 * every one of its own breakpoints, so this is a genuine variant rather than a
 * reflow of it.
 *
 * `grid` with a named row for the media and everything else in column 2 keeps
 * the shared card's own rules (which set `margin-block` and the CTA pin on the
 * card's children) working untouched — a flex-direction flip would not, because
 * the CTA's `margin-block-start: auto` would then push it sideways.
 */

/*
 * ⚠ THE MEDIA IS TAKEN OUT OF FLOW, AND THREE GRID ATTEMPTS FAILED FIRST.
 *
 * With a flat DOM — five sibling blocks, no wrapper around the text — a
 * two-column grid has to span the media across every row. A spanning item sizes
 * the rows it spans, so ROW 1 became as tall as the 118px thumbnail and the
 * category eyebrow sat alone in it, opening a 118px hole above the title.
 *
 * `align-content: start`, `grid-auto-rows: min-content` and `align-items: start`
 * were each tried and each changed nothing, because all three govern how an item
 * sits INSIDE its row — and the row itself was the wrong height. The screenshot
 * was byte-identical after two of them, which is the only reason the wrong
 * diagnosis was caught.
 *
 * Taking the media out of flow removes it from row sizing entirely: the text
 * flows as an ordinary flex column in the padding well beside it. `min-block-size`
 * guarantees a short card still clears the thumbnail.
 *
 * Board 43324:16887: 118px square at x=0, text column at x=130 — so 118 + 12.
 */
.tc-pdp .tc-pdp__related .wc-block-product {
	min-block-size: 7.375rem;
	padding-block-end: 0; /* the track's 24 gap is the board's 24 between cards */
	padding-inline-start: 8.125rem;
	position: relative;
}

/*
 * ⚠ THE CARD TAKES THE SHOP ARCHIVE'S FLUSH TEXT (Gary, "Related Products Grid
 * Has Extra Side Padding", 2026-10-06). The shared kit insets every non-media
 * child by 8; every Related board draws category, title, price and button at
 * the text column's x 0 (43324:16887 at 130, 43324:17454 and :17192 at 0), as
 * the archive does (shop-archive.css, "The horizontal card"). (0,4,0) against
 * the kit's (0,3,0), at every width.
 */
.tc-pdp .tc-pdp__related .wc-block-product > :not(.tc-product-card__media) {
	padding-inline: 0;
}

/* The text column starts level with the thumbnail's top (43324:16887: Frame
   135 and the image share a y on every card). The kit's `> * + *` counts the
   out-of-flow media as a preceding sibling and put the category 8 lower. md
   restates this as 12.5, where the image sits above instead. */
.tc-pdp .tc-pdp__related .wc-block-product > .tc-product-card__media + * {
	margin-block-start: 0;
}

.tc-pdp .tc-pdp__related .tc-product-card__media {
	inline-size: 7.375rem;
	inset-block-start: 0;
	inset-inline-start: 0;
	position: absolute;
}

@media (min-width: 48rem) {
	/*
	 * TABLET AND UP — back to the shared vertical card, and two across.
	 * Everything the mobile variant set is unset rather than overridden, so the
	 * card falls back to product-grid's rules rather than to a second copy.
	 */
	.tc-pdp .tc-pdp__related .wc-block-product-template.wc-block-product-template {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	/*
	 * Back to the shared vertical card: the media returns to flow and the padding
	 * well closes.
	 *
	 * ⚠ NO CARD PADDING AT ALL, ON ANY SIDE (Gary, "Related Products Grid Has
	 * Extra Side Padding", 2026-10-06). From T-SA-7 until then the card took the
	 * shared md inset, `8px 8px 16px`, which put the image, category, title and
	 * button 8 inside the content edge the heading and the reviews above use
	 * (measured 267 against 259 at 1918). Boards 43324:17454 and :17192 draw
	 * every card as a 332 column with the image at x 0 and nothing around it,
	 * and the Shop Archive card is `padding: 0` from md for the same reason
	 * (shop-archive.css, I17). This reverses T-SA-7 here and answers D-D-6.
	 *
	 * (0,3,0), so it beats the shared md rule at (0,2,0) on specificity, and it
	 * also closes the mobile well, which is declared unconditionally above at
	 * (0,3,0) and would otherwise leak 130 into md.
	 */
	.tc-pdp .tc-pdp__related .wc-block-product {
		min-block-size: 0;
		padding: 0;
		position: static;
	}

	/* 16 between the image and the category, Frame 135 at y 348 under the 332
	   square on 43324:17454 and :17192, less the 3.5 trimmed off the top of
	   the category's 12/16 line: recipe B, the archive's own value at md
	   (shop-archive.css; DECISIONS 2026-09-17 §4). */
	.tc-pdp .tc-pdp__related .wc-block-product > .tc-product-card__media + * {
		margin-block-start: calc(var(--wp--preset--spacing--normal, 1rem) - 3.5px); /* 12.5 */
	}

	/*
	 * ⚠ `relative`, NOT `static` — AND THE DIFFERENCE BROKE THE BUY BOX.
	 *
	 * WooCommerce's product-image block renders an absolutely-positioned
	 * `.wc-block-components-product-image__inner-container` and relies on this
	 * element being its containing block. `static` here clobbered Woo's own
	 * `position: relative`, the inner container escaped to the initial containing
	 * block, and it resolved to **1440 x 3888 at (0, 230)** — a transparent sheet
	 * over the entire page.
	 *
	 * Nothing looked wrong. What failed was every CLICK: three buy-box assertions
	 * started timing out with "subtree intercepts pointer events", nine hundred
	 * pixels above this section. The mobile variant was unaffected because an
	 * absolutely-positioned element is itself a containing block.
	 *
	 * Resetting a position to `static` is never neutral when a third party's
	 * child is absolutely positioned against it.
	 */
	.tc-pdp .tc-pdp__related .tc-product-card__media {
		inline-size: auto;
		position: relative;
	}
}

/*
 * ⚠ THE PHOTO FRAME IS RELATED'S OWN, AND IT IS NOT THE ARCHIVE'S.
 *
 * The border itself shipped 2026-08-31 (PR #199, pre-client-review items 4d+7)
 * as ONE rule on two consumers in `product-grid.css`:
 *
 *   .tc-shop-archive .tc-product-card__media img,
 *   .tc-pdp__related .tc-product-card__media img { border: 1px solid neutral-200 }
 *
 * That was right about there being a border and wrong about its VALUES here,
 * and the reason it could be wrong is recorded in the report that shipped it:
 * "⚠ There is no exported frame for the archive card … This ships on the
 * review's instruction alone." Neutral-200 was picked to match the contact
 * form's fields and the archive's own dividers — a palette argument, not a
 * measurement — and the radius was simply left at the shared kit's 2px.
 *
 * There IS a frame for THIS rail, and it disagrees on both. Read with
 * get_design_context, not off a PNG:
 *
 *   43324:17464 / :17488 / :17506 / :17524  — all four desktop related cards
 *     `border border-[var(--color/neutral/100,#e9ecef)] border-solid rounded-[4px]`
 *
 * and the mobile board's 118px thumbnail (43324:16897) is `rounded-[4px]` too,
 * so the radius is carried by two boards rather than one. The kit's own frame
 * for the homepage grid (43369:8000) draws `rounded-[2px]` and NO stroke at
 * all, which is why the 2px is correct where it lives and wrong once inherited
 * here — the two sections' frames genuinely differ.
 *
 * ⚠ THE ARCHIVE IS DELIBERATELY NOT CHANGED WITH IT. It still has no frame, so
 * moving it would be trading one unmeasured value for another. The shared rule
 * in `product-grid.css` keeps `box-sizing: border-box` and the 1px for both
 * consumers; only the two values a frame can answer are restated here.
 *
 * ⚠ AND THE MOBILE BOARD CANNOT CONFIRM THE COLOUR. 43324:16897 is a flat
 * `neutral-200` placeholder swatch with no stroke, the same placeholder
 * convention the tabbed-timeline boards use. It answers the radius and says
 * nothing about the border, so it is not read as "mobile draws none".
 *
 * (0,3,1). The kit's `border-radius: 2px` and its border colour are both
 * (0,2,1), so this wins on specificity and not on source order — §5a.
 */
.tc-pdp .tc-pdp__related .tc-product-card__media img {
	border-color: var(--wp--custom--color--neutral--100, #e9ecef);
	border-radius: 4px;
}

@media (min-width: 64rem) {
	.tc-pdp .tc-pdp__related .wc-block-product-template.wc-block-product-template {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

/*
 * Body/Small — Inter 400 16/24, flat at all three boards (G30, 2026-09-15).
 *
 * Until P12 nothing here set a size, so the prose inherited the body ROLE —
 * `body-large`, fluid 16 → 18 — and rendered 18/27 at 1920, 17.3/25.95 at
 * 1024, 16.83/25.24 at 768, reaching the frame's 16/24 only at 320. The frame's
 * role is `body-small`, which is `fluid: false` and emits a flat 1rem: the
 * same opt-in contact-form, grid-with-icons, locations, services-tabs and
 * tabbed-timeline already make for their flat-16 blocks. The token's own
 * line-height is 1.6; the frame draws 24/16 = 1.5, so it is stated here, as
 * those patterns do. The body role itself is untouched — every other page is
 * built on the ramp, and that is a site-wide token, not this pattern's.
 */
.tc-pdp .tc-pdp__prose-body.tc-pdp__prose-body {
	color: var(--color-text);
	font-size: var(--wp--preset--font-size--body-small);
	line-height: 1.5; /* 24/16 */
	margin-block: 0;
}

/*
 * ── THE ACCORDION ──────────────────────────────────────────────────────────
 *
 * Rules between rows, not around the set: the board gives row 1 a top AND a
 * bottom rule, rows 2-3 a bottom rule, and row 4 none. That is one rule above
 * every row and nothing below the last, which is what `border-block-start` on
 * each row states in a single declaration — and it stays correct at 1, 2, 3 or 4
 * rows, which a `:last-child` exception would not.
 */
.tc-pdp .tc-pdp-accordion {
	margin-block-start: clamp(2rem, 1.286rem + 3.571vw, 3rem);
}

.tc-pdp .tc-pdp-accordion__row {
	border-block-start: 1px solid var(--wp--custom--color--neutral--200, #dee2e6);
}

/*
 * The heading is the row's own control surface. `margin: 0` because the board
 * measures the 76px row from the rule, not from a collapsed margin — and core's
 * `:root :where(.is-layout-flow) > *` margin would otherwise land here.
 */
.tc-pdp .tc-pdp-accordion__title.tc-pdp-accordion__title {
	font-size: 1.25rem;
	margin-block: 0;
}

/*
 * ⚠ NO-JS: THERE IS NO BUTTON HERE UNTIL THE SCRIPT MAKES ONE, so this rule
 * styles nothing on a no-JS page and the bare `<h3>` inherits the block above.
 * That is the intended degradation — four headings and four blocks of prose,
 * content-complete, no dead controls.
 *
 * 76px = 24 + 28 + 24. `align-items: flex-start` rather than centre because the
 * board aligns the chevron to the first line of the title, which matters on the
 * two-line titles the 288px column produces.
 */
.tc-pdp .tc-pdp-accordion__control {
	align-items: flex-start;
	background: none;
	border: 0;
	color: var(--color-text);
	cursor: pointer;
	display: flex;
	font-family: var(--wp--preset--font-family--display);
	font-size: 1.25rem;
	font-weight: 700;
	gap: 1rem;
	inline-size: 100%;
	letter-spacing: -0.02em;
	line-height: 1.4;

	/*
	 * 76px = 24 + 28 + 24, and Figma draws the rule as an INSIDE stroke — so the
	 * board's 76 already contains the 1px. In CSS the border sits outside the
	 * padding box, so a flat 1.5rem measures 77 and the four rows accumulate 4px
	 * of drift against the frame. The subtraction is on the top only, matching
	 * which edge the rule is on.
	 */
	padding-block: calc(1.5rem - 1px) 1.5rem;
	padding-inline: 0;
	text-align: start;
	text-transform: capitalize;
}

/*
 * `min-inline-size: 0` on the growing item, not `flex: 1` alone: a long
 * unbroken title — and `ayc_tab` titles come from a Pods field nobody here
 * controls — would otherwise refuse to shrink and push the chevron out of the
 * 288px column.
 */
.tc-pdp .tc-pdp-accordion__label {
	flex: 1 1 auto;
	min-inline-size: 0;
}

.tc-pdp .tc-pdp-accordion__chevron {
	background-color: currentcolor;
	block-size: 1.5rem;
	flex: 0 0 auto;
	inline-size: 1.5rem;
	-webkit-mask: var(--tc-icon-chevron-down) no-repeat center / contain;
	mask: var(--tc-icon-chevron-down) no-repeat center / contain;
	transform: rotate(0deg);
	transition: transform var(--wp--custom--duration--fast, 120ms)
		var(--wp--custom--easing--standard, ease);
}

.tc-pdp .tc-pdp-accordion__control[aria-expanded="true"] .tc-pdp-accordion__chevron {
	transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
	.tc-pdp .tc-pdp-accordion__chevron {
		transition: none;
	}
}

/*
 * ⚠ `hidden` ALONE DOES NOT ALWAYS COLLAPSE A PANEL, and this theme has already
 * paid for that once: services-tabs.css:300-313 records that WP emits
 * `.is-layout-flex { display: flex }` at a specificity that beats the `hidden`
 * attribute's UA `display: none`, so a panel carrying a WP layout class stays
 * visible while announcing itself as hidden.
 *
 * The panel element here is the theme's own plain div, so it is not currently at
 * risk — but its CONTENTS are third-party (`pwb_tab` is Perfect WooCommerce
 * Brands, `ayc_tab` is a Pods loop) and a future WP or plugin change could add a
 * layout class to it. One declaration removes the whole class of failure.
 */
.tc-pdp .tc-pdp-accordion__panel[hidden] {
	display: none;
}

/*
 * The panels follow the prose down to Body/Small 16/24 (G30).
 *
 * ⚠ A JUDGMENT, NOT A FRAME VALUE. All three boards draw the accordion CLOSED,
 * so no frame specifies panel body text. Left alone the panels stayed on the
 * 16 → 18 ramp, and the short description above and the long description
 * inside the first row would have read in two sizes a few hundred pixels
 * apart. The designer confirmed on 2026-09-15 that the panels follow the
 * prose. The Description list, the Additional information table and the Brand
 * and Document links all inherit from here; the Brand panel's <h3> is a
 * heading and is not touched.
 */
.tc-pdp .tc-pdp-accordion__panel {
	font-size: var(--wp--preset--font-size--body-small);
	line-height: 1.5; /* 24/16 */
	padding-block-end: 1.5rem;
}

/*
 * ⚠ The empty heading Perfect WooCommerce Brands leaves behind.
 *
 * includes/woocommerce.php filters the three per-panel headings to an empty
 * string, because the accordion row title is already the heading and the panels
 * were opening onto a duplicate of it. Woo's own two templates guard with
 * `if ( $heading )` and drop the element; pwb prints its `<h2>` unconditionally
 * (lib/class-product.php:45), so it is left empty rather than removed.
 *
 * `display: none` and not `visibility` or a zero height: an empty heading that
 * is still in the accessibility tree is worse than no heading, because it puts a
 * blank entry in a screen reader's heading list.
 */
.tc-pdp .tc-pdp-accordion__panel > h2:empty {
	display: none;
}

/*
 * ⚠ The brand logo Perfect WooCommerce Brands leaves touching the prose above.
 *
 * `lib/class-product.php:56-68` prints the Brand panel as
 * `<h3>{name}</h3>`, then `<div>{description}</div>` ONLY IF the term has a
 * description, then `<span><a><img/></a></span>` for the logo. The span is an
 * inline element with no margins, and the description div has
 * `margin-block-end: 0` — so with a description present the logo's top edge and
 * the last line of prose are the same edge. Measured on /product/afn-hevc/
 * (Adtec, 224-character term description): **0.0px at 1440, 768 and 320**, the
 * Adtec logotype resting on the descenders of the sentence above it.
 *
 * It reads correctly on a brand with NO description only by accident: the h3's
 * own 41px bottom margin is doing the work, and 60 of this catalogue's brands
 * have a description.
 *
 * `display: block` first, because a vertical margin on an inline box does
 * nothing at all — the rule would be silently inert without it. Then
 * `--ftps-space-stack`, which is this column's own stack rhythm and not a new
 * number: the same 16px separates `.tc-pdp__prose`'s heading from its body
 * directly above this accordion.
 *
 * ⚠ THE NO-DESCRIPTION CASE IS UNCHANGED, and that is margin collapsing rather
 * than luck. The h3's 41px bottom margin and this 16px top margin are adjacent
 * siblings in the same block formatting context, so they collapse to max(41,16)
 * = 41. Measured before and after: 41 / 30.5 / 23 at the three widths, both
 * times.
 */

/* stylelint-disable-next-line selector-id-pattern -- the id is Perfect WooCommerce Brands', printed verbatim at lib/class-product.php:56; the underscore is not ours to rename */
.tc-pdp .tc-pdp-accordion__row #tab-pwb_tab-content > span {
	display: block;
	margin-block-start: var(--ftps-space-stack, 1rem);
}

/*
 * ⚠ FOCUS. theme.json declares a :focus outline on LINK elements only, and it
 * cannot be fixed there — WordPress wraps element pseudo-selectors in `:where()`,
 * which makes the rule (0,1,0) against elements.css's (0,2,0). T-D fixed the
 * global ring in the token instead. This control is a bare `<button>` with no
 * `wp-element-button` class, so no global rule reaches it and it declares its
 * own, from the same four tokens every other focus ring on the site now uses.
 */
.tc-pdp .tc-pdp-accordion__control:focus-visible {
	outline: var(--focus-width, 3px) var(--focus-style, solid)
		var(--color-focus, #123b9e);
	outline-offset: calc(var(--focus-offset, 2px) * -1);
}

/* ═══════════════════════════════════════════════════════════════════════════
 * T-F — ASK AN EXPERT, AND RECOMMENDED FOR THIS APPLICATION
 *
 * The last two sections of the buy-box column. Figma: Ask an Expert 43324:17569
 * / 43324:17306 / 43324:17054; cross-sells 43324:17558 / 43324:17295 /
 * 43324:16863. Frames in docs/design/pdp-cross-sells/.
 *
 * ── ⚠ READ THE T-D WARNING AT `.tc-pdp__cart` BEFORE EDITING BELOW ─────────
 *
 * `.tc-pdp__summary` is a grid ITEM and must stay a block CONTAINER, and this
 * section is the reason that warning was written. Everything here obeys it:
 *
 *   - the rhythm between sections is MARGIN, never `gap` on the summary;
 *   - `.tc-pdp__crosssell` is a plain block box. It is NOT flex and NOT grid,
 *     so the Swiper mount inside it is never a flex/grid item whose
 *     `min-width: auto` could consult its own content. T-C measured exactly
 *     that failure at 33,554,433px — Chromium's infinity — with the gallery.
 *   - `.tc-pdp__crosssell-head` IS a flex row, and that is safe: the slider is
 *     its SIBLING, not its child.
 *
 * DO NOT "simplify" this to `display: grid; row-gap` on either the summary or
 * the collection. It looks tidier and it reintroduces the defect.
 *
 * ── ⚠ AND DO NOT PUT `overflow` ON AN ANCESTOR ─────────────────────────────
 *
 * The sticky summary dies silently if any ancestor gets a non-`visible`
 * overflow — see the sticky block near the top of this file, which names this
 * section as the work that invites it. The slider contains its own overflow on
 * its own track element (`.swiper` from the plugin's swiper.css, applied to a
 * wrapper the JS injects), which is a DESCENDANT of the summary and therefore
 * harmless.
 * ═══════════════════════════════════════════════════════════════════════════ */

/*
 * Section rhythm, as margins. 32px between the cart block, Ask an Expert and
 * the cross-sells at desktop and tablet (the boards draw 848 - 816 = 32), 24 at
 * mobile (1084 - 1060).
 */
.tc-pdp .tc-pdp__summary .tc-pdp__expert {
	margin-block-start: 1.5rem;
}

.tc-pdp .tc-pdp__summary .tc-pdp__crosssell {
	margin-block-start: 1.5rem;
}

@media (min-width: 48rem) {
	.tc-pdp .tc-pdp__summary .tc-pdp__expert,
	.tc-pdp .tc-pdp__summary .tc-pdp__crosssell {
		margin-block-start: 2rem;
	}
}

/* ── ASK AN EXPERT ────────────────────────────────────────────────────────── */

.tc-pdp .tc-pdp__expert {
	align-items: flex-start;
	border: 1px solid var(--wp--custom--color--neutral--200, #dee2e6);
	border-radius: 2px;
	display: flex;
	gap: 13px;
	padding: 1rem;
}

/*
 * ⚠ THE AVATAR IS THE PHOTOGRAPH OR IT IS NOTHING. THERE IS NO PLACEHOLDER DISC.
 *
 * The board draws a photograph of what is evidently a real member of staff
 * (43324:17570, 50x50 — harvested from 43324:17569, it is a raster fill, not an
 * icon). Who that is, and whether they have agreed to appear on 1,171 product
 * pages, is an open client question — Standing list 1, 2026-08-06.
 *
 * ⚠ CORRECTED 2026-09-07 (review-batch-7). T-F answered "until then" with a
 * `::before` painting a 50px neutral-100 disc, reasoning that a placeholder beat
 * a stock face. Both halves of that are still true and the conclusion was still
 * wrong: an empty grey circle at avatar size, beside a name-shaped heading,
 * reads as an image that FAILED TO LOAD rather than as a deliberate blank. It is
 * a defect on every product page, which is a heavier cost than the missing
 * element it was standing in for.
 *
 * So the rule is gone. `includes/product-detail-expert.php` emits an `<img>`
 * when — and only when — a photograph is set, and the card renders without an
 * avatar until one is. The geometry below is the board's 50px circle and is
 * what the real photograph will land in; do not reintroduce a `content: ""`
 * fallback beneath it.
 */
.tc-pdp .tc-pdp__expert-photo {
	block-size: 50px;
	border-radius: 50%;
	flex: 0 0 50px;
	inline-size: 50px;
	object-fit: cover;
}

.tc-pdp .tc-pdp__expert-body {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	min-inline-size: 0;
}

.tc-pdp .tc-pdp__expert-title {
	color: var(--wp--custom--color--base--black, #0a205b);
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.875rem;
	font-weight: 700;
	line-height: 1.4286; /* 20/14 */
	margin: 0;
}

/*
 * Phone │ email. A flex row at 48rem+ with a 1px rule between, and a stacked
 * pair below it with the rule turned horizontal — both drawn, not inferred
 * (43324:17573 desktop, 43324:17058 mobile).
 *
 * The rule is an empty `aria-hidden` span rather than a border or a `::after`
 * on the phone link, because it has to change AXIS at the breakpoint and a
 * border cannot. It carries no text, so it is invisible to assistive tech.
 */
.tc-pdp .tc-pdp__expert-contact {
	align-items: flex-start;
	color: var(--wp--custom--color--base--black, #0a205b);
	display: flex;
	flex-direction: column;
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.875rem;
	gap: 0.5rem;
	line-height: 1.4286; /* 20/14 */
	margin: 0;
}

.tc-pdp .tc-pdp__expert-contact a {
	color: inherit;
}

.tc-pdp .tc-pdp__expert-rule {
	background-color: var(--wp--custom--color--neutral--300, #ced4da);
	block-size: 1px;
	display: block;
	inline-size: 100%;
}

@media (min-width: 48rem) {
	.tc-pdp .tc-pdp__expert-contact {
		align-items: center;
		flex-direction: row;
		flex-wrap: wrap;
	}

	.tc-pdp .tc-pdp__expert-rule {
		align-self: stretch;
		block-size: auto;
		inline-size: 1px;
	}
}

/* ── RECOMMENDED FOR THIS APPLICATION ─────────────────────────────────────── */

/*
 * ⚠ BLOCK, NOT FLEX OR GRID. See the warning at the head of this section. The
 * rule and the padding are the board's (border-top #dee2e6, 24px above the
 * heading at desktop and tablet, 16 at mobile).
 *
 * The whole section — rule, heading and slider — is INSIDE the
 * product-collection block, so Woo removes all of it together on the 1,157 of
 * 1,171 products that have no cross-sells. That collapse is structural and is
 * not implemented here; there is deliberately no `:has()` rule. See the docblock
 * in product-detail.php.
 */
.tc-pdp .tc-pdp__crosssell {
	--tc-icon-chevron-left: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15.4 7.4 14 6l-6 6 6 6 1.4-1.4-4.6-4.6z'/%3E%3C/svg%3E");
	--tc-icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M11 4h2v7h7v2h-7v7h-2v-7H4v-2h7z'/%3E%3C/svg%3E");

	border-block-start: 1px solid var(--wp--custom--color--neutral--200, #dee2e6);
	display: block;
	padding-block-start: 1rem;
}

@media (min-width: 48rem) {
	.tc-pdp .tc-pdp__crosssell {
		padding-block-start: 1.5rem;
	}
}

/*
 * The heading row. Flex is safe HERE — the slider is this element's sibling.
 * `flex-wrap` plus the row gap is the board's own behaviour (43887:32598 is
 * `flex-wrap` with `gap-y: 16`), which matters at mobile where the heading takes
 * two lines and the arrows would otherwise be squeezed.
 */

/*
 * `margin-block-end` is the gap between the heading row and the card: 24 at
 * mobile, 16 at desktop and tablet (the boards draw 80 - 56 and 64 - 48). It is
 * stated on the HEAD, not on the track, because the track's identity changes —
 * the JS wraps it in `.tc-pdp__crosssell-viewport`, so a rule keyed to the track
 * would apply to a grandchild once the slider initialises and to a child before
 * it.
 */

/*
 * ⚠ `nowrap`, AND `wrap` IS THE BUG — THIS IS NOT A STYLE PREFERENCE.
 *
 * `flex-wrap: wrap` was here first, copied from the desktop node's own
 * auto-layout (43887:32598 wraps, with a 16px row gap). At 320 it put the
 * arrows on a SECOND row, under the heading and flush LEFT, which no board
 * draws. Measured that way twice.
 *
 * The reason is the order flexbox does its work in: a wrapping container
 * collects items into lines using each item's HYPOTHETICAL main size — for
 * `flex-basis: auto` that is max-content — and only shrinks items afterwards,
 * within a line. The heading's max-content is ~270px against 288 available, so
 * the line broke before `flex-shrink` ever got a chance to narrow it.
 * `nowrap` removes the line-breaking step, so the heading shrinks to the 208
 * the mobile board draws and wraps its TEXT to two lines instead.
 *
 * The heading's `flex: 1 1 auto` + `min-inline-size: 0` below is the other half
 * of this and neither half works alone.
 */
.tc-pdp .tc-pdp__crosssell-head {
	align-items: center;
	display: flex;
	flex-wrap: nowrap;
	gap: 1rem;
	justify-content: space-between;
	margin-block-end: 1.5rem;
}

/*
 * The head is a core/group, so it carries `is-layout-flow` and its
 * `> * { margin-block-start: 24px }` reaches the injected controls as the second
 * child. In a flex row that does not push anything down — it makes the ROW 48
 * tall instead of 24, because the control's outer height includes the margin.
 * Measured exactly that before this rule.
 */
.tc-pdp .tc-pdp__crosssell-head > * {
	margin-block: 0;
}

/*
 * ⚠ `h2.tc-pdp__crosssell-title`, NOT `.tc-pdp__crosssell-title` — THE ELEMENT
 * IN THE SELECTOR IS LOAD-BEARING AND IS NOT DECORATION.
 *
 * WooCommerce's product-collection stylesheet declares
 *
 *   .wp-block-woocommerce-product-collection h2.wp-block-heading
 *     { font-size: var(--wp--preset--font-size--small, 14px) }
 *
 * which is (0,2,1). A plain `.tc-pdp .tc-pdp__crosssell-title` is (0,2,0) and
 * LOSES, so the heading rendered at 13px — the `small` preset — against the
 * board's 16. Adding the element name makes this (0,3,1) and it wins outright.
 *
 * `small` resolving to 13 rather than to its own 14px fallback is the same trap
 * the breadcrumb block in product-detail.php recorded (it set `fontSize` as a
 * block attribute for that reason, until I33 removed it), on a
 * different block. Third time this preset has cost this pattern a measurement.
 */

/*
 * `flex: 1 1 auto` with `min-inline-size: 0` is what keeps the arrows on the
 * heading's row at 320. Without it the h2 claims its max-content width (~230px
 * against a 288 board, 208 of which is available), the controls wrap onto a
 * second line and land on the LEFT under the heading — measured exactly that.
 * The mobile board draws the heading wrapping to two lines INSIDE 208 with the
 * arrows still at the top right, which is what shrinking produces.
 */
.tc-pdp .tc-pdp__crosssell h2.tc-pdp__crosssell-title {
	color: var(--wp--custom--color--base--black, #0a205b);
	flex: 1 1 auto;
	font-family: var(--wp--preset--font-family--body);
	font-size: 1rem;
	font-weight: 700;
	letter-spacing: 0.032px;
	line-height: 1.5; /* 24/16 */
	margin: 0;
	min-inline-size: 0;
}

/*
 * The controls. Injected by product-detail.js, so they are absent without JS
 * and absent when there is only one card — the same "chrome, not content"
 * reasoning product-grid.js records, and the same reason they are not seeded as
 * blocks in the pattern.
 */
.tc-pdp .tc-pdp__crosssell-controls {
	display: flex;
	flex: 0 0 auto;
	gap: 1rem;
}

.tc-pdp .tc-pdp__crosssell-control {
	background: none;
	block-size: 1.5rem;
	border: 0;
	color: var(--wp--custom--color--base--black, #0a205b);
	cursor: pointer;
	display: block;
	inline-size: 1.5rem;
	padding: 0;
}

.tc-pdp .tc-pdp__crosssell-control::before {
	background-color: currentcolor;
	block-size: 1.5rem;
	content: "";
	display: block;
	inline-size: 1.5rem;

	/* stylelint-disable-next-line property-no-vendor-prefix -- mask needs the prefix pair */
	-webkit-mask: var(--tc-icon-chevron-left) no-repeat center / contain;
	mask: var(--tc-icon-chevron-left) no-repeat center / contain;
}

/* One glyph, both directions — the same trick the gallery nav uses above. */
.tc-pdp .tc-pdp__crosssell-control--next::before {
	transform: scaleX(-1);
}

/*
 * At the ends the control takes `aria-disabled`, NOT `disabled` — a disabled
 * button leaves the tab order and stops being announced, so a keyboard user who
 * reached the end would lose the control rather than be told it is spent. Same
 * ruling as product-grid.js, and the handler no-ops instead.
 */
.tc-pdp .tc-pdp__crosssell-control[aria-disabled="true"] {
	cursor: default;
	opacity: 0.35;
}

.tc-pdp .tc-pdp__crosssell-control:focus-visible {
	outline: var(--focus-width, 3px) var(--focus-style, solid)
		var(--color-focus, #123b9e);
	outline-offset: var(--focus-offset, 2px);
}

/*
 * ⚠ WORDPRESS'S FLOW LAYOUT PUTS 24px BETWEEN EVERY CHILD OF THIS BLOCK, AND
 * IT HAS TO BE CANCELLED IN ONE PLACE RATHER THAN CHASED.
 *
 * `wp-block-woocommerce-product-collection` carries `is-layout-flow`, whose
 * `> * { margin-block-start: 24px }` reached BOTH the heading row and — because
 * the JS injects it as a direct child — the Swiper viewport. Measured before
 * this rule: 24 above the head on top of the section's own 24 padding, then
 * another 24 above the viewport on top of the track's own 16, i.e. every gap in
 * the section was roughly double what the board draws.
 *
 * Cancelling it on the children and stating the rhythm ONCE, as the head's own
 * bottom margin, is what stops it coming back the next time an element is
 * added here — a new direct child would silently inherit 24px again.
 */
.tc-pdp .tc-pdp__crosssell > * {
	margin-block: 0;
}

@media (min-width: 48rem) {
	.tc-pdp .tc-pdp__crosssell-head {
		margin-block-end: 1rem;
	}
}

/*
 * The track. Woo renders `ul.wc-block-product-template`; the JS adds Swiper's
 * own `swiper-wrapper` to it and `swiper-slide` to each `li`, so the plugin's
 * swiper.css does the layout and this file does not restate it.
 *
 * WITHOUT JS this stays an ordinary list and every card is readable, stacked —
 * progressive enhancement in the strict sense, as product-grid.js puts it. The
 * list bullets and padding are the only things that need removing.
 */
.tc-pdp .tc-pdp__crosssell .wc-block-product-template {
	list-style: none;
	margin: 0;
	padding: 0;
}

/*
 * ⚠ NOT THE SHARED CARD. This is `tc-pdp__crosssell`'s own scope and owns every
 * rule below. The section must never be opted into the shared card scope
 * `.tc-product-cards` in product-grid.css — that card is vertical, has a 332px
 * image and a full-width Add to Cart, and this one is horizontal, 80px and
 * compact. T-SA-6 costed the widening and confirmed the ruling: roughly +1 LOC
 * rather than a saving, and `flex-direction: column` would break this row.
 *
 * `min-block-size` rather than `block-size`: the board draws 80 at every width,
 * but at mobile the product name takes two lines and the text column is 90 tall,
 * which the mobile frame itself shows overflowing its own 80px row (43324:16867
 * is 83 inside a row of 80). Growing is the honest answer; clipping is not.
 */

/*
 * ⚠ `margin: 0` IS NOT TIDYING — it cancels Woo's list layout.
 * product-template-style.css sets
 * `.is-product-collection-layout-list .wc-block-product:not(:last-child)
 * { margin-bottom: 1.2rem }`, which in a Swiper track is 19px of dead space
 * between slides that the transform then carries around. Same specificity
 * (0,3,0) as this rule, so it is source order that decides it and this
 * stylesheet prints after Woo's — stated because that is fragile enough to be
 * worth knowing about rather than rediscovering.
 */
.tc-pdp .tc-pdp__crosssell .wc-block-product {
	align-items: center;
	display: flex;
	margin: 0;
	min-block-size: 80px;
	padding: 0;
}

/*
 * And this cancels the other one Woo aims at the card's CHILDREN:
 * `:where(.wc-block-product-template .wc-block-product)>:not(:last-child)
 * { margin-bottom: .75rem }`. In a vertical card that is the rhythm; in a
 * horizontal row it is 12px hanging under the thumbnail and the text column,
 * pushing them off centre.
 */
.tc-pdp .tc-pdp__crosssell .wc-block-product > * {
	margin-block-end: 0;
}

/*
 * ⚠ THE 1px IS SWIPER'S ROUNDING, NOT SPACING (I19, 2026-10-05).
 *
 * Swiper sizes each slide from the viewport's `clientWidth`, which is an
 * INTEGER, while the viewport itself is fractional. When it rounds up the slide
 * is up to 0.5px wider than the `overflow: hidden` box and the "+" button's
 * right border is clipped: 528.61 viewport, 529 slide, 0.39px lost at 1918
 * (measured; 5 of 6 test widths clipped). 1px of inset always covers a ≤0.5px
 * overshoot. Scoped to the initialised viewport, so the no-JS list is
 * untouched, and after the `padding: 0` rule above at equal specificity.
 */
.tc-pdp .tc-pdp__crosssell-viewport .wc-block-product {
	padding-inline-end: 1px;
}

.tc-pdp .tc-pdp__crosssell-media {
	block-size: 80px;
	border: 1px solid var(--wp--custom--color--neutral--100, #e9ecef);
	border-radius: 2px;
	flex: 0 0 80px;
	inline-size: 80px;
	margin-block: 0;
	margin-inline-end: 1rem;
	overflow: hidden;
}

.tc-pdp .tc-pdp__crosssell-media img {
	block-size: 100%;
	display: block;
	inline-size: 100%;
	object-fit: cover;
}

@media (min-width: 48rem) {
	.tc-pdp .tc-pdp__crosssell-media {
		margin-inline-end: 1.5rem;
	}
}

/*
 * `min-inline-size: 0` is load-bearing, not decoration. This is a flex item
 * whose automatic minimum size would otherwise be its content's, and product
 * names here are long unbroken SKUs — the same reason the two grid tracks at the
 * top of this file are `minmax(0, …)`.
 */
.tc-pdp .tc-pdp__crosssell-body {
	display: flex;
	flex: 1 1 auto;
	flex-direction: column;
	gap: 0.375rem;
	min-inline-size: 0;
}

/*
 * The eyebrow. Woo puts every assigned category in this block, comma separated,
 * and the board draws one — so it is clamped to a single line rather than
 * filtered down in PHP, because which category is "the" category is a content
 * question and truncating is reversible where discarding is not.
 */
.tc-pdp .tc-pdp__crosssell-cat {
	color: var(--wp--custom--color--base--black, #0a205b);
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.75rem;
	font-weight: 700;
	letter-spacing: 0.024px;
	line-height: 1.3333; /* 16/12 */
	margin: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.tc-pdp .tc-pdp__crosssell-cat a {
	color: inherit;
	text-decoration: none;
}

/*
 * Two lines, then cut. The desktop board draws one line and the mobile board
 * two; real catalogue names run longer than either, and an unclamped name
 * changes the card's height from product to product — which in a slider means
 * every slide is a different height and the track jumps as it moves.
 *
 * ⚠ `max-block-size` IS THE MECHANISM. `-webkit-line-clamp` IS THE GARNISH.
 *
 * The clamp needs `display: -webkit-box`, and that declaration does not survive
 * to the browser on this site — measured: `-webkit-line-clamp` and
 * `-webkit-box-orient` both arrive and compute, and `display` computes
 * `flow-root`, with no competing `display` rule anywhere in the cascade. So the
 * declaration is being dropped somewhere between this file and the page, not
 * overridden. The two prefixed properties are kept because they cost nothing
 * and give the ellipsis wherever `display` does survive.
 *
 * `2.5rem` is exactly two 20px lines. It is expressed in rem rather than as
 * `calc(2 * 1.4286em)` so that it cannot drift if the line-height above is
 * edited without thinking about this rule.
 */
.tc-pdp .tc-pdp__crosssell h3.tc-pdp__crosssell-name {
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	color: var(--wp--custom--color--base--black, #0a205b);
	display: -webkit-box;
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.875rem;
	font-weight: 500;
	line-height: 1.4286; /* 20/14 */
	margin: 0;
	max-block-size: 2.5rem; /* 2 lines x 20px */
	overflow: hidden;
}

.tc-pdp .tc-pdp__crosssell-name a {
	color: inherit;
	text-decoration: none;
}

.tc-pdp .tc-pdp__crosssell-name a:hover {
	text-decoration: underline;
}

.tc-pdp .tc-pdp__crosssell-price {
	color: var(--wp--custom--color--base--black, #0a205b);
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.9375rem;
	font-weight: 700;
	letter-spacing: 0.03px;
	line-height: 1.3333; /* 20/15, the mobile board */
	margin-block: 0.125rem 0;
}

@media (min-width: 48rem) {
	.tc-pdp .tc-pdp__crosssell-price {
		line-height: 1.6; /* 24/15 */
	}
}

/*
 * ═══ ⚠ THE "+" BUTTON, AND WHY IT IS NOT APPLIED UNCONDITIONALLY ═══════════
 *
 * The board draws a bare "+" in a 40x40 outlined box, and that is the shape of
 * ONE of the three things `woocommerce/product-button` renders. Measured from
 * the served markup on three fixtures, NOT inferred:
 *
 *   "Add to cart"    .add_to_cart_button.product_type_simple      → "+"
 *   "Select options" .add_to_cart_button.product_type_variable    → text
 *   "Read more"      .product_type_simple  (no add_to_cart_button)→ text
 *
 * Across all 20 distinct cross-sell targets in this database: 16, 3 and 1
 * respectively. So a fifth are NOT the drawn shape, and the divergence is
 * already visible on the live Related Products section, which renders
 * "Read more" today.
 *
 * ⚠ BOTH CLASSES ARE REQUIRED, AND THE FIRST ATTEMPT AT THIS RULE USED ONLY
 * `.add_to_cart_button` AND WAS WRONG. WooCommerce puts `add_to_cart_button` on
 * a VARIABLE product's button too — `add_to_cart_button product_type_variable`,
 * labelled "Select options" — so that selector alone squeezed a 14-character
 * text label into a 40px box and produced exactly the unlabelled "+" this rule
 * exists to prevent. Caught by assertion 7 of the verify spec, which is why
 * that assertion tests a width and not just a class.
 *
 * `product_type_simple` alone is not enough either: "Read more" carries it and
 * is not an add-to-cart at all. The pair is the discriminator.
 *
 * Shaping every button into a 40x40 icon would turn "Select options" and
 * "Read more" into a plus sign that opens a product page — a control that lies
 * about what it does. The text variants are therefore left to render as text,
 * which is honest and ugly, and Gary has the question (DECISIONS.md
 * 2026-08-06).
 *
 * DO NOT "fix" the inconsistency by widening this selector.
 * ═════════════════════════════════════════════════════════════════════════ */

.tc-pdp .tc-pdp__crosssell-button {
	flex: 0 0 auto;
	margin-block: 0;
	margin-inline-start: 0.75rem;
	max-inline-size: 40%;
}

.tc-pdp .tc-pdp__crosssell-button .wp-block-button__link {
	background-color: transparent;
	border: 1px solid var(--wp--custom--color--secondary--600, #057db9);
	border-radius: 2px;
	color: var(--wp--custom--color--secondary--600, #057db9);
	display: inline-flex;
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.875rem;
	font-weight: 500; /* G01, corrected to 500 on 2026-09-15 */
	line-height: 1.4286; /* 20/14 */
	padding: 0.5rem 0.75rem;
	text-align: center;
}

/* The drawn shape — icon only, and only where the label really is "Add to cart". */
.tc-pdp .tc-pdp__crosssell-button .add_to_cart_button.product_type_simple {
	align-items: center;
	block-size: 40px;
	inline-size: 40px;
	justify-content: center;
	padding: 0.5rem;
}

/*
 * The label is HIDDEN, not removed. Woo's button carries its accessible name in
 * this span and rewrites it to "1 in cart" after an AJAX add, so deleting it or
 * setting `font-size: 0` would cost a screen-reader user both the name and the
 * confirmation. Clipped instead.
 */
.tc-pdp .tc-pdp__crosssell-button .add_to_cart_button.product_type_simple > span {
	block-size: 1px;
	clip-path: inset(50%);
	inline-size: 1px;
	overflow: hidden;
	position: absolute;
	white-space: nowrap;
}

.tc-pdp .tc-pdp__crosssell-button .add_to_cart_button.product_type_simple::before {
	background-color: currentcolor;
	block-size: 1rem;
	content: "";
	display: block;
	flex: 0 0 1rem;
	inline-size: 1rem;

	/* stylelint-disable-next-line property-no-vendor-prefix -- mask needs the prefix pair */
	-webkit-mask: var(--tc-icon-plus) no-repeat center / contain;
	mask: var(--tc-icon-plus) no-repeat center / contain;
}

/*
 * `#f0f9ff` is `color/secondary/50`. It is in the node's variable set but is not
 * painted in the default state, so this is INFERRED as the hover/focus fill
 * rather than read off the board — approved on that basis, 2026-08-06.
 */
.tc-pdp .tc-pdp__crosssell-button .wp-block-button__link:hover,
.tc-pdp .tc-pdp__crosssell-button .wp-block-button__link:focus-visible {
	background-color: var(--wp--custom--color--secondary--50, #f0f9ff);
}

.tc-pdp .tc-pdp__crosssell-button .wp-block-button__link:focus-visible {
	outline: var(--focus-width, 3px) var(--focus-style, solid)
		var(--color-focus, #123b9e);
	outline-offset: var(--focus-offset, 2px);
}

/* No "View cart" link after an add — the header drawer opens instead. The
   reasoning, and the shared card's copy, are in product-grid.css. */
.tc-pdp .tc-pdp__crosssell-button.wc-block-components-product-button > span:has(> a.added_to_cart.wc_forward) {
	display: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * THE KLAVIYO REVIEWS WIDGET.
 *
 * Frame: the "Customer Reviews" section of `docs/design/product-detail/
 * desktop.png` (43324:17376). Mounted by `\3c !-- wp:klaviyo/product-reviews /-->`
 * at the end of product-detail.php.
 *
 * ── ⚠ READ THIS BEFORE EDITING ANYTHING BELOW ──────────────────────────────
 *
 * NONE OF THIS MARKUP IS OURS, AND IT IS NOT EVEN THE PLUGIN'S. The block
 * prints an empty `<div id="klaviyo-reviews-all">` and Klaviyo's remote onsite
 * JS fills it in at runtime. Every `.kl_reviews__*` class below is a
 * third-party runtime contract that can change without a plugin update and
 * without anything in this repo moving.
 *
 * Three consequences, all deliberate:
 *
 *   1. This block styles the CHROME the widget draws in every state — the
 *      section heading, the tab heading, the column, the section rhythm, the
 *      one button, and (since 2026-09-01) the summary score, the histogram
 *      labels and the filter row. It does NOT reach into review CARDS, star
 *      glyphs or the histogram FILL. See the DEFERRED note at the end — which
 *      this line used to promise and which did not exist until now.
 *   2. Every selector is scoped under `.tc-pdp`, so a Klaviyo class colliding
 *      with something elsewhere on the site cannot be reached from here.
 *   3. It is written to DEGRADE TO NOTHING. A renamed Klaviyo class makes a
 *      rule inert; it does not make the widget wrong. No rule here hides
 *      anything or moves anything into place from elsewhere.
 *
 * SPECIFICITY IS NOT A FIGHT, and that is worth stating because it looks like
 * one. Klaviyo ships its widget CSS inside `@layer base`, and an UNLAYERED
 * author rule beats every layered one whatever their specificities — which is
 * why these can stay at the specificity they read at.
 *
 * ⚠ THE PLUGIN ALSO PRINTS INLINE `\3c style>` INTO `wp_head` AND INTO THE BLOCK
 * (`plugins/ftm-klaviyo-extended/inc/frontend.php`, five separate blocks, some
 * with `!important`). That CSS is unlayered too, so it is a real cascade
 * neighbour and load order decides. Nothing here currently collides with it —
 * it owns `.ftmkrev-*`, the collapse behaviour and the star colour, and this
 * owns `.kl_reviews__*` — but a rule added here that touches `.ftmkrev-*` will
 * be in a fight, and should be made there instead.
 * ═══════════════════════════════════════════════════════════════════════════ */

/*
 * THE COLUMN AND THE SECTION RHYTHM — the two things that made this widget read
 * as a bolted-on third-party box rather than a section of the page.
 *
 * THE COLUMN. Measured at 1440 before this rule: the widget centred itself in
 * `max-width: 1100px` with 24px of its own inline padding, its inner container
 * added 10 more, and the "Customer reviews" heading started at x=204 while
 * "Related Products" — the section directly above, in the same column — started
 * at x=40. **164px of inset that the board does not draw**: in
 * `docs/design/product-detail/desktop.png` the two headings share a left edge
 * to the pixel (x=131 in the export, both).
 *
 * Zeroed rather than re-specified. The widget is a block in a constrained WP
 * layout and the layout already knows how wide the column is; every number the
 * widget supplies for itself is a second opinion.
 *
 * THE RHYTHM. The widget shipped a flat 24px above it, against the
 * `clamp(2.5rem, 1.071rem + 7.143vw, 4rem)` that separates every other section
 * of this pattern — `.tc-pdp__related` directly above uses exactly this, and
 * the board draws the two gaps as one rhythm.
 *
 * ⚠ THE COLUMN'S WIDTH, NOT `none` (QA batch D, V-P4 / C-3, 2026-09-29). This
 * was `max-inline-size: none`, which zeroed the widget's 1100 and ALSO lifted
 * the constrained layout's own cap: the widget is plugin markup, not
 * `alignwide`, so at 1920 it ran 40 -> 1880 (its top rule too) while
 * Related Products and the page column sit at 260 -> 1660 (the wide size,
 * board 43324:17454). Capped at the wide size, the layout's auto margins
 * centre it on that column. At 1480 and below the band is narrower than the
 * cap, so nothing there moves.
 */
.tc-pdp #klaviyo-reviews-all {
	margin-block-start: clamp(2.5rem, 1.071rem + 7.143vw, 4rem);
	max-inline-size: var(--wp--style--global--wide-size, 1400px);
	padding-inline: 0;
}

.tc-pdp #klaviyo-reviews-all .reviews_all_container {
	padding-inline: 0;
}

/*
 * "Customer reviews" — `title-large`, and that is a MEASURED role rather than a
 * chosen one. In the desktop export the string runs 108px at a 0.501 scale =
 * 215.6px at 1920; the display face at 20px draws it 160.6px, so the board's
 * size is 20 x 215.6 / 160.6 = **26.85px**. `title-large` is 1.6875rem = 27px.
 *
 * The widget drew it at 20.02px in **Inter**, near-black (rgb(32, 34, 35)) —
 * the wrong face, the wrong size and the wrong colour, in the one place on the
 * page a section heading appears. Weight, tracking and line-height follow
 * `.tc-brand-grid__heading`'s F23 treatment: the preset carries size alone.
 *
 * ⚠ THE COLOUR HAS TO BE RESET ONE LEVEL UP, and `color: inherit` on the
 * heading alone is NOT enough — that was measured the wrong way round first.
 * `.reviews_all_container` correctly inherits the page's rgb(10, 32, 91), and
 * then `.kl_reviews__summary` sets rgb(32, 34, 35) for its whole sub-tree, so a
 * heading inheriting from it inherits the near-black. rgb(32, 34, 35) is in no
 * part of this palette; restoring inheritance at the summary puts the heading,
 * the "0 reviews" count and the histogram's own labels back on the page's text
 * colour in one declaration rather than three.
 *
 * `inherit`, not a token, because the page above already holds the right value
 * and naming it again here would be a second place to change it.
 */
.tc-pdp #klaviyo-reviews-all .kl_reviews__summary {
	color: inherit;
}

.tc-pdp #klaviyo-reviews-all .kl_reviews__summary__header {
	color: inherit;
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--title-large, 1.6875rem);
	font-weight: 700;
	letter-spacing: -0.02em;
	line-height: 1.185; /* 32/27 */
}

/*
 * The "Reviews" tab heading. Already 20px and already 700 — `title-small`, the
 * same role the accordion rows above it take — so only the face and the colour
 * were wrong.
 */
.tc-pdp #klaviyo-reviews-all .kl_reviews__list__tab {
	color: inherit;
	font-family: var(--wp--preset--font-family--display);
	letter-spacing: -0.02em;
}

/*
 * "Write a review" — the only control the widget draws, and the only one the
 * board draws in this section. It shipped 12px Inter 400 on rgb(13, 26, 122),
 * which is neither the site's button type nor any colour in the palette.
 *
 * The target is the button this page already uses — the product card's, whose
 * rules are directly above in `product-grid.css`: `label-medium` at 500 (G01 took it to 600 for a day; corrected 2026-09-15), white
 * on `secondary-600`, `secondary-800` on hover and focus, 2px radius.
 * Restated rather than shared, because there is no button class on this element
 * to hang a shared rule on and putting one there would mean template work
 * inside a third-party runtime.
 *
 * THE ONE DEVIATION IS THE INLINE PADDING, and it is stated rather than
 * inherited. The card's button is `padding: xsmall` on all four sides, which
 * works because it is FULL WIDTH — its inline padding is invisible. This one is
 * intrinsically sized, so it takes `small` (12) inline against the same
 * `xsmall` (8) block. Both are scale steps; neither is a new number.
 */
.tc-pdp #klaviyo-reviews-all .kl_reviews__button {
	background-color: var(--wp--custom--color--secondary--600, #057db9);
	border-radius: 2px;
	color: var(--wp--custom--color--base--white, #fff);
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--label-medium, 0.875rem);
	font-weight: 500; /* G01, corrected to 500 on 2026-09-15 */
	line-height: 1.4286; /* 20/14 */
	padding: var(--wp--preset--spacing--xsmall, 0.5rem)
		var(--wp--preset--spacing--small, 0.75rem);
}

.tc-pdp #klaviyo-reviews-all .kl_reviews__button:hover,
.tc-pdp #klaviyo-reviews-all .kl_reviews__button:focus-visible {
	background-color: var(--wp--custom--color--secondary--800, #075985);
}

/*
 * Same ring as every other control on this page — see the accordion control's
 * note for why it has to be declared per-element rather than inherited from
 * theme.json.
 */
.tc-pdp #klaviyo-reviews-all .kl_reviews__button:focus-visible {
	outline: var(--focus-width, 3px) var(--focus-style, solid)
		var(--color-focus, #123b9e);
	outline-offset: var(--focus-offset, 2px);
}

/* ═══════════════════════════════════════════════════════════════════════════
 * THE SUMMARY SCORE, THE HISTOGRAM LABELS AND THE FILTER ROW (2026-09-01).
 *
 * Design review: "match the mockup's layout/typography as far as the
 * Klaviyo-rendered markup allows". The chrome above was already done; this is
 * the rest of what the theme can honestly reach.
 *
 * ⚠ EVERY VALUE BELOW REPLACES AN OFF-PALETTE ONE, and they were measured, not
 * assumed. Klaviyo draws this sub-tree in **Inter** — a face this site does not
 * load — with two greys that belong to no part of this palette:
 *
 *   rgb(32, 34, 35)    near-black   the score, the pills, the empty state
 *   rgb(96, 106, 114)  mid grey     "/ 5", "0 reviews", the histogram labels
 *   rgb(229, 229, 229) track grey   the histogram bar's background
 *
 * `inherit` where the page's own ink is right, and a NEUTRAL TOKEN where a
 * genuinely quieter grey is wanted — neutral-500 `#6C757D` is this palette's
 * answer to Klaviyo's `rgb(96, 106, 114)` and is already the muted text colour
 * elsewhere in the theme. Nothing new enters the palette, which is the point:
 * issue #158 is about removing literal colours, and this section was 100% of
 * this pattern's remaining ones.
 *
 * ⚠ SIZES ARE LEFT ALONE ON PURPOSE. The widget's 48/20/12 scale is close
 * enough to the board's and, unlike the faces and the greys, changing it would
 * be re-laying-out a third party's component from the outside — the thing the
 * opening note says this block does not do. Face, weight and colour are
 * presentation the page owns; the metric grid is the widget's.
 * ═══════════════════════════════════════════════════════════════════════════ */

.tc-pdp #klaviyo-reviews-all .kl_reviews__summary__average_value {
	color: inherit;
	font-family: var(--wp--preset--font-family--display);
	letter-spacing: -0.02em;
}

.tc-pdp #klaviyo-reviews-all .kl_reviews__summary__average_denominator,
.tc-pdp #klaviyo-reviews-all .kl_reviews__summary__stars__count,
.tc-pdp #klaviyo-reviews-all .kl_reviews__histogram__value {
	color: var(--wp--custom--color--neutral--500, #6c757d);
	font-family: var(--wp--preset--font-family--body);
}

/* The "5", "4", "3"… down the left of the histogram. The board draws these in
   the page's own ink, not in the muted grey the values beside them take. */
.tc-pdp #klaviyo-reviews-all .kl_reviews__histogram__rating {
	color: inherit;
	font-family: var(--wp--preset--font-family--body);
}

/*
 * The empty track. `--foreground` is deliberately NOT here — see the DEFERRED
 * note below; it is the plugin's, with `!important`, and reaching for it from
 * this file would be the cascade fight the opening note warns against.
 */
.tc-pdp #klaviyo-reviews-all .kl_reviews__histogram__bar--background {
	background-color: var(--wp--custom--color--neutral--100, #e9ecef);
}

.tc-pdp #klaviyo-reviews-all .kl_reviews__filters__pill,
.tc-pdp #klaviyo-reviews-all .kl_reviews__input_with_search_icon {
	color: inherit;
	font-family: var(--wp--preset--font-family--body);
}

/* "No reviews yet" — an <h2> the widget draws in Inter. Same face as the two
   headings above it, so the section reads as one thing in its empty state. */
.tc-pdp #klaviyo-reviews-all .kl_reviews__list_empty_state {
	color: inherit;
	font-family: var(--wp--preset--font-family--display);
	letter-spacing: -0.02em;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * ⚠ DEFERRED — WHAT THIS BLOCK STILL DOES NOT TOUCH, AND WHY EACH IS OUT.
 *
 * The opening note has promised this list since the block was written and it
 * did not exist. Written 2026-09-01 from measurement rather than memory.
 *
 * 1. THE STAR COLOUR AND THE HISTOGRAM FILL — NOT OURS, AND NOT A CSS PROBLEM.
 *    `plugins/ftm-klaviyo-extended/inc/frontend.php:51-53,165` prints
 *    `--klv-star-color` into `:root` and applies it to
 *    `.kl_reviews__histogram__bar--foreground` with `!important`. Its value is
 *    the wp_option `klaviyo_star_color`, currently `#F9E335`. The board draws
 *    **#F59E0B** (measured off the export's own star and bar pixels).
 *
 *    ⚠ SO THE FIX IS A SETTING, NOT A RULE, AND IT IS FLAGGED RATHER THAN
 *    OVERRIDDEN. The plugin exposes an admin field for it; a
 *    `option_klaviyo_star_color` filter here would win the value and silently
 *    break that panel. It is also per-environment database state, which a
 *    session may not set on staging or production. On the cutover list.
 *
 * 2. REVIEW CARDS — NOTHING TO STYLE ON THIS ENVIRONMENT. The widget is filled
 *    at runtime by Klaviyo's remote JS against their account; this install
 *    returns zero reviews on every product, so the card markup never renders
 *    here. Writing rules against classes nobody has seen would be guessing, and
 *    the opening note's rule 3 (degrade to nothing) is not a licence for that.
 *
 * 3. THE MEDIA STRIP — same reason as 2, and it needs reviews WITH photographs.
 *
 * 4. THE WIDGET'S METRIC GRID — sizes, gaps and the two-column split. Close
 *    enough to the board, and re-laying-out a third party's component from
 *    outside is what this block exists not to do.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Phone tap targets: eyebrow, Ask an Expert, Expand (QA batch A, 2026-09-27)
 *
 * Measured at 360-430 against the 44×44 the phone brief requires: eyebrow
 * category link 20 tall, expert phone / email 20 tall, gallery Expand 40×40.
 * All three get transparent pseudo-element hit boxes; nothing drawn moves.
 *
 * - EYEBROW: 12px above and below = 44. Below it is the H1, not a control.
 * - PHONE / EMAIL: stacked below md with an 8 / 1px rule / 8 pitch of 37, so
 *   symmetric 44 targets would overlap by 7. They are LOPSIDED instead: the
 *   phone reaches 17 up (over the "Ask an Expert" title, which is text) and 7
 *   down; the email reaches 8 up and 16 down, exactly to the card's border.
 *   44 each, 2px apart.
 * - EXPAND: 2px round the plugin's 40 box = 44. Themed here, not in the shared
 *   ftm-blocks stylesheet, like the rest of this control's overrides.
 */
@media (max-width: 47.9375rem) {
	.tc-pdp .tc-pdp__eyebrow.tc-pdp__eyebrow a,
	.tc-pdp .tc-pdp__expert-contact a {
		position: relative;
	}

	.tc-pdp .tc-pdp__eyebrow.tc-pdp__eyebrow a::after,
	.tc-pdp .tc-pdp__expert-contact a::after,
	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-expand::after {
		content: "";
		inset-inline: 0;
		position: absolute;
	}

	/* Centred and sized outright: the link is INLINE, so its box is the
	   font's content area (~17), not the 20 line, and -12 gave 42. */
	.tc-pdp .tc-pdp__eyebrow.tc-pdp__eyebrow a::after {
		block-size: 44px;
		inset-block-start: calc(50% - 22px);
	}

	.tc-pdp .tc-pdp__expert-contact a[href^="tel:"]::after {
		inset-block: -17px -7px;
	}

	.tc-pdp .tc-pdp__expert-contact a[href^="mailto:"]::after {
		inset-block: -8px -16px;
	}

	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-expand::after {
		inset: -2px;
	}
}

/* ── Phone tap targets, round three (QA batch F, 2026-09-28) ─────────────────
 *
 * docs/reports/qa-2026-09-28-review.md N2-N4 and this batch's sweep, measured
 * at 360 and 390 on RPA42 (four images, five cross-sells), 1207HAF (a short
 * eyebrow) and 232-ATSC-4K. Each control must own a 44×44 square; the
 * cross-sell category link keeps the card kit's 24 (D4).
 *
 * - BREADCRUMBS (N4): removed with the breadcrumb itself (I33, 2026-10-15).
 *   Its 44 hit boxes, the 45 line and the R4-1 no-wrap rule went with it.
 * - EYEBROW (N4): the QA batch A box was full-width-of-the-link, so "LNB's"
 *   (39 wide) reached 39. It now reaches at least 44 either way.
 * - GALLERY (N3): prev / next 40×40 get a 2px ring. Expand had one (QA batch
 *   A) and measured 43 wide: the swiper clips it by a column. 3px, so a lost
 *   column still leaves 45.
 * - CROSS-SELLS (N2):
 *     - arrows 24×24, 16 apart: two centred 44s would overlap. Each reaches
 *       OUTWARD instead (prev 20 to the left; next 16 to the right and 4 into
 *       the gap, because at 360 it ends 18 from the screen's edge) and 10 up
 *       and down. 12 of the gap stays nobody's. Nothing moves.
 *     - card category and title: the category row clips (`overflow: hidden`
 *       for its ellipsis) and the title clips at two lines (`max-block-size`
 *       + `overflow`), so a hit box drawn from either would be clipped with
 *       it. Their boxes are anchored to the card BODY instead: an absolutely
 *       positioned box whose containing block is outside a clipping element
 *       is not clipped by it. The body's column is fixed on phones — a
 *       16px category row (always one line: A2's one-primary-category rule
 *       applies here too), 6 of gap, then the title — so the category takes
 *       0..24 (D4's 24; the row is the swiper's top clip edge, so nothing
 *       above it survives) and the title 24..68 (44), whatever the title's
 *       line count. Below the title is the price, not a control.
 *     - Add to cart 40×40 (or a 36-tall text button, "Select options" /
 *       "Read more"): 4 more to the LEFT and centred to 44 vertically. Its
 *       right edge is the swiper's clip edge, so a centred ring would lose 2.
 *       Not on Woo's loading placeholder, which owns ::after.
 * ────────────────────────────────────────────────────────────────────────── */
@media (max-width: 47.9375rem) {
	.tc-pdp .tc-pdp__eyebrow.tc-pdp__eyebrow a::after {
		inset-inline: min(0px, calc(50% - 22px));
	}

	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-nav::after {
		content: "";
		position: absolute;
		inset: -2px;
	}

	.tc-pdp .tc-pdp__gallery-mount .ftm-product-gallery-expand::after {
		inset: -3px;
	}

	.tc-pdp .tc-pdp__crosssell-control {
		position: relative;
	}

	.tc-pdp .tc-pdp__crosssell-control::after {
		content: "";
		position: absolute;
		inset-block: -10px;
	}

	.tc-pdp .tc-pdp__crosssell-control--prev::after {
		inset-inline: -20px 0;
	}

	/* 4 into the gap and 16 out: at 360 the arrow ends 18 from the screen's
	   edge, so a 20 reach lost 2 to the viewport. */
	.tc-pdp .tc-pdp__crosssell-control--next::after {
		inset-inline: -4px -16px;
	}

	.tc-pdp .tc-pdp__crosssell-cat a ~ a,
	.tc-pdp .tc-pdp__crosssell-cat .wp-block-post-terms__separator {
		display: none;
	}

	.tc-pdp .tc-pdp__crosssell-body {
		position: relative;
	}

	.tc-pdp .tc-pdp__crosssell-cat a::after,
	.tc-pdp .tc-pdp__crosssell-name a::after {
		content: "";
		position: absolute;
		inset-inline: 0;
	}

	/* The 16px row and 8 below it: 24 (D4). Not 4 above and 4 below — the
	   row is the top edge of the swiper's clip, so anything above it is cut
	   (measured: 20). */
	.tc-pdp .tc-pdp__crosssell-cat a::after {
		inset-block-start: 0;
		block-size: 24px;
	}

	/* From the category's 24 down, 44: over the title (22..62) and the top of
	   the price, which is not a control. */
	.tc-pdp .tc-pdp__crosssell-name a::after {
		inset-block-start: 24px;
		block-size: 44px;
	}

	/* Woo clips this button (`overflow: hidden`, for its "added" text slide),
	   which clipped the hit box to the 40 box. `clip` plus a clip margin keeps
	   the clip and lets the target out — the card kit's fix for the same
	   button (product-grid.css, QA batch A). */
	.tc-pdp .tc-pdp__crosssell-button .wp-block-button__link {
		position: relative;
	}

	.tc-pdp
		.tc-pdp__crosssell-button
		.wp-block-button__link:not(.wc-block-components-product-button__button--placeholder) {
		overflow: clip;
		overflow-clip-margin: 8px;
	}

	.tc-pdp
		.tc-pdp__crosssell-button
		.wp-block-button__link:not(.wc-block-components-product-button__button--placeholder)::after {
		content: "";
		position: absolute;
		inset-block: min(0px, calc(50% - 22px));
		inset-inline: min(-4px, calc(100% - 44px)) 0;
	}
}
