/**
 * Bonza, basic product page template: buy box, gallery, sticky buy bar
 * (ClickUp 86eypb6me).
 *
 * Config investigated: Appearance > Customize > WooCommerce > Single Product
 * (gallery style, sticky product info, tabs layout) checked on
 * bonza-retheme.blz.au via `wp theme mod list`. No single/gallery/sticky
 * keys are set; Blocksy is still on stock defaults, so there is no existing
 * Customizer config to reuse or collide with. WooCommerce > Settings >
 * Payments cross-check: staging only exposes a placeholder "cheque" gateway
 * relabelled "Credit/Debit Card (Staging)", which does not represent the
 * live gateway set. The payment-icon row below calls the same
 * bonza_footer_payment_methods() the footer already uses (custom.php,
 * ClickUp 86eypb6aw) rather than a second hardcoded list, so there is one
 * place to fix once that reconciliation task lands.
 *
 * Figma: file cY9QX6IwGVaaGI4r7J2RRF, component set "PDP" (223:35864).
 * Two page-canvas frames both named "BASIC PRODUCT PAGE - D" turned out to
 * be the same component set, one instance per product-type variant
 * (TYPE=SUPERFOODS #14133:153690 on frame 21316:76931, TYPE=BIOTICS
 * #34920:65834 on frame 34926:77030), confirmed via the Figma API:
 * componentId and componentProperties are the same shape, only the parent
 * component differs. Live product data backs this up: bonza-retheme.blz.au
 * has a "Superfoods & Ancient Grains" category (1 product) and a
 * "Bioactive Bites" category (14 products), matching the two variants
 * exactly. This is one template, driven by real product data, with no
 * per-category branching required, the same as any other WooCommerce
 * single-product page.
 *
 * SCOPE: this covers the buy box the task's own estimate breakdown
 * describes (scaffold+gallery, switched states, price/variant binding,
 * sticky bar, WPML/contrast), everything from the breadcrumb down through
 * the "Free VidiVet subscription" band. The same Figma frame also holds a
 * much larger set of long-form marketing sections below that (tabbed
 * "why it's different / how it works / inside the formula / when to
 * expect results / reviews / build your routine / feeding calculator /
 * full FAQ list", 8+ sections with their own imagery and copy). That
 * content runs well past the 7h estimate and this task's Definition of
 * Done, so it is not built here. Flagged explicitly in the ClickUp comment
 * as needing its own scope decision rather than being silently built or
 * silently dropped.
 *
 * Fonts: Nominee (headings/buttons/eyebrow) and Kobe1.1 (body/links) have
 * no @font-face on this site yet (same open item already logged in
 * footer.css/shipping-panel.css). Falls back to the theme's own font vars.
 *
 * Button component (Figma component set "Button" 29049:58925, "Kind=
 * Secondary, Size=Large" variant, used for Add to Cart / FAQ CTA /
 * notify-me / sticky bar): enumerated states are Default (fill #247B4E
 * plus BUTTON SHADOW), Hover (fill #FFC125 yellow, black text), Disabled
 * (fill #247B4E, 0.6 opacity). No Focus/Active/Error/Loading variant
 * exists in Figma for this component; a focus-visible outline is added
 * anyway for the DoD's visible-focus requirement, the same precedent
 * already set in shipping-panel.css for this exact component.
 *
 * WooCommerce default woocommerce_single_product_summary hook order
 * (confirmed against current WooCommerce docs, not assumed): title (5),
 * rating and price (10), short description (20), add to cart (30), meta
 * (40), sharing (50). custom.php only moves price from 10 to 29 so it
 * renders in Figma's order (after the short description and FAQ teaser,
 * right before Add to Cart); every other default stays in place so
 * variations, bundles and the subscriptions plugin keep working exactly
 * as WooCommerce ships them.
 *
 * Contrast (WCAG 2.2 AA, measured):
 * - Body ink #2E2A2A on cream #F8EDE3: 12.30:1, passes AA/AAA (86eyv0k44
 *   GL-05 recompute, was #494545 at about 7.5:1). Same pair already
 *   audited in shipping-panel.css.
 * - White text on #247B4E (button default): about 4.9:1, passes AA normal
 *   text. Same green already used site-wide (footer, shipping panel).
 * - Black text on #FFC125 (button hover): about 13:1, passes AAA. Same
 *   pair already measured in shipping-panel.css.
 * - Selected-thumbnail border #FF6256 on cream is a non-text UI indicator.
 *   Selection state is not conveyed by color alone (JS also sets
 *   aria-current="true" on the active thumbnail button), so this is
 *   flagged for the whole-site WCAG pass (86eypb8gy) alongside the
 *   tab-underline/focus-ring findings already logged there, rather than
 *   treated as a blocker here.
 * - Out-of-stock badge text uses --bpp-red-text (#C6362B), darkened from
 *   the Figma-literal #FF6256 which only measures about 2.9:1 on white.
 *   #C6362B measures about 4.6:1: the same "darken for real text, keep the
 *   Figma value for large/non-text uses" pattern footer.css already uses
 *   for green.
 *
 * @package Blocksy_Child
 * @client  Bonza
 * @date    2026-08-27
 */

/**
 * Custom property scope: WordPress already puts body.single-product on
 * every single-product page (no wrapper div needed for this). The
 * summary column layout below targets WooCommerce's own real markup,
 * div.summary.entry-summary, printed by the default single-product
 * template, not a custom wrapper class.
 */
body.single-product {
	/*
	 * editor-friendly-ok: same documented exception as the rest of this
	 * file (line ~351) -- restyles WooCommerce's own single-product body
	 * class, no Gutenberg block/pattern counterpart exists to register
	 * add_editor_style() against.
	 *
	 * FIX (86eypb6me q4): the page's own base background was left on
	 * Blocksy's stock off-white instead of this cream. Figma's root PDP
	 * instance (node 21316:76932 "PDP", fileKey cY9QX6IwGVaaGI4r7J2RRF,
	 * read live 2026-09-23) fills its whole frame with the same value
	 * --bpp-cream already resolves to below, just never applied to body
	 * itself. Every section that draws its OWN background (the white
	 * "why it's different" panel, the green "how it works" band, etc.)
	 * already sets that background explicitly elsewhere in this stack, so
	 * this only changes what shows through everywhere else -- gutters, the
	 * buy-box column, and every section that has no background rule of
	 * its own.
	 *
	 * Contrast: not a new colour pair -- this is the same cream --bpp-ink
	 * is already measured against three lines below in this same rule
	 * (12.30:1, WCAG AA/AAA) -- this only paints that already-audited pair
	 * under a larger area of the page.
	 */
	background-color: var(--bpp-cream);
	--bpp-cream: var(--bpp-color-cream, #F8EDE3);
	--bpp-ink: var(--bpp-color-ink, #2E2A2A); /* 12.30:1 on cream (WCAG AA/AAA) */
	--bpp-ink-muted: var(--bpp-color-ink-muted, #746E60);
	--bpp-green: var(--bpp-color-green, #247B4E);
	--bpp-yellow: var(--bpp-color-yellow, #FFC125);
	/* The stroke Figma pairs with the yellow hover fill on its Button set
	   (29384:40246). A border colour, not a text colour. */
	--bpp-yellow-stroke: var(--bpp-color-yellow-stroke, #B86E00);
	--bpp-white: var(--bpp-color-white, #FFFFFF);
	--bpp-black: var(--bpp-color-black, #000000);
	--bpp-red-figma: var(--bpp-color-red-figma, #FF6256);
	--bpp-red-text: var(--bpp-color-red-text, #C6362B);
	/* Figma 34773:75358 and EL-749d10cc: the two disc fills behind the feature
	   icons, and the pale mint the allergen banner sits on. All three are
	   backgrounds rather than text colours; the copy on them is --bpp-ink,
	   measured 12.9:1 on the mint. */
	--bpp-amber: var(--bpp-color-amber, #FFA825);
	--bpp-mint: var(--bpp-color-mint, #F5FAF7);
	/* The selected option/panel fill (Figma 34190:83454, 34197:205345) and the
	   unselected option stroke (34190:83451). Both are surfaces rather than
	   text colours; what sits on the cream is --bpp-ink at 12.4:1 or
	   --bpp-green at 4.93:1. */
	--bpp-cream-selected: var(--bpp-color-cream-selected, #FFF9E9);
	--bpp-border-muted: var(--bpp-color-border-muted, #D1D5DB);
	/* The Primary button's hover fill (29049:58918) and the stroke on its
	   disabled state (29049:58913). Both are surfaces; the white label on the
	   dark green measures 9.38:1. */
	--bpp-green-dark: var(--bpp-color-green-dark, #1C5F3C);
	--bpp-coral: var(--bpp-color-coral, #FF9572);
	--bpp-heading-font: "Nominee", var(--theme-font-family, sans-serif);
	--bpp-body-font: "Kobe1.1", var(--theme-font-family, sans-serif);
}

.single-product div.summary.entry-summary {
	max-width: 680px;
	/* A flex item's auto min-width kept this column at its widest child (656px)
	   so it ran 56px past the entry wrapper's right edge at 1440. */
	min-width: 0;
	display: flex;
	flex-direction: column;
	/* 4px is Figma's TIGHT base, from the title block 34190:83335, not the
	   group rhythm. Elements that open a new group in 34190:83334 add the
	   remaining 20px to reach 24. This was 16px, which matched no Figma number
	   and, because Blocksy also spaces each child, summed with it to 51px. */
	gap: 4px;
	color: var(--bpp-ink);
	font-family: var(--bpp-body-font);
}

/* Blocksy's per-child spacing, removed so the gap above is the only lever.
 * `.product:has([data-gallery]) .entry-summary-items > *` in
 * bundle/woocommerce.min.css sets margin-bottom from
 * --product-element-spacing, and at three classes it outranks a two-class
 * child selector, so this carries the same weight the summary rule above
 * already uses. Two spacing systems on one column is how the buy box ended up
 * 51px apart where the design says 24. */
.single-product div.summary.entry-summary > * {
	margin-block: 0;
}

.single-product div.product.type-product {
	max-width: 1512px;
	margin: 0 auto;
	padding: 0 20px;
}

/* The related-products row sits outside div.product, so it carries the same
   20 / 32 / 64 ladder itself. Declared HERE, above the media queries, because
   an unconditional rule placed after them wins at every width: that is the
   mistake the breadcrumb cascade already made once on this file. */
.single-product .bc-recently-viewed.is-width-constrained {
	width: 100%;
	max-width: 1512px;
	margin-inline: auto;
	padding-inline: 20px;
}

/* Breadcrumb (Figma shows "Home > Category", no product-title crumb).
 *
 * Retargeted 2026-09-16 from '.woocommerce-breadcrumb' to Blocksy's real
 * '.ct-breadcrumbs'. The old selector matched nothing: Blocksy renders its
 * own breadcrumb component and never calls woocommerce_breadcrumb(), so this
 * entire block was dead. Verified live on the product page: one
 * '.ct-breadcrumbs' node, zero '.woocommerce-breadcrumb', zero
 * '.bpp-crumb-sep'.
 *
 * Config investigated: dropping the trailing crumb, the separator, and the
 * font and colour are all native Blocksy Customizer options
 * (breadcrumb_page_title, breadcrumb_separator / breadcrumb_custom_separator,
 * breadcrumbsFont, breadcrumbsFontColor) and are set there, not here. See the
 * breadcrumb note in custom.php. Only the container geometry below has no
 * Customizer equivalent, so only that stays in CSS. */
.single-product .ct-breadcrumbs {
	max-width: 1512px;
	margin: 0 auto;
	padding: 8px 20px 20px;
	/* Figma 34306:193734 is a ROW, align-items center, gap 4px. Blocksy left
	   this at its default `display: block` with `gap: normal`, which is inert
	   on a block box, so the gap declaration was doing nothing and the trail
	   rendered with 0px between crumbs. */
	display: flex;
	align-items: center;
	gap: 4px;
	font-family: var(--wp--preset--font-family--kobe, "Kobe1.1", sans-serif);
	font-weight: 700;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: normal;
	text-transform: none;
	color: var(--bpp-green);
}

/* The DOM nests one level deeper than the Figma frame: Blocksy puts "Home"
   AND the chevron inside .first-item, so only the chevron-to-last-item
   distance was ever a container-level gap. The 4px rhythm has to repeat one
   level down for "Home" to chevron to read the same as chevron to the
   current-page crumb. .first-item is flex: 0 0 auto so "Home" + chevron
   never compress at narrow widths; .last-item gets min-width: 0 so a long
   product name can shrink and wrap its own text there instead of being held
   to its intrinsic width by flex's default auto min-width, which was pushing
   the row wider than the column at 500px. */
.single-product .ct-breadcrumbs .first-item {
	display: flex;
	align-items: center;
	gap: 4px;
	flex: 0 0 auto;
}

.single-product .ct-breadcrumbs .last-item {
	min-width: 0;
}

/* B33: at Blocksy's mobile breakpoint (690px) Blocksy sets the trail to
   nowrap with overflow:auto, so on Bites pages "Home > Bioactive Bites
   Supplements > <product>" (354px of content) is cut at the right edge below
   about 425px, and at 320 the product name is out of sight. Let the trail wrap
   onto a second line instead of scrolling or cutting. */
@media (max-width: 690px) {
	body.single-product .ct-breadcrumbs {
		flex-wrap: wrap;
		white-space: normal;
		overflow: visible;
		row-gap: 4px;
	}
}

/* Figma node 34306:193734 draws the trail two-tone: the "Home" link in Ink
   #2E2A2A and the current crumb in Primary Green #247B4E, both in
   Links/Bold 2 (Kobe1.1 700, 14px/16px). Blocksy left links on the global
   link colour, which measured as default blue rgb(40, 114, 250) on the
   deployed page. The container above carries green so the current crumb,
   which Blocksy renders as a bare text node, inherits it. */
.single-product .ct-breadcrumbs a,
.single-product .ct-breadcrumbs a:visited {
	color: var(--bpp-ink);
	text-decoration: none;
}

.single-product .ct-breadcrumbs a:hover,
.single-product .ct-breadcrumbs a:focus-visible {
	color: var(--bpp-ink);
	text-decoration: underline;
}

/* Blocksy wraps each crumb in its own span (.first-item / .last-item, each
   holding a span[itemprop="name"]), so pull those onto the trail's values
   instead of letting theme defaults through. font-family is deliberately
   omitted here: it already inherits, and the editor-friendly gate requires
   every font-family declaration to read a preset var. */
.single-product .ct-breadcrumbs span {
	font-size: inherit;
	font-weight: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
	color: inherit;
}

/* z8udzkebk5: Figma's crumb row (34306:193734) is 16px tall (14/16 text), so
 * the gap from the header card is 20px desktop, 11px tablet, 8px mobile and 20px
 * to the gallery. Blocksy gave each crumb span and link a 24px line box, which
 * made the row 24px: text 3px low under the header and 4px away from the
 * gallery. Pin the line box to the design's 16px. */
body.single-product .ct-breadcrumbs span,
body.single-product .ct-breadcrumbs a {
	line-height: 16px;
}

/* Separator. Figma draws a 12x12 heroicons-mini/chevron-right (5011:120121)
   with a 4px gap either side (the row frame 34306:193734 uses gap: 4px).
   Blocksy emits it as <svg class="ct-separator" width="8" height="8"
   fill="currentColor"> inside .first-item, measured 8x8 with 8px margins, so
   the size is corrected here. Spacing on both sides is no longer this
   element's job: .first-item's own flex+gap now supplies the 4px on the
   "Home" side, and the container's flex+gap supplies the 4px on the
   .last-item side, so a margin here would stack on top of both and double
   the gap. An earlier attempt targeted .ct-divider, which matches nothing on
   this theme. Colour is left on currentColor so the chevron tracks whichever
   crumb it sits in. */
.single-product .ct-breadcrumbs svg.ct-separator {
	width: 12px;
	height: 12px;
	vertical-align: middle;
}

.bpp-trust-row {
	display: flex;
	align-items: center;
	gap: 8px;
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: 16px;
	color: var(--bpp-ink);
}

.bpp-trust-row svg {
	width: 18px;
	height: 18px;
	flex: 0 0 auto;
	color: var(--bpp-green);
}

/* Native WooCommerce rating output, restyled to the Kobe1.1 body pairing
   instead of Blocksy's default star block. Kept as WooCommerce's own
   average/count: no third-party trust-badge platform is confirmed active.
   Figma shows a fixed "Gold Trusted Service Award · 181 reviews" string
   tied to an unnamed review platform; real review data is used here
   instead of repeating that unverifiable literal. Flagged as an open
   item. */
.single-product .woocommerce-product-rating {
	font-family: var(--bpp-body-font);
	font-size: 12px;
	line-height: 14px;
	color: var(--bpp-ink);
	margin: 4px 0;
}

.single-product h1.product_title.entry-title {
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 24px;
	line-height: 1.2;
	text-transform: uppercase;
	color: var(--bpp-ink);
	margin: 0;
}

.bpp-stock-row {
	display: flex;
	align-items: center;
	gap: 6px;
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: 16px;
}

.bpp-stock-row .bpp-stock-dot {
	width: 10px;
	height: 10px;
	border-radius: 999px;
	flex: 0 0 auto;
}

.bpp-stock-row--in .bpp-stock-dot {
	background: var(--bpp-green);
}

.bpp-stock-row--in {
	color: var(--bpp-ink);
}

.bpp-stock-row--out .bpp-stock-dot {
	background: var(--bpp-red-text);
}

.bpp-stock-row--out {
	color: var(--bpp-red-text);
}

/* editor-friendly-ok: this restyles the native WooCommerce single-product
   template, which has no Gutenberg block/pattern surface to register a
   --wp--preset-- color against (same exception already used in
   footer.css/shipping-panel.css for this site's brand colors, which are
   not part of a site-wide theme.json preset yet, task 86eyk8nqd).
   Backorder ("order caution" in the Figma component's boolean flags), a
   third stock state distinct from plain In Stock and Out of Stock.
   #8A5A00 is a darkened amber (not a Figma-drawn value, no backorder
   example exists in any drawn instance): measures about 4.6:1 on cream
   #F8EDE3, same darken-for-real-text approach already used for
   --bpp-red-text. Needs a real design QA pass once a backorder product
   exists to compare against, same open item already flagged for the
   out-of-stock state. */
.bpp-stock-row--caution .bpp-stock-dot {
	background: var(--bpp-caution-text, #8A5A00);
}

.bpp-stock-row--caution {
	color: var(--bpp-caution-text, #8A5A00);
}

.bpp-notify-form {
	margin-top: 8px;
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}

.bpp-notify-form a.bpp-btn {
	font-size: 12px;
	padding: 10px 16px;
}

/* Daily-portion panel, Figma 34685:43006. Superfoods only.
 *
 * Column at 12px gap: an intro group (8px gap) over an action group (8px gap).
 * The button is Figma's Button set (29049:58925) Size=Large, Kind=Secondary:
 * 48px tall, 16px padding, 999px radius, green fill, 8px between label and
 * the 18x18 pencil. Its label is 10px/14px at 0.04em, which is genuinely
 * smaller than the rest of the buy box and is reproduced as drawn.
 *
 * Hover comes from that set's own State=Hover variant (#FFC125 fill, #B86E00
 * 1.5px stroke, ink label at 8.72:1). The set draws no focus variant, so the
 * outline is this theme's own. */
/* 86eypb6me, buy-box queue: repositioned into the add-to-cart form alongside
 * .bpp-faq-teaser__question above -- same reason, same 20px top margin, see
 * that rule's comment for the full explanation. */
.bpp-portion {
	display: flex;
	flex-direction: column;
	gap: 12px;
	margin-top: 20px;
}

.bpp-portion__intro,
.bpp-portion__action {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.bpp-portion__heading {
	margin: 0;
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var(--bpp-ink); /* 12.30:1 on cream */
}

.bpp-portion__body {
	margin: 0;
	font-family: var(--bpp-body-font);
	font-weight: 400;
	font-size: 16px;
	line-height: 20px;
	color: var(--bpp-ink);
}

.bpp-portion__cta {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	height: 48px;
	padding: 16px;
	border: 1px solid var(--bpp-green);
	border-radius: 999px;
	background: var(--bpp-green);
	color: var(--bpp-white); /* white on green, 5.22:1 */
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 10px;
	line-height: 14px;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	text-decoration: none;
	box-shadow: 0 4px 6px -2px rgba(0, 0, 0, 0.03), 0 12px 16px -4px rgba(0, 0, 0, 0.08);
}

.bpp-portion__cta:hover {
	background: var(--bpp-yellow);
	border: 1.5px solid var(--bpp-yellow-stroke);
	color: var(--bpp-ink); /* ink on the yellow hover fill, 8.72:1 */
}

.bpp-portion__cta:focus-visible {
	outline: 2px solid var(--bpp-green);
	outline-offset: 2px;
}

.bpp-portion__cta-icon {
	flex: 0 0 auto;
	display: inline-flex;
}

.bpp-portion__cta-icon svg {
	width: 18px;
	height: 18px;
	display: block;
}

.bpp-portion__note {
	margin: 0;
	font-family: var(--bpp-body-font);
	font-weight: 400;
	font-size: 14px;
	line-height: 16px;
	color: var(--bpp-ink);
}

/* Compact form (Biotics, heading + button only, no body/note paragraphs --
 * bonza_pdp_daily_portion_panel(), 86eypb6me).
 *
 * The gap matches the four-string Superfoods form, measured from the Biotics
 * node in checklist-pdp.json at bp 1512 (the arithmetic is in the commit that
 * added this). The class stays because the two forms are separate design
 * nodes and a later change to one must not silently move the other, and the
 * selector is doubled so that protection comes from specificity rather than
 * source order -- these two rules ending up in a different order, or in
 * separate files concatenated the other way round, would otherwise flip the
 * tie and stop it anchoring anything. */
.bpp-portion.bpp-portion--compact {
	gap: 12px;
}

/* Pack-size / weight option cards, Figma 34190:83450 (Superfoods) and
 * 34920:65918 (Biotics).
 *
 * The behaviour is Blocksy Companion Pro's native variation swatches, switched
 * on in config and set to `button` per attribute, so the real
 * <select name="attribute_*"> still drives WooCommerce. Everything below is
 * presentation over that native markup:
 *   button.ct-swatch-container > span.ct-tooltip + span.ct-swatch >
 *   span.ct-swatch-content, plus the two spans pack-size-cards.php appends.
 * Structure read off the deployed page before any of this was written.
 *
 * Figma draws only a default and a selected state for this card. Hover and
 * focus are this theme's own: hover borrows the selected fill, and focus gets
 * a real outline, because a keyboard user needs one whatever the mock omits. */
.single-product .ct-variation-swatches {
	display: flex;
	/* Wraps on option COUNT, not a fixed column count. Figma 34920:65918 shows
	   Biotics as a 2x2 because it has four options at that width; Superfoods
	   has two and other products may have three or five, so a hardcoded grid
	   would strand a card alone on a row. gap is Figma's own "20px 10px",
	   row then column. */
	flex-wrap: wrap;
	align-items: stretch;
	gap: 20px 10px;
	width: 100%;
}

/* Blocksy's own tooltip repeats the label that is already on the card. */
.single-product .ct-variation-swatches .ct-tooltip {
	display: none;
}

/* Blocksy's product-elements layout places a Divider element (`divider_1`)
   directly above the add-to-cart block, which on a variable product is the
   pack-size cards. Figma draws the "Choose your pack size" label with no rule
   above it, and the 1px rgb(225, 232, 237) line read as a stray border over
   the heading (Lan's report, 2026-09-19). Hidden only when the element it
   introduces is a variations form, so simple products keep their layout. The
   Customizer alternative is removing the Divider layer from WooCommerce >
   Single Product > Product Elements, which would drop it on every product. */
.single-product .summary .ct-product-divider:has(+ .ct-product-add-to-cart form.variations_form) {
	display: none;
}

/* 34190:83451, the default card. `flex: 1 1 0` is Figma's `fill`. */
/* "Choose your pack size", the label over the option cards.
 *
 * Figma 34190:83449 inside the group 34190:83448: Nominee Heavy Extended 900,
 * 14px/16px, letter-spacing 0.02em, uppercase, ink, with a 12px gap down to
 * the card row 34190:83450. The text itself is swapped in PHP, because
 * WooCommerce prints the attribute name and Blocksy appends the selected
 * value to it; see the label filter in product-page/pack-size-cards.php.
 *
 * The 12px lives on the table row rather than as a margin on the label,
 * because WooCommerce renders this as a <table class="variations"> and a
 * margin on the <th> would not move the <td> beneath it. */
.single-product table.variations th.label label {
	font-family: var(--bpp-heading-font);
	/* !important only on the weight, and only because blaze-blocksy's
	   components/woo-single.css sets `.variations .label label { font-weight:
	   700 !important }`, which nothing can outrank without matching it. Nominee
	   ships a single 900 face on this site, so 700 was being synthesised rather
	   than rendered, which is visibly lighter than the drawn label. */
	font-weight: 900 !important;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var(--bpp-ink);
	display: block;
	margin-bottom: 12px;
}

/* Option cards must fit their label, whatever the product puts in it.
 *
 * Blocksy sizes a swatch as a small fixed chip, which is right for a colour
 * dot and wrong for a text label: `.ct-swatch` measured a fixed 35px with
 * `overflow: clip` while the Biotics labels need 50px across three lines, so
 * the last line was cut mid-glyph. Measured on product 115308 before the fix:
 * .ct-swatch 116.5x35, scrollHeight 50.
 *
 * The type is NOT shrunk to compensate. Figma 34920:65918 sets both lines at
 * 14px, and shrinking text to fit a box the theme picked would break the type
 * scale to hide a sizing bug.
 *
 * The row adapts to the OPTION COUNT rather than a fixed column count. Figma
 * happens to show Biotics as a 2x2 because it has four options at that width,
 * but Superfoods has two short ones and other products may have three or five,
 * so a hardcoded grid would strand a card on its own row. A flex basis with a
 * min width lets any count settle evenly, and `align-items: stretch` keeps
 * every card in a row the height of the tallest. */
/* `body` on the front of these two selectors is doing real work, not padding.
 *
 * Blocksy's bundle/variation-swatches.min.css sets
 * `[data-swatches-type] .ct-swatch-container .ct-swatch { height:
 * var(--swatch-size, 30px); overflow: clip }`. That is an attribute plus two
 * classes, (0,0,3,0), which TIES a three-class child selector, and its bundle
 * is enqueued after this file, so on a tie it wins. The first version of this
 * fix was `.single-product .ct-swatch-container .ct-swatch`, measured on the
 * deployed page as still 35px and still clipping all four labels. Adding the
 * element takes it to (0,0,3,1) and settles it on specificity rather than on
 * load order, which is the part that will not drift.
 *
 * --swatch-size is set as well as height, because it is the variable Blocksy
 * reads and leaving it at 30px would keep anything else that consumes it
 * wrong. */
body.single-product .ct-swatch-container .ct-swatch {
	--swatch-size: auto;

	height: auto;
	min-height: 35px;
	overflow: visible;
	display: flex;
	align-items: center;
	justify-content: center;
}

body.single-product .ct-swatch-container .ct-swatch-content {
	height: auto;
	overflow: visible;
	white-space: normal;
}

.single-product .ct-variation-swatches .ct-swatch-container {
	flex: 1 1 160px;
	min-width: 160px;
	height: auto;
}

.single-product .ct-swatch-container {
	position: relative;
	flex: 1 1 0;
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 4px;
	padding: 16px 20px;
	border: 2px solid var(--bpp-border-muted);
	/* Measured 200px before this override: Blocksy's `data-swatches-shape`
	   defaults to "round" and applies a pill radius at a specificity this
	   selector alone does not beat. Figma draws 12px (34190:83451). */
	border-radius: 12px !important;
	/*
	 * FIXED 2026-09-24 (86eypb6me q18-buybox S30): unselected swatch cards
	 * were rendering the page's own cream fill instead of white (Figma
	 * 34190:83451 draws the unselected card #FFFFFF, only the selected/
	 * hover variant 34190:83454 is cream) -- same root cause as the
	 * border-radius override directly above this line: Blocksy's own
	 * compiled swatch CSS sets a background on this exact element at a
	 * specificity this plain selector does not beat, so !important is
	 * added here for the identical reason it was already added there, not
	 * a new pattern. */
	background: var(--bpp-white) !important;
	cursor: pointer;
	text-align: center;
}

/* !important (86eypb6me q18-buybox S30): the base rule above now sets its
   own background !important to beat Blocksy's compiled swatch default,
   which otherwise wins on source order alone against this equal-specificity
   :hover rule. !important vs !important then falls back to specificity, and
   this pseudo-class selector (0,3,0) already outranks the base rule (0,2,0),
   so this one line keeps the hover-cream state working. */
.single-product .ct-swatch-container:hover {
	background: var(--bpp-cream-selected) !important;
}

.single-product .ct-swatch-container:focus-visible {
	outline: 2px solid var(--bpp-green);
	outline-offset: 2px;
}

/* 34190:83454, the selected card. Blocksy sets aria-pressed on the button.
   !important on background (86eypb6me q18-buybox S30): same reasoning as
   the :hover rule directly above -- this attribute selector (0,3,0) still
   outranks the base rule (0,2,0) once both carry !important. */
.single-product .ct-swatch-container[aria-pressed="true"] {
	border-color: var(--bpp-amber);
	background: var(--bpp-cream-selected) !important;
}

/* Blocksy's own swatch chrome carries a border and a pill radius; the card is
   the control here, so the inner span keeps only the label. */
.single-product .ct-swatch-container .ct-swatch {
	display: block;
	width: auto;
	height: auto;
	min-width: 0;
	min-height: 0;
	border: 0;
	border-radius: 0;
	background: none;
	padding: 0;
}

/* The weight line. Two products reach this rule by different routes and Figma
 * draws them identically: on Superfoods the attribute is a real taxonomy so
 * this is the term name ("2.5KG"), and on Biotics it is a custom attribute
 * holding one long string, which pack-size-cards.php splits for display into
 * .bpp-swatch-size plus a dose line. */
.single-product .ct-swatch-container .ct-swatch-content,
.single-product .ct-swatch-container .bpp-swatch-size {
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var(--bpp-ink); /* 13.72:1 on white */
}

/* When the label was split, .ct-swatch-content holds the two lines rather than
   text of its own, so it becomes the column and carries Figma's 4px gap. */
.single-product .ct-swatch-container .ct-swatch-content:has(.bpp-swatch-size) {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 4px;
}

/* .bpp-swatch-size is listed explicitly, not left to inherit. It carries its
   own `color` from the weight-line rule above, and a child's own colour beats
   anything inherited from the parent, so setting green on .ct-swatch-content
   alone left the split cards' weight line ink. Measured on the deployed page:
   selected card reported rgb(46, 42, 42) where Figma wants #247B4E. */
.single-product .ct-swatch-container[aria-pressed="true"] .ct-swatch-content,
.single-product .ct-swatch-container[aria-pressed="true"] .bpp-swatch-size {
	color: var(--bpp-green); /* 4.93:1 on the selected cream fill */
}

/* The second line: the descriptor field on a taxonomy attribute, or the dose
 * half of a split label. Sentence case, not uppercase, because Figma writes
 * "Small-Medium dogs" and "1 chew/day" as drawn. */
.single-product .bpp-swatch-desc,
.single-product .ct-swatch-container .bpp-swatch-dose {
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: normal;
	text-transform: none;
	color: var(--bpp-ink); /* 13.72:1 on white, 12.4:1 on the selected fill */
}

/* Selected turns ONLY the weight line green. The dose line sits inside
 * .ct-swatch-content, so it would otherwise inherit that green and flatten the
 * two lines into one block of colour, losing the hierarchy the card is built
 * on. Figma keeps it ink in both states. */
.single-product .ct-swatch-container[aria-pressed="true"] .bpp-swatch-dose,
.single-product .ct-swatch-container[aria-pressed="true"] .bpp-swatch-desc {
	color: var(--bpp-ink);
}

/* ==========================================================================
   Out-of-stock swatches: readable labels, with the state shown by a mark
   ==========================================================================
   Found by running the axe scan on a genuinely out-of-stock product (Boost)
   rather than an in-stock one. It returns 2 serious violations; the in-stock
   Biotics page returns zero. 11 of the flagged nodes are these labels.

   Blocksy dims the whole content box: `.ct-out-of-stock .ct-swatch-content
   { opacity: .4 }` in its woocommerce bundle. Our label colours are fine on
   their own (--bpp-ink 12.30:1, --bpp-green 4.53:1 on cream), but 0.4
   composites them to roughly 2.26:1 against a 4.5:1 requirement.

   No opacity value fixes both lines. Composited over the cream card, --bpp-ink
   needs about 0.7 to clear AA (4.97:1 measured at 0.7), and --bpp-green still
   fails there (2.74:1) because it starts far darker. Chasing a middle value
   would have shipped one line passing and one failing.

   And the exemption does not apply. These are NOT inactive controls: the
   rendered markup is `<button class="ct-swatch-container ct-out-of-stock"
   type="button" aria-pressed="false">` with no `disabled` and no
   `aria-disabled`, so they are focusable, operable and announced. WCAG 1.4.3
   exempts disabled controls; it does not exempt these.

   So the labels come back to full opacity and the unavailability is carried
   by a mark instead of by dimming. Blocksy already has exactly this mode,
   `data-variations-out-of-stock="crossed"`, which is a Customizer setting and
   the config-first answer. The form currently renders `="faded"`.

   The cross below is scoped to the faded mode ONLY. That is deliberate: if
   the Customizer is switched to crossed, Blocksy draws its own cross and this
   rule stops matching, so the two can never double up. If the Customizer is
   never switched, the page still shows a mark rather than an un-dimmed swatch
   with no state signal at all, which is the one outcome worse than today. */
.single-product .ct-swatch-container.ct-out-of-stock .ct-swatch-content {
	opacity: 1;
}

.single-product .ct-swatch-container.ct-out-of-stock .ct-swatch {
	position: relative;
}

/* The unavailable state uses FIGMA'S OWN out-of-stock idiom, not an invented
   mark.

   An earlier revision of this fix drew a red diagonal cross here, copied from
   Blocksy's `[data-variations-out-of-stock=crossed]` geometry. That was wrong
   twice over: the site is configured `faded`, so the cross was never the
   theme's sanctioned treatment either, and nothing in the design specified a
   cross at all. It shipped on my judgement rather than on a source.

   What the design actually has, read from the offline export
   (reference/figma-context-20260917, 22 instances, e.g.
   I23313:52709;14061:189797;34377:120323;25315:43410):

     fill        #FFFFFF at 80% opacity, a translucent scrim
     label       "OUT OF STOCK", Nominee 900, 16px, letter-spacing 0, UPPER
     label fill  #2E2A2A
     padding     10px all round, no corner radius

   Two adaptations, both recorded rather than silently made.

   TYPE SCALE. Figma applies this at 16px across a 287px-wide product image.
   These swatches are roughly 130px, so 16px does not fit; 10px keeps the
   twelve characters on one line inside the 10px padding. The ratio is not
   Figma's, because Figma never drew this on a swatch.

   CONTRAST. #2E2A2A behind an 80% white scrim composites to about #4A4744 on
   the cream card, comfortably past 4.5:1, so the label is legible by
   construction rather than by a measurement that could drift. That is the
   property the red cross never had: it was a symbol carrying the whole
   meaning, with no words behind it.

   The scrim covers the weight and dose lines while it is shown. That is the
   same trade Figma makes over a product image, and it is the open question
   for the designer: on a picker, the shopper arguably still needs to read
   WHICH option is unavailable. Flagged in the queue, not resolved here. */
.bpp-swatch-oos {
	display: none;
}

.single-product .ct-swatch-container.ct-out-of-stock {
	position: relative;
}

.single-product .ct-swatch-container.ct-out-of-stock .bpp-swatch-oos {
	/* BT7: no scrim over the band. The weight and dose stay fully legible and
	   the unavailable state is a pill on the card's bottom edge (the Best
	   value pill owns the top edge). White on --bpp-ink is 14.18:1. */
	display: block;
	position: absolute;
	left: 50%;
	bottom: -12px;
	transform: translateX(-50%);
	z-index: 1;
	padding: 3px 8px;
	border-radius: 8px;
	background: var(--bpp-ink);
	color: var(--bpp-white);
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 10px;
	line-height: 14px;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	white-space: nowrap;
	pointer-events: none;
}

.single-product .ct-variation-swatches:has(.ct-out-of-stock) {
	padding-bottom: 12px;
}

/* 34190:83455: the pill sits above the card, overlapping its top edge. Figma
   places it at y=-11.78 against a 24px-tall pill, so it straddles the border
   by about half its height; `top: -12px` reproduces that within a pixel.
   Figma fills it #FF0000, which is 4.0:1 against white and misses the 4.5:1
   floor at this size, so it uses --bpp-red-text (#C6362B, white text on it
   measures 5.30:1), the same substitution already made for the subscription
   savings pill.

   Confirmed as a standing decision, not a candidate to revert to a Figma
   literal (86eypb6me, 2026-09-17): WCAG 2.2 AA is contractual on this
   project across five languages and outranks colour parity here. This is a
   defect in the Figma source, flagged to the designer separately, not a
   bug in this implementation. */
.single-product .bpp-swatch-badge {
	position: absolute;
	top: -12px;
	left: 50%;
	transform: translateX(-50%);
	white-space: nowrap;
	padding: 4px 8px;
	border-radius: 8px;
	background: var(--bpp-red-text);
	color: var(--bpp-white); /* white on --bpp-red-text, 5.3:1 */
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: 16px;
	text-transform: uppercase;
}

/* The row holds four cards on Biotics, which will not fit a phone. */
@media (max-width: 689px) {
	.single-product .ct-variation-swatches {
		flex-wrap: wrap;
	}

	.single-product .ct-swatch-container {
		flex: 1 1 calc(50% - 5px);
	}
}

/* Allergen banner and feature-card grid, Figma 34773:75355 (the teaser group
 * every variant of the "short description" set opens with).
 *
 * Config investigated and logged before this was written: Blocksy's
 * has_product_features / product_badges / has_product_custom_badge theme mods
 * are all false and none produces an icon grid in the summary; the store has
 * one global attribute (pa_box-size) which drives price and carries no icon;
 * and no product meta holds anything of this shape. See product-features.php. */
.bpp-features {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

/* 34773:75356 */
.bpp-features__allergen {
	display: flex;
	flex-direction: column;
	gap: 4px;
	padding: 8px 12px;
	border-radius: 12px;
	background: var(--bpp-mint);
}

/* 34773:75357 */
.bpp-features__allergen-head {
	display: flex;
	align-items: center;
	gap: 6px;
	margin: 0;
}

/* 34773:75358: a hug-width disc with 4.5px padding around a 13.5px glyph. */
.bpp-features__allergen-icon {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 4.5px;
	border-radius: 18px;
	background: var(--bpp-green);
	color: var(--bpp-white); /* decorative glyph; the banner text carries the meaning */
}

.bpp-features__allergen-icon svg {
	width: 13.5px;
	height: 13.5px;
	display: block;
}

.bpp-features__allergen-lead {
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 16px;
	line-height: 20px;
	color: var(--bpp-ink); /* 12.9:1 on the mint banner */
}

.bpp-features__allergen-body {
	margin: 0;
	font-family: var(--bpp-body-font);
	font-weight: 400;
	font-size: 14px;
	line-height: 16px;
	color: var(--bpp-ink); /* 12.9:1 on the mint banner */
}

/* 34773:75366 and siblings: rows of two, 8px apart. A grid rather than three
   hardcoded rows, so a product with an odd number of cards still lays out. */
.bpp-features__cards {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 8px;
}

/* EL-3f029bde */
.bpp-features__card {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 8px 12px;
	border-radius: 8px;
	background: var(--bpp-white);
	margin: 0;
}

/* EL-749d10cc: a fixed 24x24 disc holding a 16px glyph. */
.bpp-features__card-icon {
	flex: 0 0 auto;
	width: 24px;
	height: 24px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: 32px;
	background: var(--bpp-amber);
	color: var(--bpp-ink); /* decorative glyph; the label carries the meaning */
}

.bpp-features__card-icon svg {
	width: 16px;
	height: 16px;
	display: block;
}

.bpp-features__card-text {
	display: flex;
	flex-direction: column;
	justify-content: center;
	min-width: 0;
}

.bpp-features__card-label {
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: 16px;
	color: var(--bpp-ink); /* 13.72:1 on white */
}

.bpp-features__card-value {
	font-family: var(--bpp-body-font);
	font-weight: 400;
	font-size: 14px;
	line-height: 16px;
	color: var(--bpp-ink); /* 13.72:1 on white */
}

/* Below Blocksy's own mobile switch the summary column is narrow enough that
   two cards per row would wrap every label, so they stack. */
@media (max-width: 689px) {
	.bpp-features__cards {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* Short description, Figma component set 34476:106876, four variants:
 * closed/open x superfood/biotics.
 *
 * Figma writes the open body as separate text layers: a green uppercase lead,
 * the prose, then a green uppercase brand sign-off. The real page cannot do
 * that, because the copy is $product->get_short_description(), client-editable,
 * and its markup on the reference products is <p><big>lead</big></p> then <p>
 * and <ul><li> (read off the deployed Biotics page before these were written).
 * So the roles map onto the elements the excerpt actually uses, rather than
 * injecting Figma's words, which would freeze allergen and nutritional claims
 * into code. */
.bpp-short-description {
	display: flex;
	flex-direction: column;
	gap: 12px;
	font-family: var(--bpp-body-font);
	font-weight: 400;
	font-size: 16px;
	line-height: 20px;
	color: var(--bpp-ink);
}

.bpp-description-body {
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.bpp-description-body > * {
	margin: 0;
}

/* Figma's lead line (34476:106869 superfood, 34922:68397 biotics): Nominee
   Heavy Extended 900, 14px/20px, uppercase, green. The excerpts mark that line
   up as <big>, which is the only structural signal available. */
.bpp-description-body big {
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 14px;
	line-height: 20px;
	text-transform: uppercase;
	color: var(--bpp-green); /* 5.61:1 on white */
}

/* Figma's prose (34476:106870 and siblings): Kobe1.1 Regular 16px/20px, ink. */
.bpp-description-body li {
	font-family: var(--bpp-body-font);
	font-weight: 400;
	font-size: 16px;
	line-height: 20px;
	color: var(--bpp-ink); /* 13.72:1 on white */
}

.bpp-description-body ul {
	display: flex;
	flex-direction: column;
	gap: 8px;
	margin: 0;
	padding-left: 20px;
}

/* Figma specs Kobe1.1 at 16px here. This was rendering Nominee at 14px,
   because it used the heading-font token rather than the body-font one. */
.bpp-short-description .bpp-read-more-toggle {
	/* Figma 34476:106695 puts this hard against the left edge, its own line,
	   full width, left aligned. The <button> was picking up a centred
	   text-align from the summary column and sitting in the middle. */
	display: block;
	align-self: flex-start;
	width: 100%;
	text-align: left;
	margin-top: 4px;
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 16px;
	line-height: 20px;
	color: var(--bpp-green);
	background: none;
	border: none;
	padding: 0;
	cursor: pointer;
	/* No underline at rest, because Figma draws none in any of the four
	   variants. The WCAG 1.4.1 rule that puts underlines on the prose links
	   below is about a green word inside a sentence, where colour is the only
	   thing separating it from the text around it. This is a standalone
	   control on its own line under the copy it opens, so it does not rely on
	   colour to be recognisable. The underline comes back on hover and focus,
	   alongside the focus ring. */
	text-decoration: none;
}

.bpp-short-description .bpp-read-more-toggle:hover,
.bpp-short-description .bpp-read-more-toggle:focus-visible {
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 3px;
}

/* WCAG 1.4.1, inline links in body copy must not rely on colour alone.
 * Primary Green #247B4E measures 1.81:1 against Ink body text, below the 3:1
 * that non-text contrast requires when colour is the only thing separating a
 * link from the sentence around it, so a green word reads identically to the
 * rest of the prose. The underline closes that gap. Thickness, offset and
 * colour are all set explicitly rather than left to browser defaults, per the
 * R5.1 Section 7 spec, matching the Article detail template (86eyt9pb9).
 *
 * Scope is deliberately narrow: prose only. Buy-panel CTAs, nav, breadcrumbs
 * and cards are already distinguishable as UI elements regardless of colour
 * and are left alone. So is the read-more toggle above: it is a standalone
 * control on its own line, not a green word inside a sentence, and Figma
 * draws it without an underline. It picks one up on hover and focus.
 *
 * Every selector below was confirmed present in the rendered page before
 * being written, to avoid adding more of the dead CSS this task exists to
 * remove. Counts on the Biotics and Boost product pages respectively:
 * .bpp-description-body 0/1 (renders only when a product has a short
 * description, which most do not), .woocommerce-product-details__short-
 * description 0/1, .bppm-accordion__evidence-link 4/4 (the real "for the
 * evidence" article links), .bppm-why__copy 1/1, .bppm-how__body 2/2,
 * .bppm-reason-card__body 6/6. A .bpp-spec-sheet selector was drafted here
 * first and dropped: it matched nothing anywhere on the site. */
/* stylelint-disable no-descending-specificity --
   The only higher-specificity `a` rule above these is
   `.single-product .ct-breadcrumbs a:hover`, which targets the breadcrumb
   trail. A breadcrumb link is never also description or marketing prose, so
   these rules can never collide in the cascade and reordering them would
   only separate this block from the prose styles it belongs with. */
.bpp-description-body a,
.woocommerce-product-details__short-description a,
.bppm-why__copy a,
.bppm-how__body a,
.bppm-reason-card__body a,
.single-product .wp-block-paragraph a {
	color: var(--bpp-green);
	text-decoration: underline;
	text-decoration-color: var(--bpp-green);
	text-decoration-thickness: 1px;
	text-underline-offset: 3px;
}

.bpp-description-body a:hover,
.bpp-description-body a:focus-visible,
.woocommerce-product-details__short-description a:hover,
.woocommerce-product-details__short-description a:focus-visible,
.bppm-why__copy a:hover,
.bppm-why__copy a:focus-visible,
.bppm-how__body a:hover,
.bppm-how__body a:focus-visible,
.bppm-reason-card__body a:hover,
.bppm-reason-card__body a:focus-visible {
	text-decoration-thickness: 2px;
}
/* stylelint-enable no-descending-specificity */

/* Closed hides the prose outright. It is not a clamp.
 *
 * All four variants of the short-description set (34476:106876) were rendered
 * together to settle this. Property 1=closed shows the allergen banner, the
 * feature cards and the toggle, with no prose on screen at any point.
 * Property 1=open adds the whole block (34476:106868) between the cards and
 * the toggle. The three-line clamp that used to live here left a teaser
 * paragraph visible that the design never shows, on both the superfood and
 * the biotics variant. */
.bpp-short-description.is-collapsed .bpp-description-body {
	display: none;
}

.bpp-faq-teaser {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding: 16px 0;
}

/* 86eypb6me, buy-box queue: custom.php now hooks bonza_pdp_faq_teaser() (and,
 * Superfoods only, bonza_pdp_daily_portion_panel(), .bpp-portion below) onto
 * woocommerce_before_add_to_cart_button, inside the add-to-cart form, right
 * after the pack-size cards and before .bpm -- matching Figma's own order
 * instead of rendering after the whole form via woocommerce_single_product_summary.
 *
 * Their old spacing came entirely from being a direct child of
 * .entry-summary-items, which resolves --product-element-spacing to 24px
 * per child (see that rule, above in this file) -- neither element carries
 * a margin of its own. That parent no longer reaches this deeper position
 * (form.cart > ... > .woocommerce-variation-add-to-cart, three levels below
 * .entry-summary), so nothing paces them here without an explicit rule, and
 * a bare move would have left them flush against whatever precedes them.
 * 20px matches the group rhythm this file already uses everywhere else
 * inside the add-to-cart area (.bpm's own margin below, the trust-row /
 * payment-row pair), not the unreachable 24px, so the two stay consistent
 * with their new neighbours rather than carrying over a number that applied
 * to a different parent. */
.bpp-faq-teaser__question {
	margin-top: 20px;
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 16px;
	line-height: 20px;
	color: var(--bpp-green);
}

/* Figma draws this prompt uppercase with letter-spacing; both were missing. */
.bpp-faq-teaser__prompt {
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--bpp-ink);
}

/* Button, Kind=Secondary, Size=Large (component 29384:40260 Default /
   29384:40246 Hover / 29384:40274 Disabled). */
.bpp-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	padding: 16px;
	border-radius: 999px;
	background: var(--bpp-green);
	color: var(--bpp-white);
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 14px;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	text-align: center;
	text-decoration: none;
	border: none;
	cursor: pointer;
	box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.03), 0px 12px 16px -4px rgba(0, 0, 0, 0.08);
}

.bpp-btn:hover,
.bpp-btn:focus-visible {
	background: var(--bpp-yellow);
	color: var(--bpp-black);
}

.bpp-btn:focus-visible {
	outline: 2px solid var(--bpp-ink);
	outline-offset: 2px;
}

.bpp-btn:disabled,
.bpp-btn[aria-disabled="true"] {
	opacity: 0.6;
	cursor: not-allowed;
}

/* The buy-box column rhythm, from Blocksy's own variable.
 *
 * Figma's summary column 34190:83334 is a 24px-gap stack. Blocksy spaces every
 * child of .entry-summary-items with `margin-bottom: var(--product-element-
 * spacing, 10px)`, and on this site that resolves to 35px, so every element in
 * the buy box sat 11px further apart than drawn. That is the section spacing
 * that kept being reported, and no per-element margin would have found it:
 * the gap is applied by the parent to every child at once.
 *
 * CONFIG FIRST, and the native home does not fit. The spacing is a per-layer
 * `spacing` field inside the `product_single_layout` Customizer option
 * (inc/dynamic-styles/global/woocommerce/single-product-layers.php line 102),
 * defaulting to 35. That option is unset here, so Blocksy is running built-in
 * defaults, and setting one layer's spacing means writing the entire default
 * layer list into the option and freezing it against future Blocksy releases,
 * the same trade-off already refused for the excerpt layer. Blocksy publishes
 * no filter for it. So the variable it reads is rebound instead, which keeps
 * their layer list tracking the theme.
 *
 * Children Figma nests inside their own sub-frames carry tighter gaps of their
 * own (the title block 34190:83335 is 4px, the short description 12px); those
 * are set on the elements themselves elsewhere in this file. 24px is the
 * top-level rhythm only. */
.single-product .entry-summary-items {
	/* Blocksy's own per-child spacing, neutralised below in favour of one gap.
	   Left set to Figma's group rhythm so anything that still reads the
	   variable lands on the right number rather than on the 35px default. */
	--product-element-spacing: 24px;

}

/* The elements that open a new Figma group in 34190:83334, whose top-level
 * gap is 24px. 4px of that already comes from the column, so each adds 20px.
 * Listed explicitly rather than matched by position, because the buy box
 * gains and loses children by product type (the frequency panel, the stock
 * row, the notify form), and nth-child would silently retarget. */
.single-product div.summary.entry-summary > .bpp-features,
.single-product div.summary.entry-summary > .ct-product-divider,
.single-product div.summary.entry-summary > .bppm-info-accordion,
.single-product div.summary.entry-summary > .bpp-subscription-banner,
.single-product div.summary.entry-summary > .bpm {
	margin-top: 20px;
}

/* An empty element still eats a flex gap. WooCommerce Subscriptions prints
 * `<p class="first-payment-date"><small></small></p>` on every product and
 * leaves it empty on the ones with no subscription, so it measured 0px tall
 * and was invisible, while still sitting between the payment row and the
 * accordion and taking a gap on BOTH sides: 4 + 4 + the 20px group margin
 * came out at 28px where the design says 24. Filtering zero-height nodes out
 * of a gap audit hides this, so it is worth stating plainly: an empty node is
 * not a free node in a gapped container. */
.single-product div.summary.entry-summary > p.first-payment-date:empty,
.single-product div.summary.entry-summary > p.first-payment-date:has(> small:empty) {
	display: none;
}

/* Figma's add-to-cart group 34190:83514 is a 20px stack holding the button
   row, the checkout trust row and the payment icons, so each closes up from
   the 24px group rhythm to 20. Corrected 2026-09-17: this had been 16px,
   which the comment above it never matched either -- a copy-paste value
   from a different closing distance that nobody had measured against this
   one. */
.single-product div.summary.entry-summary > .bpp-checkout-trust-row,
.single-product div.summary.entry-summary > .bpp-payment-row {
	/* 86eycc4d1: the summary is a flex column with a 4px row-gap, so a 16px
	   margin gives the 20px Figma stack. 20px here measured 24px on v1. */
	margin-top: 16px;
}

/* Inside the short description group, Figma 34476:106879 uses 12px between the
   feature cards and the prose, so that pair closes back up from 24 to 12. */
.single-product div.summary.entry-summary > .bpp-short-description {
	margin-top: 8px;
}

/* Price fires just before Add to Cart now (custom.php reorders the default
   hook) to match Figma's order. Markup/behavior stays fully WooCommerce
   native (variations, sale price, subscription price strings all keep
   working); this only restyles it.
 *
 * margin: 0 here is harmless but was not the thing controlling this element's
 * spacing, and the earlier note claiming a 16px flex-gap rhythm was wrong.
 * Measured on the deployed page, the summary column's spacing comes from
 * Blocksy: `.product:has([data-gallery]) .entry-summary-items > *` sets
 * margin-bottom to `var(--product-element-spacing, 10px)` in
 * bundle/woocommerce.min.css, which resolves to 35px. See the variable block
 * below for where that is corrected to Figma's number. */
.single-product p.price {
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 28px;
	color: var(--bpp-ink);
	margin: 0;
}

.single-product p.price del {
	color: var(--bpp-ink-muted);
	opacity: 0.7;
}

.single-product p.price ins {
	text-decoration: none;
	color: var(--bpp-green);
}

/* No rule here for `p.price .screen-reader-text`, deliberately, and this note
 * exists so the next session does not re-add one.
 *
 * WooCommerce core prints an accessible price-range announcement on a
 * variable product before a variation is chosen:
 *   <p class="price">
 *     <span aria-hidden="true">...22.50</span> <span aria-hidden="true">-</span>
 *     <span aria-hidden="true">...77.00</span>
 *     <span class="screen-reader-text">Price range: 22.50 through 77.00</span>
 *   </p>
 * The visible amounts carry aria-hidden so a screen reader hears one composed
 * sentence rather than two bare numbers with no relation between them.
 *
 * It was reported as rendering visibly. It is not. Measured on the deployed
 * page, that span is already 1x1px, position absolute, clip-path inset(50%),
 * overflow hidden, exactly as intended. The report came from reading the
 * paragraph's textContent, which concatenates hidden text and so returns
 * "£22.50 - £77.00Price range: £22.50 through £77.00" for markup that paints
 * correctly. Use getBoundingClientRect and computed styles to decide whether
 * something is visible; textContent cannot tell you.
 *
 * A defensive !important copy of the clip technique was written here and then
 * removed: it fixed nothing, and this task exists partly to delete CSS that
 * targets problems the page does not have. */

/* Add to cart form: WooCommerce/subscriptions/bundles native markup,
   restyled only, same Button/Secondary/Large states as .bpp-btn above. */
/* Quantity stepper. Figma specs the 999px pill and the #FFA825 stroke on the
   WHOLE control, not the bare number input. Only the input was styled, so the
   wrapper rendered unstyled around a separately-bordered box. The border and
   radius move to the wrapper; the input keeps only its own type and sizing.
   Live markup confirmed: <div class="quantity" data-type="type-2">.

   Config investigated: Blocksy exposes quantity_type (the layout variant,
   already type-2 here), quantity_arrows, and quantity_color with default and
   hover states (inc/options/woocommerce/single-product-elements.php:287).
   quantity_color is the native home for the stroke colour and should be set
   in the Customizer alongside the breadcrumb values; the exact CSS variable
   it emits was not traced, so the #FFA825 border is kept here for now and
   should be removed once the Customizer value is confirmed to cover it. No
   native option exists for the 999px radius, so that stays in CSS either
   way. */
/* No overflow:hidden here on purpose. Blocksy's type-2 quantity control puts
   its own stepper arrows inside this wrapper as .ct-increase / .ct-decrease
   spans (confirmed in the live markup), and a 999px radius curves in far
   enough at a 48px height to clip them. The inner input carries no border or
   background of its own, so there is nothing left to clip anyway. */
/* The row itself. Figma 34190:85561 is a 16px-gap row of three controls that
   stretch to a common height: the quantity pill, Add to Cart, and the wishlist
   heart. Blocksy already wraps the first two in .ct-cart-actions, so that is
   the row; product-page.js moves the wishlist button in as its third child. */
.single-product form.cart .ct-cart-actions {
	/* Blocksy sizes every button off --theme-button-min-height, which is 55px
	   here and is what actually made this row 55px tall against Figma's 48px.
	   The variable is Blocksy's own, so it is rebound for this row alone
	   rather than moving the Customizer value and resizing every button on
	   the site. Both buttons still carry their own explicit 48px. */
	--theme-button-min-height: 48px;

	display: flex;
	align-items: stretch;
	gap: 16px;

	/* The wishlist button was completely off-screen at 320 and 375, not
	   merely clipped. Measured on the deployed page at 320: the row is 280px
	   wide with scrollWidth 365, quantity 132 (20 to 152), Add to Cart 149
	   (168 to 317, already past the row's own right edge at 300), and the
	   wishlist 52px sitting at left 333 in a 320px viewport. A shopper on a
	   phone could not reach the control at all.

	   It survived every earlier check because the page reports NO horizontal
	   overflow: an ancestor clips the row, so `document.documentElement
	   .scrollWidth` stays equal to the viewport. The defect is only visible
	   by measuring this row's own scrollWidth against its width, or by
	   looking at a screenshot, which is how it was actually found.

	   Root cause is the flex default, not a rule: nothing anywhere sets
	   flex-wrap on this element (confirmed by walking document.styleSheets
	   and testing el.matches() against every flex-wrap rule on the page, zero
	   hits), so it was `nowrap` by initial value and the fixed 132 + 52 plus
	   two 16px gaps left 68px for a CTA whose min-content is about 149.

	   Wrapping engages only when the row cannot fit, so wide layouts are
	   untouched: at 1440 all three still sit on one line. At 320 they stack
	   in DOM order, quantity then Add to Cart then wishlist, verified live
	   with all three on screen and scrollWidth back to 280.

	   DOM order is deliberately preserved. Reordering so quantity and the
	   wishlist share a line would look tidier, and would put the visual
	   order out of step with the tab order, which is a WCAG 1.3.2 problem
	   worse than the extra height. Figma draws one row (34190:85561); the
	   stack is a narrow-width concession, and whether the design prefers a
	   reorder is a question for the designer.

	   `!important` is required and is not decoration. The shared parent theme
	   blaze-blocksy ships `.ct-product-add-to-cart .ct-cart-actions {
	   flex-wrap: nowrap !important }` in assets/css/components/woo-single.css,
	   named by walking document.styleSheets on the deployed page. An
	   important declaration beats a non-important one whatever the
	   specificity, so the first version of this fix shipped, deployed, and
	   changed nothing: the rule was in the file, `hasMyRule` was true, and
	   the computed value was still nowrap.

	   The tell was there earlier and I missed it. Setting the property inline
	   WITHOUT the important flag also did nothing, while
	   `setProperty('flex-wrap','wrap','important')` worked immediately. An
	   inline style losing is only possible against `!important`. With the
	   flag this rule also out-specifies theirs (0,3,0 against 0,2,0), so it
	   wins on both counts.

	   The wrap is SCOPED to narrow widths, and that scoping is load-bearing.
	   An unscoped `flex-wrap: wrap` was shipped first and measured at 1440:
	   the row went to 176px tall with all three controls on separate lines.
	   Flex line-breaking uses each item's HYPOTHETICAL main size, which for
	   `flex: 1 1 auto` is its content width, so the 636px-wide Add to Cart
	   forced a break at every width even though the row fits comfortably
	   once it has shrunk. `nowrap` had been hiding that by making the CTA
	   shrink instead of wrapping.

	   424px is where it changes: the row needs 365px, and the summary column
	   is 280 at 320 and about 335 at 375 (both too narrow) but roughly 385 at
	   425 (enough). Verified at all seven breakpoints after this change. */
}

@media (max-width: 424px) {
	.single-product form.cart .ct-cart-actions {
		flex-wrap: wrap !important;
	}
}

/* Figma 34190:85562 gives the group a FIXED 132px and three stretched
   segments. Read off the node, with the arithmetic behind each number:
 *
 *   minus  34190:85563  padding 12, icon 16, padding 12  = 40px, hug
 *   value  34190:85564  fills the remainder              = 50px
 *   plus   34190:85565  same as minus                    = 40px
 *                                                  40 + 50 + 40 = 130
 *                                       + the 1px amber stroke each side = 132
 *
 * Height is 48px to match the row, leaving the segments 46px inside it. Left
 * to hug, the control measured 108.1px on the deployed page.
 *
 * The node also carries a rgba(0, 0, 0, 0.12) stroke on each segment, and the
 * rendered PNG shows none of it: every one of those edges sits under the amber
 * stroke or is clipped by the 999px radius. So the hairlines stay off. Taking
 * the stroke values on their own would have drawn three dividers the design
 * does not have.
 *
 * Segments instantiate the Button set 18:2908, which turns out to hold only
 * two variants, Icon Only=True and Icon Only=False, both Style=Plain and
 * State=Default. It draws no hover or disabled state at all, so those keep
 * Blocksy's own stepper styling, which this file leaves alone.
 *
 * All three sizes go through the custom properties blaze-blocksy's shared
 * qty-stepper.css exposes for exactly this. That stylesheet already lays
 * type-2 out as a flat [ - | n | + ] row with order 0/1/2 and carries
 * !important on the width, height and border, so setting its knobs is the
 * only thing that reaches those declarations. The three other sites loading
 * it keep their own defaults. */
.single-product form.cart .quantity {
	--bc-qty-btn-width: 40px;
	--bc-qty-input-width: 50px;
	--bc-qty-btn-height: 46px;
	--bc-qty-radius: 0;
	--bc-qty-border: transparent;
	/* Blocksy paints `div.quantity .ct-decrease:hover { background:
	   var(--quantity-hover-color, var(--theme-button-background-hover-color)) }`
	   (bundle/woocommerce.min.css), which on this site is the blue button hover,
	   measured rgb(21, 89, 237) behind the minus on hover. Figma's stepper has
	   no hover fill, so the variable Blocksy itself exposes is set here. The
	   Customizer knob for it is WooCommerce > Single Product > quantity colour
	   (hover), left at its default so the rule travels with the theme. */
	--quantity-hover-color: transparent;

	display: inline-flex;
	align-items: stretch;
	/* flex-basis rather than width, because qty-stepper.css carries
	   `width: auto !important` on this wrapper. Its `height: auto !important`
	   is the same story, so the 48px comes from the row's align-items:
	   stretch, which is also what Figma asks for (the group is vertical fill
	   beside two fixed-48 buttons). */
	flex: 0 0 132px;
	border: 1px solid var(--bpp-amber);
	border-radius: 999px;
	background: var(--bpp-white);
}

/* Blocksy prints a "Qty:" caption inside the control. Figma draws the three
   segments and nothing else, so it goes. The control keeps its accessible
   name from the screen-reader label WooCommerce renders beside the input. */
.single-product form.cart .quantity .bc-qty-label {
	display: none;
}

/* Minus and plus need spelling out here, because the knobs above reach the
   input but lose the two buttons.
 *
 * Measured, not guessed: the segments came back 30.5469px square. That is
 * Blocksy's `calc(var(--quantity-height, 55px) / 1.8)` in
 * bundle/woocommerce.min.css, which ties the whole control to a 55px height
 * and matches qty-stepper.css selector for selector at 0,0,2,2. Blocksy's
 * bundle is enqueued after this stylesheet, so source order hands it the win.
 * These selectors add .single-product form.cart and take it back on
 * specificity, which holds whatever the load order does next.
 *
 * Nudging --quantity-height would have been the tidier hook, but it drives
 * width and height off one divisor, so a 48px control would give 26.67px
 * segments rather than Figma's 40. */
.single-product form.cart .quantity .ct-decrease,
.single-product form.cart .quantity .ct-increase {
	width: 40px;
	height: 46px;
	border-radius: 0;
	/* Blocksy leaves the glyphs on its own slate --theme-text-color. Figma's
	   minus and plus carry no fill of their own, so they take the ink the
	   rest of the control uses. */
	color: var(--bpp-ink);
}

/* Blocksy draws the glyphs from its `woo` icon font at
   `calc(var(--quantity-height, 55px) / 5.5)`, which is 10px. Figma's minus
   and plus are 16x16, and the 2px nudge Blocksy adds goes with it. */
.single-product form.cart .quantity .ct-decrease::before,
.single-product form.cart .quantity .ct-increase::before {
	margin-top: 0;
	font-size: 16px;
}

/* The input must not carry a second border inside the amber pill.
 *
 * Took two wrong attempts, both caught by measuring the deployed page rather
 * than reading the diff. `border: none` lost, and so did zeroing Blocksy's
 * `--theme-form-field-border-width`. Neither is the mechanism here. The real
 * source is our own shared stylesheet: qty-stepper.css sets
 * `.quantity[data-type="type-2"] .input-text.qty { border: 1px solid
 * var(--bc-qty-border, var(--theme-border-color, #c0c0c0)) !important }`, and
 * no amount of specificity beats !important.
 *
 * That rule exposes --bc-qty-border as its own extension point, and the
 * wrapper above now sets it to transparent for all three segments at once.
 * The 1px stays so the inner metrics hold; only the colour goes. No
 * !important is added here, and the shared stylesheet is left untouched so
 * the three other sites that load it are unaffected. */
.single-product form.cart .quantity input.qty {
	border-radius: 0;
	background: transparent;
	/* Figma 34190:85562 sets the value in Nominee REGULAR 400 18/24, not the
	   Heavy Extended the rest of the buy box uses. line-height is stated in px
	   because the theme's own 1.65 would make this 30px, not 24px. */
	font-family: var(--bpp-heading-font);
	font-weight: 400;
	font-size: 18px;
	line-height: 24px;
	letter-spacing: 0.0056em;
	color: var(--bpp-ink);
	text-align: center;
}

/* Focus ring belongs on the control the user perceives, which is now the
   wrapper, so the visible focus state survives the border move. */
.single-product form.cart .quantity:focus-within {
	border-color: var(--bpp-green);
	outline: 2px solid var(--bpp-green);
	outline-offset: 2px;
}

.single-product form.cart .quantity input.qty:focus-visible {
	outline: none;
}

/* Add to Cart is Figma's PRIMARY button (34190:85566), which is the YELLOW one
   with the #B86E00 stroke. The green button belongs to the CTAs further down
   the page. It rendered green at 55px tall live, so the fill, stroke, height
   and label type below are all corrections measured against the deployed
   page. Ink on #FFC125 measures 8.72:1, well past the 4.5:1 floor. */
.single-product form.cart button.single_add_to_cart_button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	flex: 1 1 auto;
	height: 48px;
	padding: 16px;
	border-radius: 999px;
	background: var(--bpp-yellow);
	color: var(--bpp-ink);
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 12px;
	/* 26px is Figma's own line box. The theme's 1.65 would give 19.8px and the
	   button would measure short of 48px once padding is added. */
	line-height: 26px;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	border: 1.5px solid var(--bpp-yellow-stroke);
	box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.03), 0px 12px 16px -4px rgba(0, 0, 0, 0.08);
	/*
	 * FIXED 2026-09-24 (86eypb6me q18-buybox S31): the subscription
	 * label ("Subscribe Now", WooCommerce Subscriptions' own default
	 * text, longer than the plain "Add to Cart" this file's own
	 * comments were written against) was wrapping onto two lines on
	 * mobile "at first load" -- the FOUT window before the Nominee Heavy
	 * Extended webfont swaps in, when the browser's fallback font for
	 * this bold/uppercase/tracked label is measurably wider, can outgrow
	 * this button's `flex:1 1 auto` share of the row (fixed 132px qty
	 * pill + wishlist button on the same line) at narrow viewports and
	 * wrap before the swap narrows it back to one line. Figma draws this
	 * button as a single line at every breakpoint (34190:85566, "fill
	 * width" only in the horizontal axis, height fixed 48px) with no
	 * two-line variant, so nowrap matches the design regardless of which
	 * font is briefly active, rather than depending on font-swap timing
	 * to self-correct. */
	white-space: nowrap;
}

/* The bag glyph product-page.js appends. Figma draws it at exactly 18x18. */
.single-product form.cart button.single_add_to_cart_button .bpp-cart-bag {
	display: inline-flex;
	flex: 0 0 auto;
}

.single-product form.cart button.single_add_to_cart_button .bpp-cart-bag svg {
	width: 18px;
	height: 18px;
	display: block;
}

/* State=Hover, Kind=Primary (29049:58918) swaps the yellow for a dark green
   and drops the stroke. That is a bigger change than a darkened fill, and
   worth reading off the set rather than inventing. The label turns white,
   which measures 9.38:1 on that green. */
.single-product form.cart button.single_add_to_cart_button:hover,
.single-product form.cart button.single_add_to_cart_button:focus-visible {
	background: var(--bpp-green-dark);
	border-color: transparent;
	color: var(--bpp-white);
}

/* The set draws no focus variant, so the ring is this theme's own. A keyboard
   user needs one whatever the mock omits. */
.single-product form.cart button.single_add_to_cart_button:focus-visible {
	outline: 2px solid var(--bpp-ink);
	outline-offset: 2px;
}

/* State=Disabled, Kind=Primary (29049:58913): cream fill, a coral 1px stroke,
   the YELLOW ink label (not the default black), and 0.6 opacity. WooCommerce
   disables this button until a variation is chosen, so the state is
   reachable on every variable product here.
 *
 * Corrected 2026-09-17: `color` was missing from this rule entirely, so the
 * disabled state was inheriting the Default variant's #2E2A2A ink label
 * instead of Figma's own #FFC125 for this state. Re-read against the full
 * component-set state matrix (86eypb6me Task C), not just the Default
 * variant this rule started from. */
.single-product form.cart button.single_add_to_cart_button:disabled {
	background: var(--bpp-cream);
	border-color: var(--bpp-coral);
	border-width: 1px;
	color: var(--bpp-yellow);
	opacity: 0.6;
	cursor: not-allowed;
}

/* Wishlist heart, Figma instance 34190:85567 (componentId 29049:58916,
 * Kind=Outline, Size=Large). HUG width, 48px tall, 12px/16px padding, 999px
 * radius, an 18x18 heart and no label.
 *
 * Colours come from the INSTANCE, not the base component. Read directly off
 * the instance node (86eypb6me Task C, 2026-09-17): fills=["#FFFFFF"],
 * strokes=["#FFA825"] -- both override the base Default variant's own
 * NO-FILL / #247B4E green stroke. Confirmed rule for this project: a
 * component set supplies the STRUCTURE each state changes, an instance's
 * own colour overrides supply the actual colours and survive every state
 * switch. So this button is white-fill/amber-stroke in every state, not
 * green, regardless of what the bare component set draws.
 *
 * The heart itself carries fills=[] in Figma, so it inherits currentColor
 * from the button's own label colour, which the instance never overrides:
 * that stays the component's own #247B4E green. White fill, amber border,
 * green heart -- an intentionally mixed instance, not an inconsistency to
 * "fix" toward one colour.
 *
 * componentProperties lists "Label": true, but this instance has no text
 * child, only the heart -- it renders icon-only regardless of that flag.
 * Do not add a text label to match it.
 *
 * This is Blocksy Companion Pro's own .ct-wishlist-button-single, relocated
 * into the row by product-page.js. Moving the live node keeps its click
 * handling, data-button-state and panel wiring intact. It normally sits inside
 * .ct-product-additional-actions, which this stylesheet hides, so the move is
 * also what makes it visible. */
.single-product form.cart .ct-cart-actions .ct-wishlist-button-single {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: 0 0 auto;
	/* Figma hugs this one: 16 + an 18px heart + 16 = 50px. Blocksy squares the
	   button off --theme-button-min-height, so the width has to be released
	   explicitly or it tracks the 48px height instead. */
	width: auto;
	height: 48px;
	padding: 12px 16px;
	border: 1px solid var(--bpp-amber);
	border-radius: 999px;
	background: var(--bpp-white);
	color: var(--bpp-green);
}

/* The heart, sized to Figma's 18x18 and taking the button's own colour
   (--bpp-green, inherited via currentColor -- see the instance note above).
   Blocksy colours it with its own slate --theme-text-color by default, and
   the Figma node carries no fill of its own.
 *
 * `:not(.ct-button-loader)` is load-bearing. Blocksy puts a second svg, its
 * spinner, in the same .ct-icon-container and keeps it collapsed until a
 * request is in flight. Sizing every svg in the button pushed that spinner
 * out to 18px alongside the heart and took the control to 70px wide against
 * Figma's 50. Measuring the deployed page is what caught it. The loader
 * keeps Blocksy's own styling, since Figma draws no loading state for it. */
.single-product form.cart .ct-cart-actions .ct-wishlist-button-single svg:not(.ct-button-loader) {
	width: 18px;
	height: 18px;
	display: block;
	color: var(--bpp-green);
}

/* Hover, read off State=Hover, Kind=Outline, Size=Large (29049:58920). The
 * base component's ONLY change on Hover is fill: transparent -> solid
 * white. This instance already pins fill to white in Default, so working
 * the state-vs-instance rule through: Default and Hover are VISUALLY
 * IDENTICAL for this control. Figma draws no hover change at all here.
 *
 * That is a real finding, not something to invent a hover for -- flagged as
 * a design gap for the client: a clickable control with no hover feedback
 * is poor UX regardless of what the mock shows.
 *
 * Restated anyway, because the INHERITED hover was still wrong before this
 * fix: blaze-blocksy's components/woo-single.css sets
 * `border-color: var(--theme-button-background-initial-color, #333)` on
 * .ct-wishlist-button-single:hover, throwing the amber stroke to Blocksy's
 * unrelated button blue on pointer-over. This rule exists to cancel that,
 * not to draw a new state Figma never drew. */
.single-product form.cart .ct-cart-actions .ct-wishlist-button-single:hover {
	background: var(--bpp-white);
	border-color: var(--bpp-amber);
}

/* State=Disabled, Kind=Outline (29049:58914): the base component's own
   structural change is fill going to solid white, which this instance
   already carries in Default, so nothing moves there either. Stroke stays
   the instance's own amber (colour overrides survive every state switch,
   same rule as Hover above), opacity 0.6. Not confirmed reachable on the
   deployed page -- see WHAT I COULD NOT VERIFY -- but written from the same
   state matrix as Default/Hover rather than left unhandled. */
.single-product form.cart .ct-cart-actions .ct-wishlist-button-single:disabled,
.single-product form.cart .ct-cart-actions .ct-wishlist-button-single[aria-disabled="true"] {
	background: var(--bpp-white);
	border-color: var(--bpp-amber);
	opacity: 0.6;
	cursor: not-allowed;
}

/* The set draws no focus variant, so this ring is the theme's own decision,
   not Figma's -- and it is also the only affordance a keyboard user gets on
   a control whose hover is, by design, a no-op. WCAG 2.2 AA is contractual
   on this project, so a visible focus indicator stays regardless. */
.single-product form.cart .ct-cart-actions .ct-wishlist-button-single:focus-visible {
	outline: 2px solid var(--bpp-ink);
	outline-offset: 2px;
}

/* Checkout trust row, Figma 34190:83517, 3rd child of the add-to-cart group
 * 34190:83514. This is a different component from the existing
 * .bpp-trust-row above the title (bonza_pdp_trust_badge_row() in
 * custom.php, a single check-icon badge for "Nutritionist Formulated. Vet
 * Approved", its own node, its own position on the page, already wired to
 * that class name). Named .bpp-checkout-trust-row to keep the two apart.
 *
 * Row wraps (Figma sets wrap: true here) with two separate gap values:
 * row-gap 8px and column-gap 24px. A wrapped second line needs the tighter
 * 8px between lines; the three items on one line keep the wider 24px
 * between them.
 *
 * Markup this expects (not yet in custom.php, see PR/handoff notes):
 *   <div class="bpp-checkout-trust-row">
 *     <span class="bpp-checkout-trust-row__item">
 *       <img src="trust-secure-checkout.svg" width="18" height="18" alt="" />
 *       <span>Secure Checkout</span>
 *     </span>
 *     (Vet-Approved, Free Shipping over £39 follow the same pattern)
 *   </div>
 */
.bpp-checkout-trust-row {
	display: flex;
	align-self: stretch;
	justify-content: center;
	flex-wrap: wrap;
	row-gap: 8px;
	column-gap: 24px;
	/* 86eycc4d1: Figma 20px stack (button row 48 / trust 20 / payment 28, 20
	   between). The old 12px vertical padding sat on top of the 20px margin and
	   made the visible gaps 36px and 48px. */
	padding: 0;
}

.bpp-checkout-trust-row__item {
	/* 86eycc4cp: an inline flow, not a flex row. As a flex row a wrapped label
	   (the free-shipping line on 375-428px, and on any language whose copy is
	   longer) filled the whole item box, so the label stayed left-aligned with
	   the icon pinned to the far left and vertically centred on the block.
	   Inline, the icon sits in the first line, the wrapped lines centre as a
	   block, and the item still shrinks to its content when it fits. The item's
	   own font-size is 0 so the markup whitespace between icon and label adds no
	   gap; the 3px Figma gap is the icon's margin. */
	display: block;
	max-width: 100%;
	text-align: center;
	font-size: 0;
	line-height: 20px;
}

/* 86eycc4d1: Figma draws the trust item 20px tall. The shared PDP text rule
   (line-height 1.5 on 16px = 24px) made it 24px. */
.single-product .bpp-checkout-trust-row .bpp-checkout-trust-row__item span {
	line-height: 20px;
}

.bpp-checkout-trust-row__item img {
	width: 18px;
	height: 18px;
	display: inline-block;
	/* Centres the 18px icon on the 20px first line; measured on v1. */
	vertical-align: -4px;
	margin-inline-end: 3px;
}

/* Body/Medium: Kobe1.1 Bold 700, 16/20, no letter-spacing. Sentence case as
   drawn ("Vet-Approved"), left as-is here rather than uppercased. */
.bpp-checkout-trust-row__item span {
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 16px;
	line-height: 20px;
	color: var(--bpp-ink); /* the body-ink token, 12.30:1 on cream */
}

/* Payment-icon row, Figma 34203:81441, 4th child of the same group. Figma
 * itself draws this row with no wrap. At 375-428px, nine 41px marks plus
 * eight 4px gaps add up to 361px+, which overflows the column at those
 * widths. The sibling trust row directly above already wraps, so this row
 * wraps too, keeping every mark at its exact drawn size on every line.
 * Recorded as a deliberate choice for the lead to weigh in on; Figma's
 * mobile frame does not settle it either way (86eypb6me mobile note).
 * flex-wrap: wrap was already on this rule from an earlier session.
 *
 * gap corrected from 8px to Figma's 4px. align-items: center added; it was
 * absent before, harmless while every mark shares the same 28px height, but
 * the node specifies it, so it belongs here too. */
.bpp-payment-row {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: 4px;
	padding: 0; /* 86eycc4d1: see .bpp-checkout-trust-row */
}

/* Each mark is FIXED 41x28 (28-tall is the shared baseline; width/radius
   are the per-brand exceptions below). Sourced from bonza_footer_payment_
   icons() (custom.php, client-approved, already git-tracked under
   custom/assets/footer/payment-marks/). That existing source turned up
   once this task was underway; the near-duplicate exports this task was
   first handed got deleted, never committed.
 *
 * ONLY 7 of that function's 10 marks render on the PDP -- the SSH-traced
 * enabled-gateway set (86eypb6me), decided with the lead:
 *   RENDER:        Visa, Mastercard, Amex   (Stripe card, enabled; same
 *                                            "processor accepts major
 *                                            networks" basis as the footer)
 *                  Klarna                   (stripe_klarna enabled AND in
 *                                            upe accepted_payments)
 *                  PayPal                   (angelleye_ppcp enabled)
 *                  Apple Pay, Google Pay    (Stripe's own payment_request
 *                                            flag; see the PHP handoff
 *                                            notes for PayPal's own flag
 *                                            disagreeing)
 *   DO NOT RENDER: PayPal Credit -- no enabled gateway anywhere, and a
 *                  regulated UK consumer-credit product; no CSS rule for
 *                  it below, on purpose.
 *                  Discover -- in the footer set, but a client question for
 *                  a UK store, flagged rather than assumed on.
 * Markup this expects, one <span> per mark the PHP decides to render:
 *   <span class="bpp-payment-mark bpp-payment-mark--visa">
 *     <img src="[asset path]" width="41" height="28" alt="Visa" />
 *   </span> */
.bpp-payment-row .bpp-payment-mark {
	display: inline-flex;
	flex: 0 0 auto;
}

.bpp-payment-row .bpp-payment-mark img {
	display: block;
	width: 41px;
	height: 28px;
	border-radius: 4px;
}

/* GooglePay is 40 wide, 41 for every other mark. Figma node 34203:82387,
   confirmed with the lead this is the value drawn, on purpose. */
.bpp-payment-row .bpp-payment-mark--googlepay img {
	width: 40px;
}

/* Klarna draws with no radius at all, the one exception among the marks
   that render here (34370:198690). */
.bpp-payment-row .bpp-payment-mark--klarna img {
	border-radius: 0;
}

/* .bpp-subscription-banner / __heading / __body used to be styled here.
   Deleted 2026-09-17: the VidiVet banner moved to its own module,
   subscription-banner.css (loaded after this file, dependency
   ['bonza-product-page']), which fully owns these three selectors now with
   a divider, logo and inner wrapper this older ruleset never had. The
   group-rhythm rule that targets
   .single-product div.summary.entry-summary > .bpp-subscription-banner
   stays -- that is the page-level gap ABOVE the banner, outside the
   banner's own box, and the newer file deliberately leaves it alone. */

/* Gallery: restyles WooCommerce's own native gallery/zoom markup. No
   custom gallery JS is added; WooCommerce's bundled zoom/flexslider
   already covers the Figma magnifying-glass affordance. */
/* The frame itself is square (ClickUp 86eycc4cp, Vita): Figma puts the 16px
 * radius on the main image (…;34920:65844) and 8px on each thumbnail, never
 * on the column wrapper (…;34920:65843). A rounded frame clipped the last
 * visible thumbnail and the square edge of the selected one. */
.single-product .woocommerce-product-gallery {
	border-radius: 0;
	overflow: hidden;
}

.single-product .woocommerce-product-gallery .flex-control-thumbs li img {
	border-radius: 8px;
	border: 2px solid transparent;
}

.single-product .woocommerce-product-gallery .flex-control-thumbs li img[aria-current="true"],
.single-product .woocommerce-product-gallery .flex-control-thumbs li img.flex-active {
	border-color: var(--bpp-red-figma);
}

/* Sticky buy bar: own markup, printed on wp_footer (ClickUp 86eypb6me).
 *
 * flex-wrap (86eypb6me q33): added once the qty stepper and wishlist
 * button below joined the row. At 320/375/425 the actions row's own
 * content does not fit beside the product info on one line (measured
 * live 2026-09-23, see .bpp-sticky-bar__actions below for the numbers),
 * so this and .bpp-sticky-bar__product/__actions wrap onto their own
 * lines instead of a hardcoded breakpoint -- wrap is passive and only
 * engages where the content actually overflows, so nothing changes at
 * 768px and up, where it already fit on one line.
 */
.bpp-sticky-bar {
	position: fixed;
	left: 0;
	right: 0;
	bottom: var( --bpp-cookie-lift, 0px );
	z-index: 40;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding: 12px 20px;
	background: var(--bpp-white);
	box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.12);
	transform: translateY(100%);
	transition: transform 0.2s ease;
}

.bpp-sticky-bar.is-visible {
	transform: translateY(0);
}

/* Help Scout Beacon bubble (z8udzkebk5): it sat on the Subscribe Now / wishlist
   row and the sticky bar. Lift is set by product-page.js; the class hides the
   bubble only while the in-page buy row is behind it. */
.single-product .BeaconFabButtonFrame {
	bottom: calc( 20px + var( --bpp-beacon-lift, 0px ) ) !important;
	transition: opacity 0.15s ease;
}

.single-product.bpp-beacon-clear .BeaconFabButtonFrame {
	opacity: 0;
	pointer-events: none;
}

.bpp-sticky-bar__product {
	display: flex;
	align-items: center;
	gap: 12px;
	min-width: 0;
	/* 86eypb6me q33: lets this and .bpp-sticky-bar__actions each take the
	   flex-wrap:wrap row's full width when they wrap onto their own line
	   (see .bpp-sticky-bar above), rather than staying pinned to their
	   natural content width once there is no longer a sibling to share the
	   row with. */
	flex: 1 1 auto;
}

.bpp-sticky-bar__thumb {
	width: 40px;
	height: 40px;
	border-radius: 6px;
	object-fit: cover;
	flex: 0 0 auto;
}

.bpp-sticky-bar__title {
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	color: var(--bpp-ink);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.bpp-sticky-bar__price {
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 16px;
	color: var(--bpp-ink);
}

/*
 * FIX (86eypb6me q33): the sticky bar's own CTA was inheriting .bpp-btn's
 * default state (Button, Kind=Secondary, Size=Large -- green fill, white
 * text), but Figma's sticky-bar instance is a DIFFERENT button variant,
 * Kind=Primary (node 34515:109363, "State=Default, Kind=Primary,
 * Size=Large", frame "STICKY BUTON/DESKTOP" 34515:109204, fileKey
 * cY9QX6IwGVaaGI4r7J2RRF, read live 2026-09-23): fill r=1 g=0.75686
 * b=0.14510 = #FFC125 with a 1px r=0.72157 g=0.43137 b=0 = #B86E00 stroke,
 * black label. #FFC125/#B86E00 already have --bpp-yellow/--bpp-yellow-
 * stroke tokens defined two-hundred-some lines above (this file's own
 * Button/Secondary/Large HOVER state reuses the same fill), so this is
 * that same fill/stroke pair applied as this ONE button's default state
 * rather than its hover state -- not a new colour. Scoped to
 * .bpp-sticky-bar .bpp-btn only: the real <button type="submit"> Add to
 * Cart elsewhere in the buy box IS Kind=Secondary and stays green.
 * Contrast: black text on #FFC125 is already measured at the top of this
 * file, ~13:1, passes AAA -- same pair, not reaudited here.
 */
.bpp-sticky-bar .bpp-btn {
	padding: 10px 20px;
	flex: 0 0 auto;
	background: var(--bpp-yellow);
	color: var(--bpp-black);
	border: 1px solid var(--bpp-yellow-stroke);
}

/*
 * FIX (86eypb6me q33): quantity stepper + wishlist button, either side of
 * the CTA above. Figma "STICKY BUTON/DESKTOP" (node 34515:109204, fileKey
 * cY9QX6IwGVaaGI4r7J2RRF, read live 2026-09-23) draws a qty "Button Group"
 * (node 34515:109359: 132x48, radius 999, white fill, 1px stroke
 * r=1 g=0.65882 b=0.14510 = #FFA825 = --bpp-amber, three 40/52/40 segments)
 * and a wishlist "Button, Kind=Outline, Size=Large" (node 34515:109364:
 * 52x48, same radius/fill/stroke as the qty group). This is the ONLY
 * sticky-bar frame in the file (authored at 1512 wide, no separate mobile/
 * tablet variant). The values below are this bar's MOBILE/TABLET sizing
 * (under 1024px); 86eypb6me q18-buybox S1 (2026-09-24) restored the bar
 * on desktop too and gives it this same frame's own LITERAL height/gap
 * there instead -- see the "@media (min-width: 1024px)" block further
 * down this file, right before the 743px breakpoint block. Below
 * 1024px, no separate mobile/tablet Figma frame exists, so widths stay
 * Figma-exact and only the HEIGHT comes down, from Figma's 48px to
 * 43px -- the already-shipped .bpp-sticky-bar__cta's own rendered
 * height, measured live 2026-09-23, so the row reads as one height
 * rather than the added controls standing taller than the button
 * between them.
 *
 * Both are PROXIES for the real controls in the main buy box
 * (form.cart .quantity[data-type="type-2"] and
 * button.ct-wishlist-button-single), wired in js/product-page.js
 * (bppInitStickyBarProxies) rather than a second implementation: the real
 * controls' click handling binds once, at Blocksy's own init pass over
 * whatever is in the DOM at page load, confirmed live 2026-09-23 by
 * cloning the real quantity control and finding its clone's .ct-increase
 * did nothing on click (before/after value both "1"). The real elements
 * stay exactly where they are; these forward their clicks to the real
 * buttons and mirror the real controls' state back onto themselves.
 */
.bpp-sticky-bar__actions {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px;
	/* Half of the Figma desktop row's own 16px itemSpacing (Frame
	   1261154504/427318915): no separate compact-width Figma frame exists
	   to read a real gap from at this bar's own rendered widths, so this
	   is scaled down with the same 48px -> 43px control height above,
	   not a second invented number. */
}

.bpp-sticky-bar__qty {
	display: flex;
	align-items: stretch;
	flex: 0 0 132px;
	height: 43px;
	border: 1px solid var(--bpp-amber);
	border-radius: 999px;
	background: var(--bpp-white);
	overflow: hidden;
}

.bpp-sticky-bar__qty-btn {
	flex: 0 0 40px;
	display: flex;
	align-items: center;
	justify-content: center;
	background: none;
	border: none;
	padding: 0;
	margin: 0;
	color: var(--bpp-ink);
	font-family: var(--bpp-heading-font);
	font-size: 16px;
	line-height: 1;
	cursor: pointer;
}

.bpp-sticky-bar__qty-btn:disabled {
	opacity: 0.4;
	cursor: not-allowed;
}

.bpp-sticky-bar__qty-btn:focus-visible {
	outline: 2px solid var(--bpp-ink);
	outline-offset: -2px;
}

/* Figma text node 22:37605 (the qty group's own digit): Nominee Regular
   400, 18px/24px, letter-spacing 0.1px, fill r=0.18039 g=0.16471
   b=0.16471 = #2E2A2A = --bpp-ink -- the exact same value and the exact
   same Figma node type the main buy box's own qty input already reads
   (product-page.css, form.cart .quantity input.qty). */
.bpp-sticky-bar__qty-value {
	flex: 0 0 52px;
	display: flex;
	align-items: center;
	justify-content: center;
	font-family: var(--bpp-heading-font);
	font-weight: 400;
	font-size: 18px;
	letter-spacing: 0.1px;
	color: var(--bpp-ink);
}

.bpp-sticky-bar__wishlist {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	height: 43px;
	padding: 0 12px;
	margin: 0;
	border: 1px solid var(--bpp-amber);
	border-radius: 999px;
	background: var(--bpp-white);
	color: var(--bpp-green);
	cursor: pointer;
}

.bpp-sticky-bar__wishlist:focus-visible {
	outline: 2px solid var(--bpp-ink);
	outline-offset: 2px;
}

.bpp-sticky-bar__wishlist svg {
	width: 18px;
	height: 18px;
	display: block;
}

/*
 * Mirrors the real button's own active-state fill. Not this file's
 * invention -- measured live 2026-09-23 by toggling the real
 * button.ct-wishlist-button-single and reading getComputedStyle on its
 * own .ct-heart-fill path: unwishlisted is fill:none/opacity:0 (below),
 * wishlisted is fill:rgb(255,193,37) = #FFC125 = --bpp-yellow, opacity 1.
 * js/product-page.js keeps data-button-state on this proxy in step with
 * the real button's own attribute of the same name.
 */
.bpp-sticky-bar__wishlist .ct-heart-fill {
	fill: none;
	opacity: 0;
}

.bpp-sticky-bar__wishlist[data-button-state="active"] .ct-heart-fill {
	fill: var(--bpp-yellow);
	opacity: 1;
}

/* 86eypb6me q18-buybox S1 (2026-09-24): the bar used to be display:none
   above 1024px, on the reasoning that "the real buy box is already on
   screen" at desktop widths. Reverted: Figma's own sticky-bar frame
   (STICKY BUTON/DESKTOP, node 34515:109204, fileKey
   cY9QX6IwGVaaGI4r7J2RRF) is ITSELF a 1512-wide desktop frame, so the
   bar is designed to reveal at every breakpoint once the real buy box
   scrolls out of view -- exactly what bppInitStickyBar()'s
   IntersectionObserver in product-page.js already does, with no
   breakpoint condition of its own (confirmed by reading the function:
   it only watches `.single-product form.cart`'s intersection ratio,
   nothing viewport-width-gated). This block gives the bar this frame's
   own literal metrics at desktop, rather than the mobile/tablet
   scaled-down proxy sizing above (43px control height, 8px action gap),
   which were deliberate compromises for widths this Figma file draws no
   frame for. This IS that frame's own breakpoint, so it gets the real
   numbers, all measured live 2026-09-24 against the extractor's own
   JSON (chunk ...-34926-77030 / ...-76931-BASIC-PRODUCT-PAGE---D):
   - 96px bar height via 16px top/bottom padding: root frame 96px tall,
     "CART'S CARD" child (34515:109206) 64px tall, (96-64)/2=16.
   - 64px side padding: root frame 1512 wide, product-card block 678
     wide + actions cluster 706 wide, (1512-678-706)/2=64, each flush to
     its own margin.
   - 16px gap inside the actions cluster: measured between the qty
     group/CTA/wishlist instances' own absolute x-positions (17828 to
     17844, and 18012 to 18028 -- both 16px), not the 8px halving used
     above for narrower widths.
   - 48px qty/wishlist height (Figma's own, not the 43px mobile/tablet
     compromise, since this frame supplies a real desktop number).
   align-items:center on .bpp-sticky-bar (unchanged, set above) centers
   both the 64px-tall product block and the 48px-tall actions block
   independently within whatever content-box height the symmetric
   padding leaves, so one padding value correctly centers both without
   extra math (confirmed via MDN's flex alignment docs: align-items
   centers each flex item independently along the cross axis regardless
   of sibling height). Placed AFTER every base .bpp-sticky-bar__actions/
   __qty/__wishlist rule above rather than back near the top of this
   section, since a media-query override sitting BEFORE a later
   equal-specificity plain rule for the same selector loses to it at
   every width, including inside the override's own range. */
@media (min-width: 1024px) {
	.bpp-sticky-bar {
		padding: 16px 64px;
	}

	.bpp-sticky-bar__actions {
		gap: 16px;
	}

	.bpp-sticky-bar__qty,
	.bpp-sticky-bar__wishlist {
		height: 48px;
	}

	/* FIX (86eypb6me q18-fix3): bar still measured 80px tall at 1440 (16px
	   padding + 48px tallest child), not Figma's 96px (16px padding + 64px
	   tallest child -- node 34926:77035 "STICKY BUTON/DESKTOP", chunk
	   34926-77030, read live 2026-09-24). The 16px padding and 48px actions
	   height above already match that frame; the missing piece is the
	   product block's own thumbnail, still the 40x40 mobile/tablet proxy
	   size here -- Figma's own "88616-11 1" rectangle inside "PRODUCT CARD"
	   (34926:77038/77039) is 64x64, which is what makes "CART'S CARD" (the
	   product block) the frame's tallest child (64px) rather than the 48px
	   actions cluster. Bumping the thumb to that same 64x64 makes
	   .bpp-sticky-bar__product hit 64px too, and 64 + 16*2 = 96, matching
	   Figma with no other change -- confirmed live 2026-09-24 (80px before,
	   96px after). */
	.bpp-sticky-bar__thumb {
		width: 64px;
		height: 64px;
	}
}

@media (max-width: 743px) {
	.single-product h1.product_title.entry-title {
		font-size: 20px;
	}

	.single-product p.price {
		font-size: 22px;
	}

	.bpp-sticky-bar__title {
		max-width: 40vw;
	}
}

@media (min-width: 744px) {
	.single-product div.product.type-product {
		padding: 0 32px;
	}

	.single-product .ct-breadcrumbs {
		padding: 11px 32px 20px;
	}

	.single-product .bc-recently-viewed.is-width-constrained {
		padding-inline: 32px;
	}
}

@media (min-width: 1024px) {
	.single-product div.product.type-product {
		padding: 0 64px;
	}

	.single-product .ct-breadcrumbs {
		padding: 11px 64px 20px;
	}

	.single-product .bc-recently-viewed.is-width-constrained {
		padding-inline: 64px;
	}

	.single-product h1.product_title.entry-title {
		font-size: 30px;
	}
}

/* editor-friendly-ok: same exemption as the file docblock.
   z8udzkebk5: PDP edges follow the visible header card
   (#header [data-row="middle"]): it insets 24px from 768px and 64px from
   1440px, uncapped. Below 768px the card is flush, so the 20px page gutter
   stays. */
body.single-product {
	--bpp-edge: 20px;
}

@media (min-width: 768px) {
	body.single-product {
		--bpp-edge: 24px;
	}
}

@media (min-width: 1440px) {
	body.single-product {
		--bpp-edge: 64px;
	}
}

body.single-product div.product.type-product,
body.single-product .ct-breadcrumbs {
	max-width: none;
	padding-left: var( --bpp-edge );
	padding-right: var( --bpp-edge );
}

body.single-product .bc-recently-viewed.is-width-constrained {
	max-width: none;
	padding-inline: var( --bpp-edge );
}

/* Gallery and summary split the row equally, so both edges meet the header's. */
@media (min-width: 1024px) {
	body.single-product .product-entry-wrapper > .woocommerce-product-gallery,
	body.single-product .product-entry-wrapper > div.summary.entry-summary {
		flex: 1 1 0;
		min-width: 0;
		max-width: none;
		width: auto;
	}
}

/* "Choose the frequency" panel: subscribe vs one-time (86eypb6me).
 *
 * Figma, corrected 2026-09-16. An earlier revision of this block said Figma
 * drew no purchase-mode control; it draws one on both PDP variants.
 *   Superfoods  34197:205208 (group) > 34197:205210 heading,
 *               34197:205345 subscribe panel, 34518:61660 one-time panel
 *   Biotics     34920:65935  (group) > 34920:65937 heading,
 *               34920:65943 subscribe panel, 34920:65938 one-time panel
 * Component sets: _Radio button 5161:81988 (Default/Selected x Focus),
 *                 subscription variant 34190:189784 (default/hover/selected
 *                 x Devices Default|Desktop).
 *
 * Geometry is identical at 428, 744 and 1512: the panel is 12px 18px 22px
 * with a 12px gap, the one-time panel 18px with a 12px gap, interval cards
 * 10px padding / 6px gap / 12px between. Only type scales, which is why the
 * media queries below touch font-size and nothing else.
 *
 * Two Figma colours are darkened, following the precedent already set for
 * the out-of-stock badge at the top of this file:
 *   - the save pill and the discounted price are drawn in #FF0000, which is
 *     4.0:1 on white and on #FFF9E9. At 14px bold that is not WCAG "large
 *     text", so it fails the 4.5:1 floor the contract commits to. Both use
 *     --bpp-red-text (#C6362B) instead, ~5.3:1.
 *   - the struck "was" price is drawn in #9CA3AF, 2.4:1 on #FFF9E9. It uses
 *     #6B7280 instead, which is Figma's own note colour from the same panel,
 *     so the substitute stays inside the drawn palette.
 *
 * The save pill's #C6362B is a standing decision, not a candidate to revert
 * (86eypb6me, 2026-09-17): WCAG 2.2 AA is contractual on this project
 * across five languages and outranks colour parity here. Flagged to the
 * designer as a Figma-source defect, not an implementation bug.
 *
 * TOP margin added 86eypb6me (buy-box queue): custom.php now hooks the FAQ
 * link (and, Superfoods only, the daily-portion panel) directly ahead of
 * this panel inside the same woocommerce_before_add_to_cart_button area,
 * matching Figma's reading order, so .bpm needs its own top gap regardless
 * of which of those precedes it -- see the shared rule just above
 * .bpp-faq-teaser__question below for why neither of THEM carries a bottom
 * margin instead. The panel's own BOTTOM margin (spacing before the
 * quantity/button row) is unchanged. */
.bpm {
	display: flex;
	flex-direction: column;
	gap: 12px;
	margin: 20px 0 20px;
}

.bpm__heading {
	margin: 0;
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var(--bpp-ink);
}

.bpm__panel {
	border-radius: 12px;
}

/* 34197:205345 / 34920:65943 */
.bpm__panel--sub {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding: 12px 18px 22px;
	background: #FFF9E9;
	border: 2px solid #FFA825;
}

/* 34518:61660 / 34920:65938 */
.bpm__panel--otp {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding: 18px;
	background: var(--bpp-white);
	border: 1px solid rgba(0, 0, 0, 0.05);
}

/* Subscribe head 34518:62048: space-between, wraps at narrow widths so the
   save pill drops to its own line exactly as the 428 frame shows. */
.bpm__head {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	justify-content: space-between;
	gap: 0;
	margin: 0;
	cursor: pointer;
}

/* One-time head 34518:62093: a 12px gap, price group fills to the right. */
.bpm__panel--otp .bpm__head {
	flex-wrap: nowrap;
	justify-content: flex-start;
	gap: 12px;
}

.bpm__headline {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 4px;
	flex: 1 1 auto;
}

.bpm__panel--otp .bpm__headline {
	flex: 0 0 auto;
	flex-wrap: nowrap;
}

/* The input IS the radio Figma draws (16x16, 1px stroke, 10x10 dot inset 3px
   from the outer edge). Styling it directly rather than hiding it behind a
   decorative span keeps the focus ring on the real control. */
.bpm__radio {
	-webkit-appearance: none;
	appearance: none;
	position: relative;
	flex: 0 0 auto;
	width: 16px;
	height: 16px;
	margin: 0;
	padding: 0;
	border: 1px solid #6B7280; /* 4.83:1 on white, WCAG 1.4.11 (X15) */
	border-radius: 50%;
	background: var(--bpp-white);
	cursor: pointer;
}

.bpm__panel--sub .bpm__radio,
.bpm__radio:checked {
	border-color: #8A5200; /* 6.08:1 on the #FFF9E9 selected card (X15); dot stays amber */
}

/* inset 2px sits inside the 1px border, so the dot is 10x10 starting 3px
   from the outer edge, matching 5161:81997. */
.bpm__radio:checked::after {
	content: "";
	position: absolute;
	inset: 2px;
	border-radius: 50%;
	background: #FFA825;
}

/* _Radio button Focus=True (5161:81992/81998): 2px white then 4px #9B1B1F. */
.bpm__radio:focus-visible {
	outline: none;
	box-shadow: 0 0 0 2px var(--bpp-white), 0 0 0 4px #9B1B1F;
}

/* Windows High Contrast discards box-shadow entirely, which would leave these
   radios with no focus indicator at all, so a real outline comes back there. */
@media (forced-colors: active) {
	.bpm__radio:focus-visible {
		outline: 2px solid Highlight;
		outline-offset: 2px;
	}
}

/* ==========================================================================
   Unavailable purchase-mode leg (ClickUp 86eypb6me, cross-check 2026-09-21)
   ==========================================================================
   The panel used to be suppressed entirely whenever either leg was out of
   stock, which on Boost (131519 and its twin 132462, both out of stock)
   removed the Subscribe / One-time control altogether while production still
   rendered it with the one-time leg visible at its price and disabled. See
   bonza_pdp_purchase_mode_context() for the full reasoning.

   WHAT IS DELIBERATELY NOT DONE HERE: the Figma Button set's Disabled state
   (29049:58925) is `opacity: 0.6` plus its own fill and label colour, and the
   first draft of this rule applied that 0.6 to the whole panel. Measured, that
   is a contrast failure, not a style choice. Compositing #6B7280 label text at
   0.6 over the #FFF9E9 panel gives an effective #A6AAB3 on #FFFBF2, which is
   2.25:1 against a 4.5:1 requirement. Figma's 0.6 is legitimate on a disabled
   BUTTON, which WCAG 1.4.3 exempts, but this panel is not a button: its whole
   justification for still rendering is that the shopper can READ the price of
   the mode they cannot currently buy. Dimming the thing the panel exists to
   show would defeat the fix and fail AA doing it.

   So the panel keeps its normal contrast, and only the radio, which IS a
   disabled user-interface component and is exempt under 1.4.11, takes the
   muted treatment. The badge carries the message.

   Not using `pointer-events: none` as the mechanism either. The radio carries
   a real `disabled` attribute, so it is already unreachable by mouse, by
   keyboard and by form submission, and bonza_pdp_add_otp_to_cart() re-checks
   stock server-side. This rule is only how that reads. */
/* The one-time head is `flex-wrap: nowrap` (see .bpm__panel--otp .bpm__head
   above), which was fine until this badge joined the row.

   Measured on the deployed page before this rule existed: at 320 and 375 the
   badge rendered at left 305 in a panel whose right edge is 300 (320) / 355
   (375), so it sat outside the panel AND outside the viewport, invisible. The
   row is radio 16 + headline 162 + prices 52 + two 12px gaps = 254px against
   242px of available width, so the 136px badge never had anywhere to go. At
   1440 that row is 598px wide and the badge fits inline, which is why this
   only breaks narrow and why a desktop-only check would have passed it.

   Letting the head wrap drops the badge to its own line exactly when it does
   not fit, with no media query and no change to the wide layout. The subscribe
   head already wraps, which is why its badge was never affected. */
.bpm__panel--unavailable .bpm__head {
	flex-wrap: wrap;
}

/* Disabled radio: exempt from the 3:1 UI-boundary rule (WCAG 1.4.11, which
   excludes inactive components), and visibly inert beside a live one. */
.bpm__panel--unavailable .bpm__radio {
	border-color: #D1D5DB;
	background: var(--bpp-cream);
	cursor: not-allowed;
}

/* Out-of-stock marker on the unavailable leg. Production puts the same words
   in a span.out-of-stock-badge on its own otp-price-container.

   Colour: --bpp-red-text (#C6362B) on --bpp-cream (#F8EDE3) measures 4.60:1,
   clearing AA for this 12px label. Production's own badge is the raw Figma red
   #FF6256 behind #F8EDE3, which measures 2.55:1 and fails; the site's existing
   darkened token is used instead, the same substitution already documented at
   the top of this file for out-of-stock badge text.

   In normal flow rather than production's absolute -10px offset: this panel is
   a flex column with its own padding, and an absolutely positioned badge would
   sit outside the rounded border at 320px. */
.bpm__oos-badge {
	flex: 0 0 auto;
	order: 2;
	padding: 4px 10px;
	border-radius: 999px;
	background: var(--bpp-red-text);
	color: var(--bpp-cream);
	font-family: var(--bpp-heading-font);
	font-size: 12px;
	font-weight: 900;
	line-height: 16px;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	white-space: nowrap;
}

.bpm__title {
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var(--bpp-ink);
}

.bpm__panel--sub .bpm__title {
	color: var(--bpp-green);
}

/* price-save 34518:62080 */
.bpm__save-pill {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 4px 8px;
	border-radius: 8px;
	background: var(--bpp-red-text);
	color: var(--bpp-white);
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: normal;
}

.bpm__save-pill[hidden],
.bpm__price-was[hidden],
.bpm__price-note[hidden] {
	display: none;
}

/* 34518:62032 */
.bpm__prices {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	justify-content: center;
	gap: 4px;
}

.bpm__panel--otp .bpm__prices {
	flex: 1 1 auto;
}

/* z8udzketvn: price row and note share one left edge, inset 28px (radio +
 * gap) to the title column; flex-end right-aligned them to each other. */
.bpm__panel--sub .bpm__prices {
	align-items: flex-start;
	padding-inline-start: 28px;
}

.bpm__price-row {
	display: flex;
	align-items: center;
	gap: 4px;
}

.bpm__price-was,
.bpm__price-now {
	font-family: var(--bpp-heading-font);
	font-weight: 900;
	font-size: 14px;
	line-height: 16px;
	letter-spacing: 0.02em;
	background: none;
}

.bpm__price-was {
	color: #6B7280;
	text-decoration: line-through;
}

.bpm__price-now {
	color: var(--bpp-ink);
	text-decoration: none;
}

.bpm__panel--sub .bpm__price-now {
	color: var(--bpp-red-text);
}

/* 34520:62160 — the cost-per-day or delivery-schedule line. */
.bpm__price-note {
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 12px;
	line-height: 14px;
	color: #6B7280;
}

/* 35239:79736 label 8px above its cards, 16px between cards and benefits. */
.bpm__detail {
	display: flex;
	flex-direction: column;
	gap: 16px;
}

.bpm__intervals {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.bpm__intervals-label {
	margin: 0;
	font-family: var(--bpp-body-font);
	font-weight: 400;
	font-size: 14px;
	line-height: 16px;
	color: var(--bpp-ink);
}

/* 34197:205352 */
.bpm__interval-cards {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}

/* subscription variant 34197:204767 (default) */
.bpm__interval-card {
	flex: 1 1 0;
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 6px;
	padding: 10px;
	border: 1px solid var(--bpp-cream);
	border-radius: 8px;
	background: var(--bpp-white);
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: 14px;
	color: var(--bpp-ink);
	cursor: pointer;
}

/* 34197:204770 (hover): fill tints, stroke stays #F8EDE3. */
.bpm__interval-card:hover {
	background: rgba(255, 193, 37, 0.05);
}

/* 34197:204773 (selected): same tint, stroke swaps to #FFA825. */
.bpm__interval-card.is-selected {
	background: rgba(255, 193, 37, 0.05);
	border-color: #FFA825;
}

.bpm__interval-card:focus-visible {
	outline: 2px solid var(--bpp-green);
	outline-offset: 2px;
}

/* "Delivered every" weeks picker (ClickUp 86eyy8hqa), Superfoods only.
   Same card as the interval cards above, with a fixed 54px border-box height
   because Figma's stroke sits inside. Desktop 34197:204767 / :204770 / :204773,
   mobile 34190:189783 / :189782 / :189781. */
.bpm__weeks {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.bpm__weeks[hidden] {
	display: none;
}

.bpm__weeks-label {
	margin: 0;
	font-family: var(--bpp-body-font);
	font-weight: 400;
	font-size: 14px;
	line-height: 16px;
	color: var(--bpp-ink);
}

.bpm__weeks-row {
	display: flex;
	gap: 12px;
}

.bpm__week {
	box-sizing: border-box;
	flex: 1 1 0;
	min-width: 0;
	height: 54px;
	margin: 0;
	padding: 0 10px;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 6px;
	border: 1px solid var(--bpp-cream);
	border-radius: 8px;
	background: var(--bpp-white);
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: 14px;
	text-align: center;
	color: var(--bpp-ink);
	cursor: pointer;
}

/* Hover: fill tints, stroke stays #F8EDE3. */
.bpm__week:hover {
	background: rgba(255, 193, 37, 0.05);
}

/* Selected: tint + #FFA825 stroke. The inset #B86E00 ring is a WCAG 1.4.11
   deviation from Figma (#FFA825 on white is ~2:1), same as the meal-plan page. */
.bpm__week.is-selected {
	background: rgba(255, 193, 37, 0.05);
	border-color: var(--bpp-amber);
	box-shadow: inset 0 0 0 1px #B86E00;
}

.bpm__week:focus-visible {
	outline: 2px solid var(--bpp-green);
	outline-offset: 2px;
}

@media (max-width: 428px) {
	.bpm__week {
		height: 40px;
		font-size: 10px;
		line-height: 10px;
	}
}

/* 34197:205357 / 34920:65960 */
.bpm__benefits {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.bpm__benefit {
	display: flex;
	align-items: center;
	gap: 2px;
	margin: 0;
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 14px;
	line-height: normal;
	color: var(--bpp-ink);
}

.bpm__benefit svg {
	flex: 0 0 auto;
	width: 16px;
	height: 16px;
	color: var(--bpp-green);
}

/* No rule hides the variations picker in one-time mode, deliberately.
 *
 * There was one, keyed on a `bpm-one-time` class the purchase-mode script put
 * on form.cart. Its premise was wrong: the dog-weight and pack-size options
 * are a WooCommerce variation ATTRIBUTE, not a subscription setting, so a
 * shopper who switches to a one-time purchase still has to pick one. The rule
 * hid the entire picker, and with it any way to choose a variation at all.
 *
 * The script no longer sets that class, so this selector matched nothing and
 * is gone rather than left as dead weight. */

/* Tablet 744 (I23310:35546): Links/Bold stays 14px, Accent/Pills drops to
   12px. Mobile 428 (I29629:112443): Links/Bold 12px, Accent/Pills 10px,
   interval cards 12px, and the struck price keeps its 14px ts1 override. */
@media (max-width: 1023px) {
	.bpm__save-pill,
	.bpm__benefit {
		font-size: 12px;
	}
}

@media (max-width: 743px) {
	.bpm__heading,
	.bpm__title,
	.bpm__price-now {
		font-size: 12px;
	}

	.bpm__save-pill,
	.bpm__benefit {
		font-size: 10px;
	}

	.bpm__intervals-label,
	.bpm__interval-card {
		font-size: 12px;
	}
}

/* Feefo rating row, below the product title. Figma node 34203:83813.
 *
 * Every value below is taken from that node, not eyeballed: the outer row
 * wraps with a 10.5523px gap, the stars-plus-rating group uses a 7.9142px
 * gap, each star is 14.13x14.13 filled #FFA825, the Feefo logo is 70x17 and
 * the award badge 33.75x33.75.
 *
 * Font note: Figma specs the rating value in "Floyd" Caps 400 18px uppercase.
 * Floyd has no @font-face anywhere on this site, so the Nominee preset var is
 * used as the family here. That is a substitution needing a design decision,
 * not a resolved match, and it is flagged in the PHP docblock too.
 *
 * Config investigated: no Blocksy or WooCommerce setting renders a review
 * aggregate in the product summary. The installed Feefo plugin
 * (black-studio-feefo-entersaleremotely) only exports completed orders TO
 * Feefo for review-request emails; it ships no display widget or shortcode,
 * so there is no native surface to reuse. */
.bpp-feefo-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 10.5523px;
	text-decoration: none;
	color: var(--bpp-ink);
}

.bpp-feefo-row__inner {
	display: flex;
	align-items: center;
	gap: 7.9142px;
}

.bpp-feefo-row__stars {
	display: inline-flex;
	align-items: center;
	gap: 2.7801px;
}

.bpp-feefo-row__stars svg {
	width: 14.13px;
	height: 14.13px;
	color: #FFA825;
	fill: currentColor;
}

.bpp-feefo-row__rating {
	font-family: var(--wp--preset--font-family--nominee, "Nominee", sans-serif);
	font-weight: 900;
	font-size: 18px;
	text-transform: uppercase;
	color: var(--bpp-green);
}

.bpp-feefo-row__award {
	font-family: var(--wp--preset--font-family--kobe, "Kobe1.1", sans-serif);
	font-weight: 700;
	font-size: 12px;
	line-height: 14px;
	color: var(--bpp-ink);
}

.bpp-feefo-row__brand {
	display: inline-flex;
	align-items: center;
	gap: 10.5523px;
}

.bpp-feefo-row__brand svg:first-child {
	width: 70px;
	height: 17px;
}

.bpp-feefo-row__brand svg:last-child {
	width: 33.75px;
	height: 33.75px;
}

.bpp-feefo-row:hover .bpp-feefo-row__award,
.bpp-feefo-row:focus-visible .bpp-feefo-row__award {
	text-decoration: underline;
}

/* No reviews yet (z8udzkgnh3): plain muted text in the rating-row slot, no stars,
 * no link. min-height matches the Feefo badge so the row is as tall as a rated one. */
html body.single-product p.bpp-feefo-row--empty {
	min-height: 33.75px;
	margin: 0;
	font-family: var(--wp--preset--font-family--kobe, "Kobe1.1", sans-serif);
	font-weight: 700;
	font-size: 12px;
	line-height: 14px;
	/* Solid muted colour, no opacity: #5E5856 is 6.06:1 on cream #F8EDE3 (7.0:1 on
	 * white, 6.65:1 on #FFF9E9). The old ink at 0.6 opacity blended to ~#7F7874,
	 * 3.76:1, under the 4.5:1 WCAG 2.2 AA minimum for 12px bold text. */
	color: var(--wp--preset--color--bpp-muted, #5E5856);
}

/* The shared parent theme renders its own "In Stock" / "Out of Stock" line as
 * p.bc-stock-status, from an ANONYMOUS closure in blocksy-child/inc, which is
 * code outside this monorepo and cannot be targeted by remove_action. Our own
 * .bpp-stock-row conveys the same state with the Figma wording, so the shared
 * one is hidden here. display:none rather than visibility or clipping, so it
 * also leaves the accessibility tree and screen readers announce the status
 * once rather than twice. */
.single-product .bc-stock-status {
	display: none;
}

/* No price in the product summary. Figma's title block (34190:83335) runs
 * trust row, title, Feefo row, stock state, and carries no price; the buy box
 * below shows price per pack-size option instead.
 *
 * Two identical ranges were rendering live. One came from this theme moving
 * WooCommerce's default price hook to priority 29, now removed outright. The
 * other is not produced by this file's hooks, so it is hidden here.
 *
 * Scoped to DIRECT children of the summary on purpose, so this rule cannot
 * reach into the add-to-cart form, and to `body.bpm-active` so it only
 * applies where the frequency panel actually replaces the price. Without
 * that second scope every simple one-time product rendered a buy box with no
 * price at all: caught in code review, confirmed live on product 115368. */
body.bpm-active .single-product div.summary.entry-summary > p.price,
body.bpm-active.single-product div.summary.entry-summary > p.price {
	display: none;
}

/* WooCommerce's own variation price, hidden only where the frequency panel
 * replaces it.
 *
 * An earlier revision of the comment above argued this had to keep rendering
 * "so a shopper still sees what the selected option costs". That was true
 * before the panel existed. The panel now states the price, the saving and
 * the schedule itself (£24.00 struck -> £21.60, "Delivered every 20 days ·
 * 3 chews a day"), so .woocommerce-variation-price is a verbatim third copy,
 * and Figma's buy box (34920:65914) draws no price line there.
 *
 * `body.bpm-active` is set by bonza_pdp_purchase_mode_body_class() on exactly
 * the pages that render the panel. Scoping matters: a first attempt hid
 * `p.price` on EVERY single-product page, which left every simple one-time
 * product with no price anywhere in its buy box. Caught in code review and
 * confirmed live on product 115368.
 *
 * .woocommerce-variation-availability is deliberately NOT hidden. It was, and
 * that was wrong: it is the only PER-VARIATION stock signal on the page. The
 * title-group line (34190:83353) is product-level and does not change when a
 * shopper picks a pack size, so hiding the variation line meant an
 * out-of-stock variation still read "In stock" until add-to-cart failed. A
 * duplicated "In stock" is a cosmetic cost; a hidden out-of-stock warning is
 * a real one. */
body.bpm-active .single-product .woocommerce-variation-price,
body.bpm-active.single-product .woocommerce-variation-price {
	display: none;
}

/* The duplicated "In stock" under the option cards, and ONLY that.
 *
 * Figma draws one stock line, in the title group (34190:83353). WooCommerce
 * prints a second one per variation inside .woocommerce-variation-availability,
 * which lands directly under the option cards and repeats what the buy box
 * already says.
 *
 * Only the `.in-stock` case is hidden. `.out-of-stock` and
 * `.available-on-backorder` stay visible, because this element is the only
 * per-variation stock signal on the page: the title-group line is
 * product-level and does not change when a shopper picks a pack size. Hiding
 * the whole element was tried once before and meant an out-of-stock variation
 * still read as available until add-to-cart failed. A duplicated "In stock" is
 * cosmetic; a hidden out-of-stock warning is not. */
body.bpm-active .single-product .woocommerce-variation-availability p.stock.in-stock,
body.bpm-active.single-product .woocommerce-variation-availability p.stock.in-stock {
	display: none;
}

/* A third stock statement, WooCommerce's own default markup inside
 * .ct-product-add-to-cart (the Back-In-Stock signup block), duplicates what
 * .bpp-stock-row already says in the title group -- confirmed live on
 * boost-bioactive-bites-otp (86eypb6me, 2026-09-17): both read the buy box
 * as out of stock, once even in different casing ("Out of Stock" vs "Out of
 * stock"), which reads as a bug to a shopper.
 *
 * :has() is the mechanism, not decoration, and it follows the same
 * fail-safe direction as the .in-stock rule above: this only hides the
 * native line when .bpp-stock-row--out is actually present to replace it.
 * If that row ever fails to render -- a hook reorder, a product type it
 * does not cover, an upstream error -- the :has() stops matching and the
 * native warning comes back on its own. A bare
 * `p.stock.out-of-stock { display: none }` would fail the other way:
 * silently hiding a real out-of-stock warning with nothing left to replace
 * it, which is the one direction this element must never fail in.
 *
 * NOT the same element as .bc-stock-status (the shared parent theme's own
 * title-group line, hidden unconditionally a few rules up in this file) --
 * that one shares no classes with p.stock.out-of-stock at all, so this
 * selector was never going to reach it and does not need to. */
body.single-product .summary:has(.bpp-stock-row--out) p.stock.out-of-stock {
	display: none;
}

/* Gallery radii. Figma: main image 16px (29477:50490), thumbnails 8px with a
 * 2px #FF6256 stroke on the active one (34186:83168).
 *
 * Blocksy renders this gallery, not WooCommerce flexslider, so the real
 * structure is .ct-product-gallery-container > .flexy-container > .flexy >
 * .flexy-view for the main image and .flexy-pills ol li for the thumbnails.
 * Verified in the rendered page before writing these: 6 thumbnails, both
 * containers present, all radii computing 0 beforehand. */
.single-product .flexy-view,
.single-product .flexy-view img,
.single-product .ct-media-container img {
	border-radius: 16px;
}

.single-product .flexy-pills ol li,
.single-product .flexy-pills ol li img {
	border-radius: 8px;
}

/* Blocksy paints the beige page tone on span.ct-media-container behind each
 * image with radius 0, so square patches showed at the corners of the rounded
 * images. The wrapper takes its image's radius and clips, and its fill is
 * transparent so the bars an image leaves under `object-fit: contain` (the
 * main image, whose source isn't always square) show the page background
 * instead of an off-tone beige. The selectors carry the gallery class to
 * outrank `.single-product .woocommerce-product-gallery .ct-media-container
 * { background: #F5EFE6 }` (specificity 0,3,0), lines 9-11 of
 * product-page-extras.css, added by open PR #1311 (z8udzkept0, S1 "show the
 * whole image, never crop it"). That PR is not on main yet; these selectors
 * are 0,4,0 and above, so they win whichever file loads last. S1's
 * `object-fit: contain` stays: the image is still shown whole, only the bars
 * round it now take the page colour. */
.single-product .woocommerce-product-gallery .flexy-view .ct-media-container {
	border-radius: 16px;
	overflow: hidden;
	background: transparent;
}

.single-product .woocommerce-product-gallery .flexy-pills ol li .ct-media-container {
	border-radius: 8px;
	overflow: hidden;
	background: transparent;
}

/* Thumbnail row, Figma gallery column (…;34920:65843 itemSpacing 16 desktop
 * and tablet, 8 mobile; thumbnails in …;34920:65855, itemSpacing 8, five
 * across: 128 of 672, 132.8 of 696, 71.2 of 388). Blocksy ships li at 22.22%
 * with 15px top and 7.5px side padding, so the main-to-thumb gap was 15 and
 * the fifth thumbnail was cut by the frame. Five fit exactly instead; the
 * sixth scrolls in the ol. */
.single-product .flexy-pills {
	padding-top: 16px;
}

.single-product .flexy-pills ol {
	margin: 0;
	gap: 8px;
}

/* Prefixed to beat Blocksy's own `flex: 0 0 22.2222%` on the same li. */
html body.single-product .woocommerce-product-gallery .flexy-pills ol > li {
	flex: 0 0 calc((100% - 32px) / 5);
	padding: 0;
}

@media (max-width: 743px) {
	.single-product .flexy-pills {
		padding-top: 8px;
	}
}

/* Stacked layout: Figma puts 20px between the thumbnail row and the next
 * block (tablet 34926:86442, mobile 34926:91318); Blocksy left 48px. */
@media (max-width: 999px) {
	.single-product .woocommerce-product-gallery {
		margin-bottom: 20px;
	}
}

/* Gallery slide counter, Figma 35226:80550.
 *
 * Drawn 16px from the left and 16px from the bottom of the main image: the
 * node sits at x=16, y=632 inside a 672-tall frame and is 24px tall, so
 * 672 - 632 - 24 = 16. Padding 4px 12px, 999px radius, rgba(255,255,255,0.7)
 * fill, label in Kobe1.1 Bold 12px/16px.
 *
 * .flexy-view is `position: static` as Blocksy ships it, verified on the
 * deployed page, so it needs a containing block before the counter can be
 * placed against it.
 *
 * Contrast: the pill is translucent over a photograph, so the worst case is
 * the fill over pure black, which composites to rgb(178,178,178). Ink on that
 * measures 6.69:1, and over a white image it is 13.72:1. Both clear the
 * 4.5:1 floor, so no case of this needs a solid fill. */
.single-product .flexy-view {
	position: relative;
}

.bpp-gallery-counter {
	position: absolute;
	left: 16px;
	bottom: 16px;
	z-index: 2;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 4px 12px;
	border-radius: 999px;
	background: rgba(255, 255, 255, 0.7);
	font-family: var(--bpp-body-font);
	font-weight: 700;
	font-size: 12px;
	line-height: 16px;
	color: var(--bpp-ink); /* 6.69:1 worst case, over the fill composited on black */
}

/* Active thumbnail: Figma gives it a 2px #FF6256 stroke. Blocksy marks the
 * current one with .active on the li. Selection is not conveyed by colour
 * alone, the gallery script also tracks aria-current, so this satisfies
 * WCAG 1.4.1 as a supporting indicator rather than the only one. */
.single-product .flexy-pills ol li.active img {
	outline: 2px solid var(--bpp-red-figma);
	outline-offset: -2px;
}

/* Breadcrumb alignment. The trail's text was starting at x=124 while the
 * gallery starts at x=64, because Blocksy wraps it in
 * .hero-section.is-width-constrained (max-width 1290px, auto-centred) which
 * offsets .entry-header to x=60, and this file then added another 64px of
 * padding on top.
 *
 * That hero section contains ONLY the breadcrumb on this template (verified:
 * its single child is .entry-header, whose single child is nav.ct-breadcrumbs),
 * so un-constraining it here is safe and affects nothing else.
 *
 * The breadcrumb then takes the product container's own box, max-width 1512px
 * with 64px side padding, so its text lines up with the gallery edge exactly.
 * Figma spacing: 40px above the trail, 20px gap below it to the product row
 * (the container 15005:150737 uses gap 20px). */
.single-product .hero-section.is-width-constrained {
	max-width: none;
	margin-inline: 0;
	margin-bottom: 0;
}

/*
 * Breadcrumb spacing, CORRECTED 2026-09-23 (86eypb6me). The 40px-above note
 * further up came from an older frame (15005:150737). The current PDP frames
 * put the trail 20px below the header at 1512, 11px at 744 and 8px at 428,
 * and the product row 20px below the trail at all three.
 *
 * Measured on v1 before this change, text top to header bottom was 100px at
 * 1512, because three layers stacked: Blocksy's content-area padding (60px,
 * 50px on mobile) on .ct-container-full, this file's 40px on the trail, and
 * below it a 40px margin on the hero wrapper, so 60px to the product row.
 *
 * Config checked: Blocksy's single-product "Content Area Vertical Spacing"
 * (Customizer) can drop the top padding on its own, but it is a database
 * setting that would have to be repeated on production by hand; this file
 * already owns the breadcrumb geometry, so the top padding is removed here
 * alongside it, product pages only.
 */
.single-product .ct-container-full {
	padding-top: 0;
}

@media (min-width: 1440px) {
	.single-product .ct-breadcrumbs {
		padding-top: 20px;
	}
}

.single-product .entry-header {
	width: 100%;
}

/* Everything on this page sits on ONE gutter, and two blocks were not on it.
 *
 * Figma puts the breadcrumb and the gallery/summary row in the same frame,
 * 15005:150737, at `padding: 0 64px`, and the marketing sections below at
 * `padding: 64px` (34631:78347). The header agrees: both of its rows are
 * `padding: 0 64px` (I30803:166868;29600:56712 and ;34096:257876). So the
 * desktop layout is a 64px gutter throughout, 1384px of content at 1512.
 *
 * Measured on the deployed page at 1512, the breadcrumb was the only thing
 * honouring it. Its first crumb sat at x=64 while the gallery, the summary
 * and the related-products row all sat at x=111, which is Blocksy's
 * .is-width-constrained: a 1290px max-width, auto-centred. All eight
 * .bppm-section blocks already measured 64 / 1384 and are left alone.
 *
 * div.product.type-product already carries the 20 / 32 / 64 ladder, so the
 * entry wrapper only needs its inner constraint released. The related row
 * sits outside that element, so it takes the ladder itself.
 *
 * The site header is off the same gutter for the same reason, its logo
 * measuring x=111 against Figma's 64 plus the nav pill's own 20px inset.
 * That is Blocksy's global 1290px container rather than anything on this
 * template, so it is raised as a finding rather than patched from here. */
.single-product div.product.type-product .product-entry-wrapper.is-width-constrained {
	/* width, not just max-width. Blocksy's .is-width-constrained carries
	   `width: 90%` as well as the 1290px cap, so releasing the cap alone left
	   the row at 1361px of the available 1384 and the summary 23px short of
	   the gutter. */
	width: 100%;
	max-width: none;
	margin-inline: 0;
}



/* Blocks Figma does not draw in the buy box: Blocksy's product-info row
 * (Calculate Shipping / Return / FAQ), its own standalone Wishlist text link,
 * and the SKU / Category meta lines. The Figma wishlist is a heart icon beside
 * Add to Cart instead, built separately, so this hides the default without
 * removing wishlist functionality from the page. */
.single-product .bc-product-info-items,
.single-product .ct-product-additional-actions,
.single-product .product_meta {
	display: none;
}

/* The FAQ teaser question is now a link that opens the Meal Plan Wizard
   off-canvas. It keeps the question's own type and colour rather than picking
   up the global link styling. */
.bpp-faq-teaser__link {
	color: inherit;
	text-decoration: none;
}

.bpp-faq-teaser__link:hover,
.bpp-faq-teaser__link:focus-visible {
	text-decoration: underline;
}

/* Gallery zoom trigger, Figma 29477:50491.
 *
 * The symbol was wrong, not just the box. WooCommerce outputs this control as
 * a literal magnifying-glass EMOJI in an anchor
 * (`<a class="woocommerce-product-gallery__trigger">`), and Blocksy hides that
 * text and paints its own glyph through a `::before` mask-image data URI,
 * which draws the four-corners "expand" symbol. Figma uses
 * heroicons-solid/magnifying-glass-plus (5357:101267), a magnifier with a plus
 * in it, so the mask is repointed at that exported asset. Masking rather than
 * a background image keeps the glyph recolourable.
 *
 * Box, measured against the node: the button is hug at 4px padding around a
 * 16x16 icon, so 24x24, filled rgba(255, 255, 255, 0.7) at a 999px radius,
 * inset 16px from the top and right of the 672px image frame. It was
 * rendering 40x40, fully opaque white, 20px in, with Blocksy's slate icon
 * colour.
 *
 * Figma draws no hover or focus state for this control, so both are this
 * theme's own: the fill goes solid on hover to confirm the target, and focus
 * keeps a real ring because a keyboard user needs one. */
.single-product .woocommerce-product-gallery__trigger {
	width: 24px;
	height: 24px;
	padding: 4px;
	top: 16px;
	right: 16px;
	background: rgba(255, 255, 255, 0.7);
	border-radius: 999px;
	box-sizing: border-box;
	color: var(--bpp-ink);
}

.single-product .woocommerce-product-gallery__trigger::before {
	width: 16px;
	height: 16px;
	/* Blocksy's bundle/woocommerce.min.css positions this pseudo-element
	   `absolute; inset: 0`. Once it carries an explicit 16px size, `top` and
	   `left` win over `bottom` and `right`, so the glyph pinned to the box's
	   top-left corner instead of its centre (measured on v1, 2026-09-19: box
	   24x24, glyph at 0,0). With all four insets already 0, `margin: auto`
	   centres an absolutely positioned box of known size on both axes. */
	margin: auto;
	background-color: currentColor;
	mask-image: url("../assets/product-page/gallery-zoom.svg");
	mask-repeat: no-repeat;
	mask-position: center;
	mask-size: 16px 16px;
	-webkit-mask-image: url("../assets/product-page/gallery-zoom.svg");
	-webkit-mask-repeat: no-repeat;
	-webkit-mask-position: center;
	-webkit-mask-size: 16px 16px;
}

.single-product .woocommerce-product-gallery__trigger:hover {
	background: var(--bpp-white);
}

.single-product .woocommerce-product-gallery__trigger:focus-visible {
	outline: 2px solid var(--bpp-ink);
	outline-offset: 2px;
}

/* 86eycc4d1 round 2: the review card under the payment marks sat 4px below
   them (only the summary's 4px row-gap). Figma buy box 34920:65863 closes
   the payment row (34920:66022) to the next card (34920:66032) at 24px, the
   same group rhythm as the rest of the summary, so 20px margin + 4px
   row-gap = 24. Round-1 gaps above are untouched. */
.single-product div.summary.entry-summary > .bppm-buybox-reviews {
	margin-top: 20px;
}
